From cff06656b0b13e49953e34e1fb326cee5be911ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 17:22:38 +0000 Subject: [PATCH] feat: add pooled-model exploration (PRD + FundingBeacon + StabilityPool skeleton) Captures the move from the isolated StabilityVault/StabilityOffer model to a pooled model in response to quant feedback (positions non-fungible, Seeker redemptions act as enforced margin calls). Variant B is chosen: Seekers transfer-only, no on-chain redeem to BTC; Provider exit gated by leverage. Index-based funding accrual via a standalone FundingBeacon. - docs/stability-pool-prd.md: full design doc (state model, leverage gates, contract surface, phased build, open questions). - examples/stability/funding_beacon.ark (Phase 1): monotone cumulative yield-index oracle; mirrors PriceBeacon shape. - examples/stability/stability_pool.ark (Phase 2 skeleton): provider-only flows (deposit, leverage-gated withdraw) with index-based accrual. - examples/stability/provider_share.ark (Phase 2 skeleton): per-Provider share UTXO; share-equity dilution math marked TODO for Phase 2 completion. Compiles cleanly; full test suite passes; cargo fmt clean. Isolated-model contracts and tests are intentionally left in place until the pooled model is feature-complete. --- docs/stability-pool-prd.md | 274 +++++++++++ examples/stability/funding_beacon.ark | 124 +++++ examples/stability/funding_beacon.json | 482 +++++++++++++++++++ examples/stability/provider_share.ark | 49 ++ examples/stability/provider_share.json | 101 ++++ examples/stability/stability_pool.ark | 229 +++++++++ examples/stability/stability_pool.json | 619 +++++++++++++++++++++++++ 7 files changed, 1878 insertions(+) create mode 100644 docs/stability-pool-prd.md create mode 100644 examples/stability/funding_beacon.ark create mode 100644 examples/stability/funding_beacon.json create mode 100644 examples/stability/provider_share.ark create mode 100644 examples/stability/provider_share.json create mode 100644 examples/stability/stability_pool.ark create mode 100644 examples/stability/stability_pool.json diff --git a/docs/stability-pool-prd.md b/docs/stability-pool-prd.md new file mode 100644 index 0000000..b1c6422 --- /dev/null +++ b/docs/stability-pool-prd.md @@ -0,0 +1,274 @@ +# StabilityPool — Product Requirements Document (v0, exploration) + +**Status:** Exploration / planning. Not yet approved for build. +**Variant chosen:** B — perpetual-bond (Seeker transfer-only, Provider exit +gated by leverage). +**Accrual model:** Index-based (monotone yield index published by +`FundingBeacon`). +**Funding rate source:** Standalone `FundingBeacon` (separate from +`PriceBeacon`). +**Replaces (when shipped):** `StabilityVault` + `StabilityOffer` +isolated/segregated model from `docs/stability-vault-prd.md`. + +This document captures the design only. The accompanying contracts +(`examples/stability/funding_beacon.ark`, `examples/stability/stability_pool.ark`, +`examples/stability/provider_share.ark`) are Phase-1 / Phase-2 skeletons +and intentionally leave the deeper settlement math to later phases. + +--- + +## 1. Motivation + +The isolated model gives every position its own `fundingSatPerBlock` and its +own post-open leverage. Quant feedback (Christian, Slack 2026-05-XX): + +- Positions are non-fungible, so Providers and Seekers churn continuously to + capture best market conditions. +- Seekers redeeming when BTC drops acts as a margin call enforced *by* Seekers, + forcing Providers to settle at the worst time. +- Settling in/out of BTC on every Seeker churn is operationally and + economically painful. + +The pooled model collapses isolated positions into a single covenant. Funding +rate is common to all users and set by an oracle. Leverage is a system-wide +ratio that gates entries and exits. Seekers in Variant B never redeem to BTC +on-chain — they transfer the claim, and exits to fiat happen through swap +services. + +--- + +## 2. Actors + +| Actor | Pooled-model role | +|---|---| +| **Seeker** | Holds a transferable USD-cent claim against the pool. Cannot redeem to BTC on-chain. | +| **Provider** | Holds a pro-rata claim on `providerCapital`. Can withdraw only when `leverage` is below a configured floor. | +| **Rate Oracle** | Publishes the cumulative `yieldIndex` on `FundingBeacon`. Trust-critical in Variant B. | +| **Price Oracle** | Unchanged. Publishes BTC/USD on `PriceBeacon`. | +| **Swap Service** | Bridge between Seeker claims and USDT/USDC. Primary Seeker exit path. | +| **Arkade Operator** | Co-signs cooperative spends. Same role as today. | + +--- + +## 3. Economic model (pooled) + +``` +Pool state at any tx: + totalCapital = pool UTXO value in sats + aggregateSeekerUSD = Σ targetUSD of all live SeekerShares (cents) + poolYieldIndex = pool's last-snapshotted yield index + currentPrice = PriceBeacon.ticker quantity (cents/BTC) + currentIndex = FundingBeacon.yieldIndex quantity + +Derived: + seekerCapitalNominal = aggregateSeekerUSD × 1e8 / currentPrice + fundingAccrued = aggregateSeekerUSD × (currentIndex - poolYieldIndex) / INDEX_SCALE + seekerCapital = seekerCapitalNominal + fundingAccrued + providerCapital = totalCapital − seekerCapital + leverage = totalCapital / providerCapital +``` + +Notes: +- `INDEX_SCALE` is a fixed denominator (proposed: `1e8`) so the index + can move with sat-precision per cent of USD. +- `seekerCapital` is a *claim*, not a held balance. The pool BTC stays fungible. +- Leverage uses the post-accrual derived values; deposits/withdrawals must + refresh the index before gating. + +### Gating constants (Variant B) + +| Constant | Proposed value | Rationale | +|---|---|---| +| `MAX_LEVERAGE_X100` | 167 | Seekers cannot push leverage past 1.67×. | +| `PROVIDER_WITHDRAW_LEVERAGE_X100` | 150 | Providers can only exit if leverage ≤ 1.50×. Tighter than Seeker cap to keep system from skating the edge. | +| `INDEX_SCALE` | 100_000_000 | Sat-per-cent precision on funding accrual. | +| `STALE_BLOCKS` | 144 | Same as PriceBeacon. | + +These are deploy-time constants for v0. Later they can be parameterised. + +### Action table + +| Action | Caller | Gate | +|---|---|---| +| Provider deposit | Provider | always allowed | +| Provider withdraw | Provider | `leverage_after ≤ PROVIDER_WITHDRAW_LEVERAGE_X100 / 100` | +| Seeker entry | Seeker (via swap service) | `leverage_after ≤ MAX_LEVERAGE_X100 / 100` | +| Seeker transfer | Seeker | always allowed | +| Seeker split | Seeker | always allowed | +| Seeker redeem to BTC | — | **disallowed in Variant B** | +| Force-unwind | anyone | `totalCapital < seekerCapital` (insolvency) | + +--- + +## 4. Contract surface + +``` +PriceBeacon — unchanged +FundingBeacon — new, Phase 1 +StabilityPool — new, Phase 2 (singleton covenant) +ProviderShare — new, Phase 2 (per-Provider UTXO) +SeekerShare — new, Phase 3 (per-Seeker UTXO, transferable USD claim) +``` + +### 4.1 FundingBeacon (Phase 1) + +Dual-asset oracle: +- `yieldTicker` quantity = cumulative `yieldIndex`, monotone non-decreasing. +- `yieldClock` quantity = block height of last update. + +Functions: `update(oracleSig, newIndex, newHeight)`, `passthrough()`, +`migrate(oracleSig, newOraclePk)` — same shape as `PriceBeacon`. + +The on-chain contract enforces only: +- `newIndex ≥ currentIndex` (monotone) +- `newHeight ≥ currentHeight` (monotone) +- `newHeight ≥ currentHeight` for the same Bitcoin block is permitted to + support sub-block updates (same as PriceBeacon). + +The off-chain oracle is trusted to compute +`newIndex - oldIndex = fundingSatPerBlock × INDEX_SCALE × (newHeight - oldHeight)`. + +### 4.2 StabilityPool (Phase 2) + +Constructor (immutables + state): + +``` +StabilityPool( + bytes32 priceTicker, // PriceBeacon ticker asset id + bytes32 priceClock, // PriceBeacon clock asset id + bytes32 yieldTicker, // FundingBeacon yield-index asset id + bytes32 yieldClock, // FundingBeacon clock asset id + int aggregateSeekerUSD, // STATE: Σ live SeekerShare.targetUSD (cents) + int poolYieldIndex, // STATE: last-snapshotted yield index + int exit +) +``` + +`aggregateSeekerUSD` and `poolYieldIndex` are part of the script, so every +spend creates a new pool UTXO with updated state. + +Tx layout convention (for all pool spends): + +``` +input[0]: StabilityPool +input[1]: PriceBeacon (passthrough) +input[2]: FundingBeacon (passthrough) +input[3+]: Caller's UTXO(s) (provider deposit sats, share UTXO for withdraw, …) + +output[0]: New StabilityPool +output[1]: PriceBeacon passthrough +output[2]: FundingBeacon passthrough +output[3+]: Caller's outputs (new ProviderShare, payout SingleSig, …) +``` + +Functions (Phase 2 — provider-only flows): + +| Function | Inputs | Effects | +|---|---|---| +| `providerDeposit` | `int depositSats`, `pubkey providerPk` | pool.value += deposit; mint ProviderShare(providerPk, deposit, currentIndex) | +| `providerWithdraw` | `signature providerSig`, `int withdrawSats`, `int providerCapitalBefore` | check leverage gate; burn ProviderShare; pay sats to provider | + +Functions (Phase 3+): + +| Function | Notes | +|---|---| +| `seekerEntry` | Mints SeekerShare. Gated by `MAX_LEVERAGE_X100`. | +| `seekerTransfer` | Recursive — produces new SeekerShare with same `targetUSD` and `entryIndex`. No pool touch needed (pool state unchanged). | +| `seekerSplit` | Recursive — produces two SeekerShares with proportional `targetUSD` shares. | +| `accrue` | Refreshes `poolYieldIndex` against FundingBeacon. Anyone can call. | +| `forceUnwind` | Insolvency path. Permissionless. Pays out seekers pro-rata. | + +### 4.3 ProviderShare (Phase 2) + +``` +ProviderShare( + pubkey providerPk, + int depositedSats, // sats committed at entry + int entryIndex, // FundingBeacon index at entry + int exit +) +``` + +Effective value at withdraw time = `depositedSats - (aggregateSeekerUSD-share × Δindex / scale)`. +Detailed math is deferred to Phase 2 implementation. The skeleton in this +PR documents the surface; the production math comes after the doc is +reviewed. + +### 4.4 SeekerShare (Phase 3) + +``` +SeekerShare( + pubkey seekerPk, + int targetUSD, // USD cents + int entryIndex, // FundingBeacon index at entry + int exit +) +``` + +In Variant B, SeekerShare has `transfer` and `split` only. No +`seekerRedeem` function. Exit to fiat is via swap services. + +--- + +## 5. Open design questions + +1. **Provider equity dilution math.** With many providers entering at different + `yieldIndex` values, fair payout on withdraw needs to weight each share by + its time-in-pool. Two options to evaluate in Phase 2: + (a) per-share `entryIndex` + simple linear depreciation, or + (b) ERC-4626-style "share token" with a price-per-share. (a) is simpler in + UTXO; (b) is fairer. + +2. **Force-unwind partitioning.** A single tx cannot pay out all Seekers. + Likely shape: a permissionless `redeemPro(seekerShareIn)` function active + only when `totalCapital < seekerCapital`, paying the share's pro-rata claim + on `totalCapital`. The pool itself does not unwind atomically; Seekers + unwind their own shares against the halted pool. + +3. **Anti-gaming.** 0.1% entry/exit fee + 1-block price-snapshot delay. + Fee: skim into pool (helps Providers). Delay: require the price beacon + read to be at least 1 block stale on Seeker entry / Provider withdraw. + Cost: worsens UX. Defer until Phase 6. + +4. **Sharding.** A singleton pool serializes all activity. If throughput + becomes a problem, partition the pool by series (one pool per + `(priceTicker, yieldTicker, series_id)`). v0 ships singleton. + +5. **Rate-cap.** Christian flagged death-spiral risk if rate is unbounded in + distress. Decide whether to cap the on-chain index growth rate. Strongly + recommend yes for v0. + +6. **Index unit.** `INDEX_SCALE = 1e8` gives sat-precision per cent of USD. + Worth running the numbers at $1B aggregateSeekerUSD to confirm no + overflow risk (Arkade ints are 64-bit signed → `1e10 × 1e8 = 1e18`, fits). + +--- + +## 6. Build phases + +| Phase | Output | Status | +|---|---|---| +| 0 | This PRD | In this PR | +| 1 | `FundingBeacon` contract + tests | In this PR (Phase 1 done) | +| 2 | `StabilityPool` skeleton (provider deposit/withdraw) + `ProviderShare` | In this PR (skeleton only) | +| 3 | `SeekerShare` (transfer + split) + `seekerEntry` on pool | Future PR | +| 4 | `accrue` function + index integration completeness | Future PR | +| 5 | `forceUnwind` insolvency path | Future PR | +| 6 | Anti-gaming (fees + price-snapshot delay) + rate cap | Future PR | + +Each phase ships with integration tests in `tests/` and example fixtures in +`examples/stability/`. + +--- + +## 7. What this PR explicitly does NOT do + +- Does not delete the isolated `StabilityVault` / `StabilityOffer` / their + tests. They remain shipping until the pooled model is feature-complete and + audited. +- Does not implement Seeker-side flows on the pool. +- Does not implement the full provider-equity-dilution math; the + ProviderShare skeleton currently encodes `(providerPk, depositedSats, + entryIndex)` but the withdraw math is a TODO. +- Does not implement `accrue`, `forceUnwind`, or anti-gaming. +- Does not parameterise the gating constants. They are hard-coded for v0. diff --git a/examples/stability/funding_beacon.ark b/examples/stability/funding_beacon.ark new file mode 100644 index 0000000..137747a --- /dev/null +++ b/examples/stability/funding_beacon.ark @@ -0,0 +1,124 @@ +// FundingBeacon Contract +// +// Pooled-model funding-rate oracle. Publishes a monotone cumulative +// yield index for the StabilityPool. Distinct from PriceBeacon — this +// contract carries the funding rate, not BTC/USD price. +// +// Asset layout: +// - yieldTicker (bytes32): identifies the funding feed (e.g. commitment to +// "BTC-USD-FUNDING-v1"). The asset's quantity is the cumulative +// yieldIndex in INDEX_SCALE units (proposed scale: 1e8). Monotone +// non-decreasing. +// - yieldClock (bytes32): block height of the last update. Same +// semantics as PriceBeacon.clock — Bitcoin block height for +// nLockTime parity with `tx.time`. +// +// Index semantics (off-chain trust, on-chain monotonicity): +// newIndex - oldIndex +// == fundingSatPerBlock × INDEX_SCALE × (newHeight - oldHeight) +// The oracle is trusted to compute the increment. The contract only +// enforces that the index never decreases. +// +// Consumers (StabilityPool, SeekerShare) read the current index and +// snapshot it. Funding accrual for an aggregate USD claim from +// entryIndex to currentIndex is: +// accruedSats = aggregateUSD × (currentIndex - entryIndex) / INDEX_SCALE +// +// Staleness: same convention as PriceBeacon. Consumers enforce +// tx.time - yieldClock <= 144 (≈ 24 hours) +// +// Trust model: v0 is single-oracle, reputation-based. v1 should be +// threshold-of-N. The off-chain oracle is required to publish a +// rate-capped sequence to avoid distress death-spirals (see PRD §5). + +options { + server = server; + exit = exit; +} + +contract FundingBeacon( + bytes32 yieldTicker, // asset whose quantity = cumulative yield index + bytes32 yieldClock, // asset whose quantity = block height of last update + pubkey oraclePk, // authorized updater + int exit // exit timelock in blocks +) { + + // ------------------------------------------------------------------------- + // UPDATE + // Oracle publishes a new cumulative index and block height. Both are + // monotonically non-decreasing. Same-block updates are permitted so the + // oracle can refresh the index in sub-block cadence on Arkade. + // ------------------------------------------------------------------------- + function update(signature oracleSig, int newIndex, int newBlockHeight) { + require(checkSig(oracleSig, oraclePk), "invalid oracle signature"); + + int currentIndex = tx.inputs[0].assets.lookup(yieldTicker); + int currentHeight = tx.inputs[0].assets.lookup(yieldClock); + + require(newIndex >= currentIndex, "index must not regress"); + require(newBlockHeight >= currentHeight, "block height must not regress"); + + require( + tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, oraclePk, exit), + "beacon script must survive" + ); + require( + tx.outputs[0].assets.lookup(yieldTicker) == newIndex, + "index not updated correctly" + ); + require( + tx.outputs[0].assets.lookup(yieldClock) == newBlockHeight, + "block height not updated correctly" + ); + } + + // ------------------------------------------------------------------------- + // PASSTHROUGH + // Any transaction reading the beacon routes it through passthrough. + // Both yieldTicker and yieldClock assets must be preserved. + // ------------------------------------------------------------------------- + function passthrough() { + require( + tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, oraclePk, exit), + "beacon script must survive" + ); + + int currentIndex = tx.inputs[0].assets.lookup(yieldTicker); + require( + tx.outputs[0].assets.lookup(yieldTicker) >= currentIndex, + "index asset must survive" + ); + + int currentHeight = tx.inputs[0].assets.lookup(yieldClock); + require( + tx.outputs[0].assets.lookup(yieldClock) >= currentHeight, + "clock asset must survive" + ); + } + + // ------------------------------------------------------------------------- + // MIGRATE + // Transfers oracle authority to a new key. Index and block height are + // preserved. Asset IDs are stable across rotations so existing pool + // contracts remain valid. + // ------------------------------------------------------------------------- + function migrate(signature oracleSig, pubkey newOraclePk) { + require(checkSig(oracleSig, oraclePk), "invalid oracle signature"); + + int currentIndex = tx.inputs[0].assets.lookup(yieldTicker); + int currentHeight = tx.inputs[0].assets.lookup(yieldClock); + + require( + tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, newOraclePk, exit), + "invalid new beacon" + ); + require( + tx.outputs[0].assets.lookup(yieldTicker) == currentIndex, + "index must be preserved" + ); + require( + tx.outputs[0].assets.lookup(yieldClock) == currentHeight, + "block height must be preserved" + ); + } +} diff --git a/examples/stability/funding_beacon.json b/examples/stability/funding_beacon.json new file mode 100644 index 0000000..136c43f --- /dev/null +++ b/examples/stability/funding_beacon.json @@ -0,0 +1,482 @@ +{ + "contractName": "FundingBeacon", + "constructorInputs": [ + { + "name": "yieldTicker_txid", + "type": "bytes32" + }, + { + "name": "yieldTicker_gidx", + "type": "int" + }, + { + "name": "yieldClock_txid", + "type": "bytes32" + }, + { + "name": "yieldClock_gidx", + "type": "int" + }, + { + "name": "oraclePk", + "type": "pubkey" + }, + { + "name": "exit", + "type": "int" + } + ], + "functions": [ + { + "name": "update", + "functionInputs": [ + { + "name": "oracleSig", + "type": "signature" + }, + { + "name": "newIndex", + "type": "int" + }, + { + "name": "newBlockHeight", + "type": "int" + } + ], + "witnessSchema": [ + { + "name": "oracleSig", + "type": "signature", + "encoding": "schnorr-64" + }, + { + "name": "newIndex", + "type": "int", + "encoding": "scriptnum" + }, + { + "name": "newBlockHeight", + "type": "int", + "encoding": "scriptnum" + }, + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "signature" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL", + "", + "", + "OP_GREATERTHANOREQUAL", + "", + "0", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,)>", + "OP_EQUAL", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_EQUAL", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_EQUAL", + "OP_VERIFY", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "update", + "functionInputs": [ + { + "name": "oracleSig", + "type": "signature" + }, + { + "name": "newIndex", + "type": "int" + }, + { + "name": "newBlockHeight", + "type": "int" + }, + { + "name": "oraclePkSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "oraclePkSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "nOfNMultisig", + "message": "1-of-1 signatures required (introspection fallback)" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + }, + { + "name": "passthrough", + "functionInputs": [], + "witnessSchema": [ + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "comparison" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "0", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,)>", + "OP_EQUAL", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "passthrough", + "functionInputs": [ + { + "name": "oraclePkSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "oraclePkSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "nOfNMultisig", + "message": "1-of-1 signatures required (introspection fallback)" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + }, + { + "name": "migrate", + "functionInputs": [ + { + "name": "oracleSig", + "type": "signature" + }, + { + "name": "newOraclePk", + "type": "pubkey" + } + ], + "witnessSchema": [ + { + "name": "oracleSig", + "type": "signature", + "encoding": "schnorr-64" + }, + { + "name": "newOraclePk", + "type": "pubkey", + "encoding": "compressed-33" + }, + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "signature" + }, + { + "type": "comparison" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "0", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,)>", + "OP_EQUAL", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_EQUAL", + "OP_VERIFY", + "0", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_EQUAL", + "OP_VERIFY", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "migrate", + "functionInputs": [ + { + "name": "oracleSig", + "type": "signature" + }, + { + "name": "newOraclePk", + "type": "pubkey" + }, + { + "name": "oraclePkSig", + "type": "signature" + }, + { + "name": "newOraclePkSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "oraclePkSig", + "type": "signature", + "encoding": "schnorr-64" + }, + { + "name": "newOraclePkSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "nOfNMultisig", + "message": "2-of-2 signatures required (introspection fallback)" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIGVERIFY", + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + } + ], + "source": "\noptions {\n server = server;\n exit = exit;\n}\n\ncontract FundingBeacon(\n bytes32 yieldTicker,\n bytes32 yieldClock,\n pubkey oraclePk,\n int exit\n) {\n\n function update(signature oracleSig, int newIndex, int newBlockHeight) {\n require(checkSig(oracleSig, oraclePk), \"invalid oracle signature\");\n\n int currentIndex = tx.inputs[0].assets.lookup(yieldTicker);\n int currentHeight = tx.inputs[0].assets.lookup(yieldClock);\n\n require(newIndex >= currentIndex, \"index must not regress\");\n require(newBlockHeight >= currentHeight, \"block height must not regress\");\n\n require(\n tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, oraclePk, exit),\n \"beacon script must survive\"\n );\n require(\n tx.outputs[0].assets.lookup(yieldTicker) == newIndex,\n \"index not updated correctly\"\n );\n require(\n tx.outputs[0].assets.lookup(yieldClock) == newBlockHeight,\n \"block height not updated correctly\"\n );\n }\n\n function passthrough() {\n require(\n tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, oraclePk, exit),\n \"beacon script must survive\"\n );\n\n int currentIndex = tx.inputs[0].assets.lookup(yieldTicker);\n require(\n tx.outputs[0].assets.lookup(yieldTicker) >= currentIndex,\n \"index asset must survive\"\n );\n\n int currentHeight = tx.inputs[0].assets.lookup(yieldClock);\n require(\n tx.outputs[0].assets.lookup(yieldClock) >= currentHeight,\n \"clock asset must survive\"\n );\n }\n\n function migrate(signature oracleSig, pubkey newOraclePk) {\n require(checkSig(oracleSig, oraclePk), \"invalid oracle signature\");\n\n int currentIndex = tx.inputs[0].assets.lookup(yieldTicker);\n int currentHeight = tx.inputs[0].assets.lookup(yieldClock);\n\n require(\n tx.outputs[0].scriptPubKey == new FundingBeacon(yieldTicker, yieldClock, newOraclePk, exit),\n \"invalid new beacon\"\n );\n require(\n tx.outputs[0].assets.lookup(yieldTicker) == currentIndex,\n \"index must be preserved\"\n );\n require(\n tx.outputs[0].assets.lookup(yieldClock) == currentHeight,\n \"block height must be preserved\"\n );\n }\n}", + "compiler": { + "name": "arkade-compiler", + "version": "0.1.0" + }, + "updatedAt": "2026-05-15T17:19:01.384798613+00:00", + "warnings": [ + "warning[type]: fn update: comparison '>=' mixes uint64le ('int') with scriptnum ('uint64le') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn update: comparison '>=' mixes uint64le ('int') with scriptnum ('uint64le') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn update: comparison '==' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn update: comparison '==' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control" + ] +} \ No newline at end of file diff --git a/examples/stability/provider_share.ark b/examples/stability/provider_share.ark new file mode 100644 index 0000000..0dd4654 --- /dev/null +++ b/examples/stability/provider_share.ark @@ -0,0 +1,49 @@ +// ProviderShare Contract (Phase 2 skeleton) +// +// Per-Provider UTXO representing a claim on StabilityPool.providerCapital. +// Holds the provider's pubkey, the sats they deposited at entry, and the +// FundingBeacon yield index at entry (so equity dilution from accrued +// Seeker funding can be computed at exit time). +// +// v0 enforcement scope: +// - redeem() checks the provider signature +// - redeem() requires the spending tx to spend a StabilityPool input +// (deferred: introspect input[0].scriptPubKey shape match — needs +// the pool's current state to instantiate, which the share doesn't +// directly observe; v0 trusts the pool to enforce the leverage gate) +// +// TODO (Phase 2 completion, post-PRD review): +// - Equity dilution: actual payout = depositedSats × (currentProviderCapital +// / providerCapitalAtEntry). Requires the share to read both indices and +// the pool's aggregateSeekerUSD/poolYieldIndex. One option: share carries +// a "shares-issued snapshot" rather than raw sats, mirroring ERC-4626. +// - Split / merge for partial withdraws. +// +// In the current skeleton, redeem() is a permission token only. The pool's +// providerWithdraw function decides how many sats may leave, gated by the +// system-wide leverage check. Fairness across providers is not yet enforced. + +options { + server = server; + exit = exit; +} + +contract ProviderShare( + pubkey providerPk, + int depositedSats, // sats committed at entry + int entryIndex, // FundingBeacon yield index at entry + int exit +) { + + // ------------------------------------------------------------------------- + // REDEEM + // Provider authorizes burning this share as part of a pool withdraw tx. + // The pool covenant (input[0]) enforces the actual sat-withdraw and the + // leverage gate. This function only authorizes the burn. + // ------------------------------------------------------------------------- + function redeem(signature providerSig) { + require(checkSig(providerSig, providerPk), "invalid provider sig"); + // TODO Phase 2 completion: enforce that input[0] is a StabilityPool and + // verify the share's fair equity claim against current pool state. + } +} diff --git a/examples/stability/provider_share.json b/examples/stability/provider_share.json new file mode 100644 index 0000000..f91a485 --- /dev/null +++ b/examples/stability/provider_share.json @@ -0,0 +1,101 @@ +{ + "contractName": "ProviderShare", + "constructorInputs": [ + { + "name": "providerPk", + "type": "pubkey" + }, + { + "name": "depositedSats", + "type": "int" + }, + { + "name": "entryIndex", + "type": "int" + }, + { + "name": "exit", + "type": "int" + } + ], + "functions": [ + { + "name": "redeem", + "functionInputs": [ + { + "name": "providerSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "providerSig", + "type": "signature", + "encoding": "schnorr-64" + }, + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "signature" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "redeem", + "functionInputs": [ + { + "name": "providerSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "providerSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "signature" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + } + ], + "source": "\noptions {\n server = server;\n exit = exit;\n}\n\ncontract ProviderShare(\n pubkey providerPk,\n int depositedSats,\n int entryIndex,\n int exit\n) {\n\n function redeem(signature providerSig) {\n require(checkSig(providerSig, providerPk), \"invalid provider sig\");\n }\n}", + "compiler": { + "name": "arkade-compiler", + "version": "0.1.0" + }, + "updatedAt": "2026-05-15T17:20:55.774781263+00:00" +} \ No newline at end of file diff --git a/examples/stability/stability_pool.ark b/examples/stability/stability_pool.ark new file mode 100644 index 0000000..585b082 --- /dev/null +++ b/examples/stability/stability_pool.ark @@ -0,0 +1,229 @@ +// StabilityPool Contract (Phase 2 skeleton — provider-only flows) +// +// Pooled-model replacement for the isolated StabilityVault + StabilityOffer. +// Variant B (perpetual-bond): +// - Seekers can transfer / split their claim, never redeem on-chain to BTC. +// - Providers can deposit any time; withdraw only when leverage <= 1.50. +// - Funding rate is common to all users, published by FundingBeacon as a +// monotone cumulative yield index. +// +// State carried in the script (mutable across recursive recreations): +// aggregateSeekerUSD — Σ targetUSD of all live SeekerShares (USD cents) +// poolYieldIndex — last snapshotted FundingBeacon index value +// +// Immutable script parameters: +// priceTicker, priceClock — PriceBeacon asset IDs +// yieldTicker, yieldClock — FundingBeacon asset IDs +// exit — exit timelock blocks +// +// Tx layout convention (all functions): +// input[0]: StabilityPool +// input[1]: PriceBeacon (passthrough) +// input[2]: FundingBeacon (passthrough) +// input[3+]: caller-side UTXOs (deposit funds, ProviderShare being burned, ...) +// +// output[0]: new StabilityPool +// output[1]: PriceBeacon (passthrough) +// output[2]: FundingBeacon (passthrough) +// output[3+]: caller-side outputs (new ProviderShare, SingleSig payout, ...) +// +// What this skeleton implements: +// - providerDeposit: extends pool, mints a ProviderShare +// - providerWithdraw: gates by post-withdraw leverage; pays sats to provider +// +// What this skeleton intentionally does NOT yet implement (future phases): +// - seekerEntry / seekerExit (no on-chain Seeker→BTC in Variant B; entry +// gated by 1.67× and routed via swap services — Phase 3) +// - accrue() to refresh poolYieldIndex without a flow event (Phase 4) +// - forceUnwind() insolvency path (Phase 5) +// - 0.1% entry/exit fee + 1-block price-snapshot delay (Phase 6) +// - Detailed ProviderShare equity math (per-share dilution) — owned by the +// ProviderShare contract; v0 placeholder there. +// +// Leverage math (Variant B): +// seekerCapitalNominal = aggregateSeekerUSD × 1e8 / currentPrice +// fundingAccrued = aggregateSeekerUSD × (currentIndex - poolYieldIndex) / INDEX_SCALE +// seekerCapital = seekerCapitalNominal + fundingAccrued +// totalAfter = pool.input.value ± delta +// providerCapitalAfter = totalAfter - seekerCapital +// leverage_after = totalAfter / providerCapitalAfter +// +// For PROVIDER_WITHDRAW_LEVERAGE_X100 = 150: +// require leverage_after <= 1.5 +// == totalAfter * 100 <= 150 × (totalAfter - seekerCapital) +// == 3 × seekerCapital <= totalAfter (clean integer form) +// +// INDEX_SCALE = 100_000_000 (1e8). See docs/stability-pool-prd.md §3. + +import "provider_share.ark"; +import "single_sig.ark"; + +options { + server = server; + exit = exit; +} + +contract StabilityPool( + bytes32 priceTicker, + bytes32 priceClock, + bytes32 yieldTicker, + bytes32 yieldClock, + int aggregateSeekerUSD, // STATE + int poolYieldIndex, // STATE + int exit +) { + + // ------------------------------------------------------------------------- + // PROVIDER DEPOSIT + // Provider adds sats to the pool, receives a ProviderShare UTXO. + // No leverage gate — deposits only reduce leverage. + // poolYieldIndex is refreshed to the FundingBeacon's current value, locking + // in accrued funding for already-live SeekerShares. + // ------------------------------------------------------------------------- + function providerDeposit(int depositSats, pubkey providerPk) { + require(depositSats >= 330, "deposit below dust"); + + // PriceBeacon passthrough (read but not used for the deposit gate) + int currentPrice = tx.inputs[1].assets.lookup(priceTicker); + require(currentPrice > 0, "invalid price beacon"); + int priceHeight = tx.inputs[1].assets.lookup(priceClock); + int priceAge = tx.time - priceHeight; + require(priceAge <= 144, "stale price oracle"); + + // FundingBeacon — read the current cumulative yield index + int currentIndex = tx.inputs[2].assets.lookup(yieldTicker); + int yieldHeight = tx.inputs[2].assets.lookup(yieldClock); + int yieldAge = tx.time - yieldHeight; + require(yieldAge <= 144, "stale funding oracle"); + require(currentIndex >= poolYieldIndex, "yield index regressed"); + + // Pool output: same immutables, unchanged aggregateSeekerUSD, + // refreshed yield index (so accrual baseline moves forward). + require( + tx.outputs[0].scriptPubKey == new StabilityPool( + priceTicker, priceClock, yieldTicker, yieldClock, + aggregateSeekerUSD, currentIndex, exit + ), + "invalid pool output" + ); + int poolValueAfter = tx.inputs[0].value + depositSats; + require( + tx.outputs[0].value >= poolValueAfter, + "pool deposit not credited" + ); + + // PriceBeacon passthrough + require( + tx.outputs[1].assets.lookup(priceTicker) >= currentPrice, + "price beacon must survive" + ); + require( + tx.outputs[1].assets.lookup(priceClock) >= priceHeight, + "price clock must survive" + ); + + // FundingBeacon passthrough + require( + tx.outputs[2].assets.lookup(yieldTicker) >= currentIndex, + "funding beacon must survive" + ); + require( + tx.outputs[2].assets.lookup(yieldClock) >= yieldHeight, + "funding clock must survive" + ); + + // ProviderShare output for the depositor + require( + tx.outputs[3].scriptPubKey == new ProviderShare( + providerPk, depositSats, currentIndex, exit + ), + "invalid provider share output" + ); + } + + // ------------------------------------------------------------------------- + // PROVIDER WITHDRAW + // Provider burns a ProviderShare (passed as input[3]) and reclaims sats. + // Gated by leverage_after <= 1.50 (PROVIDER_WITHDRAW_LEVERAGE_X100 = 150). + // + // The pool covenant verifies: + // - The post-withdraw leverage gate holds + // - The pool output value is correctly decremented + // - The provider receives a SingleSig output for withdrawSats + // + // ProviderShare equity math (how much a given share is actually entitled + // to vs depositedSats) is enforced in the ProviderShare contract's own + // redeem function. This skeleton trusts that contract; v0 enforcement is + // a TODO there. + // ------------------------------------------------------------------------- + function providerWithdraw(int withdrawSats, pubkey providerPk) { + require(withdrawSats >= 330, "withdraw below dust"); + + int currentPrice = tx.inputs[1].assets.lookup(priceTicker); + require(currentPrice > 0, "invalid price beacon"); + int priceHeight = tx.inputs[1].assets.lookup(priceClock); + int priceAge = tx.time - priceHeight; + require(priceAge <= 144, "stale price oracle"); + + int currentIndex = tx.inputs[2].assets.lookup(yieldTicker); + int yieldHeight = tx.inputs[2].assets.lookup(yieldClock); + int yieldAge = tx.time - yieldHeight; + require(yieldAge <= 144, "stale funding oracle"); + require(currentIndex >= poolYieldIndex, "yield index regressed"); + + // Compute current seeker capital claim including accrued funding. + int seekerCapitalNominal = aggregateSeekerUSD * 100000000 / currentPrice; + int deltaIndex = currentIndex - poolYieldIndex; + int fundingAccrued = aggregateSeekerUSD * deltaIndex / 100000000; + int seekerCapital = seekerCapitalNominal + fundingAccrued; + + int totalAfter = tx.inputs[0].value - withdrawSats; + require(totalAfter > seekerCapital, "withdraw would undercollateralise"); + + // Leverage gate: 3 × seekerCapital <= totalAfter ⇔ leverage_after <= 1.5 + int leverageBound = seekerCapital * 3; + require(leverageBound <= totalAfter, "leverage gate: provider withdraw blocked"); + + // The ProviderShare being burned is at input[3]; the share contract + // enforces its own redemption rules (signature check, equity math). + // The pool covenant here only enforces pool-level invariants. + + // Pool output: refreshed yield index, unchanged aggregateSeekerUSD, + // value reduced by withdrawSats. + require( + tx.outputs[0].scriptPubKey == new StabilityPool( + priceTicker, priceClock, yieldTicker, yieldClock, + aggregateSeekerUSD, currentIndex, exit + ), + "invalid pool output" + ); + require(tx.outputs[0].value >= totalAfter, "pool value mismatch"); + + // PriceBeacon passthrough + require( + tx.outputs[1].assets.lookup(priceTicker) >= currentPrice, + "price beacon must survive" + ); + require( + tx.outputs[1].assets.lookup(priceClock) >= priceHeight, + "price clock must survive" + ); + + // FundingBeacon passthrough + require( + tx.outputs[2].assets.lookup(yieldTicker) >= currentIndex, + "funding beacon must survive" + ); + require( + tx.outputs[2].assets.lookup(yieldClock) >= yieldHeight, + "funding clock must survive" + ); + + // Provider payout + require( + tx.outputs[3].scriptPubKey == new SingleSig(providerPk), + "output 3 not provider" + ); + require(tx.outputs[3].value >= withdrawSats, "provider underpaid"); + } +} diff --git a/examples/stability/stability_pool.json b/examples/stability/stability_pool.json new file mode 100644 index 0000000..6c1c25f --- /dev/null +++ b/examples/stability/stability_pool.json @@ -0,0 +1,619 @@ +{ + "contractName": "StabilityPool", + "constructorInputs": [ + { + "name": "priceTicker_txid", + "type": "bytes32" + }, + { + "name": "priceTicker_gidx", + "type": "int" + }, + { + "name": "priceClock_txid", + "type": "bytes32" + }, + { + "name": "priceClock_gidx", + "type": "int" + }, + { + "name": "yieldTicker_txid", + "type": "bytes32" + }, + { + "name": "yieldTicker_gidx", + "type": "int" + }, + { + "name": "yieldClock_txid", + "type": "bytes32" + }, + { + "name": "yieldClock_gidx", + "type": "int" + }, + { + "name": "aggregateSeekerUSD", + "type": "int" + }, + { + "name": "poolYieldIndex", + "type": "int" + }, + { + "name": "exit", + "type": "int" + } + ], + "functions": [ + { + "name": "providerDeposit", + "functionInputs": [ + { + "name": "depositSats", + "type": "int" + }, + { + "name": "providerPk", + "type": "pubkey" + } + ], + "witnessSchema": [ + { + "name": "depositSats", + "type": "int", + "encoding": "scriptnum" + }, + { + "name": "providerPk", + "type": "pubkey", + "encoding": "compressed-33" + }, + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "comparison" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "", + "OP_GREATERTHANOREQUAL", + "330", + "1", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "0", + "OP_GREATERTHAN", + "1", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "144", + "OP_LESSTHANOREQUAL", + "2", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "144", + "OP_LESSTHANOREQUAL", + "", + "OP_GREATERTHANOREQUAL", + "", + "0", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,,,,)>", + "OP_EQUAL", + "0", + "OP_INSPECTINPUTVALUE", + "", + "OP_SCRIPTNUMTOLE64", + "OP_ADD64", + "OP_VERIFY", + "0", + "OP_INSPECTOUTPUTVALUE", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "1", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "1", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "3", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,)>", + "OP_EQUAL", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "providerDeposit", + "functionInputs": [ + { + "name": "depositSats", + "type": "int" + }, + { + "name": "providerPk", + "type": "pubkey" + }, + { + "name": "providerPkSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "providerPkSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "nOfNMultisig", + "message": "1-of-1 signatures required (introspection fallback)" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + }, + { + "name": "providerWithdraw", + "functionInputs": [ + { + "name": "withdrawSats", + "type": "int" + }, + { + "name": "providerPk", + "type": "pubkey" + } + ], + "witnessSchema": [ + { + "name": "withdrawSats", + "type": "int", + "encoding": "scriptnum" + }, + { + "name": "providerPk", + "type": "pubkey", + "encoding": "compressed-33" + }, + { + "name": "serverSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": true, + "require": [ + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "assetCheck" + }, + { + "type": "comparison" + }, + { + "type": "comparison" + }, + { + "type": "serverSignature" + } + ], + "asm": [ + "", + "OP_GREATERTHANOREQUAL", + "330", + "1", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "0", + "OP_GREATERTHAN", + "1", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "144", + "OP_LESSTHANOREQUAL", + "2", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTINASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "144", + "OP_LESSTHANOREQUAL", + "", + "OP_GREATERTHANOREQUAL", + "", + "", + "OP_SCRIPTNUMTOLE64", + "100000000", + "OP_MUL64", + "OP_VERIFY", + "", + "OP_SCRIPTNUMTOLE64", + "OP_DIV64", + "OP_VERIFY", + "", + "OP_SCRIPTNUMTOLE64", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "OP_SCRIPTNUMTOLE64", + "", + "OP_SCRIPTNUMTOLE64", + "OP_MUL64", + "OP_VERIFY", + "100000000", + "OP_DIV64", + "OP_VERIFY", + "", + "OP_SCRIPTNUMTOLE64", + "", + "OP_SCRIPTNUMTOLE64", + "OP_ADD64", + "OP_VERIFY", + "0", + "OP_INSPECTINPUTVALUE", + "", + "OP_SCRIPTNUMTOLE64", + "OP_SUB64", + "OP_VERIFY", + "", + "", + "OP_GREATERTHAN", + "", + "OP_SCRIPTNUMTOLE64", + "3", + "OP_MUL64", + "OP_VERIFY", + "", + "", + "OP_LESSTHANOREQUAL", + "0", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ",,,,,,)>", + "OP_EQUAL", + "0", + "OP_INSPECTOUTPUTVALUE", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "1", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "1", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "2", + "", + "", + "OP_INSPECTOUTASSETLOOKUP", + "OP_DUP", + "OP_1NEGATE", + "OP_EQUAL", + "OP_NOT", + "OP_VERIFY", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "3", + "OP_INSPECTOUTPUTSCRIPTPUBKEY", + ")>", + "OP_EQUAL", + "3", + "OP_INSPECTOUTPUTVALUE", + "", + "OP_GREATERTHANOREQUAL64", + "OP_VERIFY", + "", + "", + "OP_CHECKSIG" + ] + }, + { + "name": "providerWithdraw", + "functionInputs": [ + { + "name": "withdrawSats", + "type": "int" + }, + { + "name": "providerPk", + "type": "pubkey" + }, + { + "name": "providerPkSig", + "type": "signature" + } + ], + "witnessSchema": [ + { + "name": "providerPkSig", + "type": "signature", + "encoding": "schnorr-64" + } + ], + "serverVariant": false, + "require": [ + { + "type": "nOfNMultisig", + "message": "1-of-1 signatures required (introspection fallback)" + }, + { + "type": "older", + "message": "Exit timelock of exit blocks" + } + ], + "asm": [ + "", + "", + "OP_CHECKSIG", + "", + "OP_CHECKSEQUENCEVERIFY", + "OP_DROP" + ] + } + ], + "source": "\nimport \"provider_share.ark\";\nimport \"single_sig.ark\";\n\noptions {\n server = server;\n exit = exit;\n}\n\ncontract StabilityPool(\n bytes32 priceTicker,\n bytes32 priceClock,\n bytes32 yieldTicker,\n bytes32 yieldClock,\n int aggregateSeekerUSD,\n int poolYieldIndex,\n int exit\n) {\n\n function providerDeposit(int depositSats, pubkey providerPk) {\n require(depositSats >= 330, \"deposit below dust\");\n\n int currentPrice = tx.inputs[1].assets.lookup(priceTicker);\n require(currentPrice > 0, \"invalid price beacon\");\n int priceHeight = tx.inputs[1].assets.lookup(priceClock);\n int priceAge = tx.time - priceHeight;\n require(priceAge <= 144, \"stale price oracle\");\n\n int currentIndex = tx.inputs[2].assets.lookup(yieldTicker);\n int yieldHeight = tx.inputs[2].assets.lookup(yieldClock);\n int yieldAge = tx.time - yieldHeight;\n require(yieldAge <= 144, \"stale funding oracle\");\n require(currentIndex >= poolYieldIndex, \"yield index regressed\");\n\n require(\n tx.outputs[0].scriptPubKey == new StabilityPool(\n priceTicker, priceClock, yieldTicker, yieldClock,\n aggregateSeekerUSD, currentIndex, exit\n ),\n \"invalid pool output\"\n );\n int poolValueAfter = tx.inputs[0].value + depositSats;\n require(\n tx.outputs[0].value >= poolValueAfter,\n \"pool deposit not credited\"\n );\n\n require(\n tx.outputs[1].assets.lookup(priceTicker) >= currentPrice,\n \"price beacon must survive\"\n );\n require(\n tx.outputs[1].assets.lookup(priceClock) >= priceHeight,\n \"price clock must survive\"\n );\n\n require(\n tx.outputs[2].assets.lookup(yieldTicker) >= currentIndex,\n \"funding beacon must survive\"\n );\n require(\n tx.outputs[2].assets.lookup(yieldClock) >= yieldHeight,\n \"funding clock must survive\"\n );\n\n require(\n tx.outputs[3].scriptPubKey == new ProviderShare(\n providerPk, depositSats, currentIndex, exit\n ),\n \"invalid provider share output\"\n );\n }\n\n function providerWithdraw(int withdrawSats, pubkey providerPk) {\n require(withdrawSats >= 330, \"withdraw below dust\");\n\n int currentPrice = tx.inputs[1].assets.lookup(priceTicker);\n require(currentPrice > 0, \"invalid price beacon\");\n int priceHeight = tx.inputs[1].assets.lookup(priceClock);\n int priceAge = tx.time - priceHeight;\n require(priceAge <= 144, \"stale price oracle\");\n\n int currentIndex = tx.inputs[2].assets.lookup(yieldTicker);\n int yieldHeight = tx.inputs[2].assets.lookup(yieldClock);\n int yieldAge = tx.time - yieldHeight;\n require(yieldAge <= 144, \"stale funding oracle\");\n require(currentIndex >= poolYieldIndex, \"yield index regressed\");\n\n int seekerCapitalNominal = aggregateSeekerUSD * 100000000 / currentPrice;\n int deltaIndex = currentIndex - poolYieldIndex;\n int fundingAccrued = aggregateSeekerUSD * deltaIndex / 100000000;\n int seekerCapital = seekerCapitalNominal + fundingAccrued;\n\n int totalAfter = tx.inputs[0].value - withdrawSats;\n require(totalAfter > seekerCapital, \"withdraw would undercollateralise\");\n\n int leverageBound = seekerCapital * 3;\n require(leverageBound <= totalAfter, \"leverage gate: provider withdraw blocked\");\n\n\n require(\n tx.outputs[0].scriptPubKey == new StabilityPool(\n priceTicker, priceClock, yieldTicker, yieldClock,\n aggregateSeekerUSD, currentIndex, exit\n ),\n \"invalid pool output\"\n );\n require(tx.outputs[0].value >= totalAfter, \"pool value mismatch\");\n\n require(\n tx.outputs[1].assets.lookup(priceTicker) >= currentPrice,\n \"price beacon must survive\"\n );\n require(\n tx.outputs[1].assets.lookup(priceClock) >= priceHeight,\n \"price clock must survive\"\n );\n\n require(\n tx.outputs[2].assets.lookup(yieldTicker) >= currentIndex,\n \"funding beacon must survive\"\n );\n require(\n tx.outputs[2].assets.lookup(yieldClock) >= yieldHeight,\n \"funding clock must survive\"\n );\n\n require(\n tx.outputs[3].scriptPubKey == new SingleSig(providerPk),\n \"output 3 not provider\"\n );\n require(tx.outputs[3].value >= withdrawSats, \"provider underpaid\");\n }\n}", + "compiler": { + "name": "arkade-compiler", + "version": "0.1.0" + }, + "updatedAt": "2026-05-15T17:21:56.331826983+00:00", + "warnings": [ + "warning[type]: fn providerDeposit: comparison '>' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerDeposit: comparison '<=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerDeposit: comparison '<=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerDeposit: comparison '>=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerWithdraw: comparison '>' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerWithdraw: comparison '<=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerWithdraw: comparison '<=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerWithdraw: comparison '>=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control", + "warning[type]: fn providerWithdraw: comparison '>=' mixes uint64le ('uint64le') with scriptnum ('int') — implicit conversion applied; use le64ToScriptNum() for explicit control" + ] +} \ No newline at end of file