From 1b8ceb2349564c8b5248b4018d548ba611beaa4c Mon Sep 17 00:00:00 2001 From: Zintarh <35270183+zintarh@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:47:29 +0100 Subject: [PATCH 1/3] docs: comprehensive protocol documentation overhaul - closes #336 --- docs/prediction-lifecycle.md | 292 +++++++++++++++++++++++++++++++++++ docs/quickstart.md | 199 ++++++++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 docs/prediction-lifecycle.md create mode 100644 docs/quickstart.md diff --git a/docs/prediction-lifecycle.md b/docs/prediction-lifecycle.md new file mode 100644 index 00000000..9085e305 --- /dev/null +++ b/docs/prediction-lifecycle.md @@ -0,0 +1,292 @@ +# Prediction Lifecycle + +Understanding how predictions flow through the PrediFi protocol—from market creation to reward distribution. + +## Overview + +Every prediction market on PrediFi follows a structured lifecycle with four distinct phases: + +1. **Creation** - A new market is created with defined parameters +2. **Trading** - Users place predictions and add liquidity +3. **Resolution** - The market outcome is determined via oracle +4. **Settlement** - Winners claim their rewards + +```mermaid +graph TD + A[Market Creator] -->|create_pool| B[Pool Created] + B -->|place_prediction| C[Users Add Predictions] + C -->|place_prediction| C + C -->|end_time reached| D[Pool Closed] + D -->|resolve_pool| E[Oracle Resolves Outcome] + E -->|claim_winnings| F[Winners Claim Rewards] + F -->|End| G[Lifecycle Complete] + + style B fill:#e1f5ff + style C fill:#fff4e1 + style E fill:#e8f5e9 + style F fill:#f3e5f5 +``` + +## Phase 1: Market Creation + +A market creator calls `create_pool()` to establish a new prediction market. + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `end_time` | `u64` | Unix timestamp after which no predictions are accepted | +| `token` | `Address` | Stellar token contract address for staking | +| `description` | `String` | Human-readable event description (max 256 bytes) | +| `metadata_url` | `String` | URL to extended metadata, e.g., IPFS link (max 512 bytes) | + +### Example + +```rust +let pool_id = contract.create_pool( + env, + 1735689600, // Unix timestamp + token_address, + String::from_str(&env, "Will Bitcoin reach $100k by 2025?"), + String::from_str(&env, "ipfs://QmXxx...") +); +``` + +### What Happens + +- A new `Pool` struct is created with `resolved: false` +- Pool ID is auto-incremented and returned +- `PoolCreatedEvent` is emitted for off-chain indexers +- Pool is stored in persistent storage with TTL extension + +:::info +**Pool IDs**: Pool IDs are sequential integers starting from 0. Each new pool increments the counter. +::: + +## Phase 2: Trading & Liquidity + +Users place predictions by calling `place_prediction()` before the pool's `end_time`. + +### Prediction Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as Contract + participant T as Token Contract + + U->>C: place_prediction(pool_id, amount, outcome) + C->>C: Validate pool state + C->>C: Check end_time not passed + C->>T: transfer(user, contract, amount) + C->>C: Update total_stake + C->>C: Update outcome_stake[outcome] + C->>C: Store user prediction + C->>C: Emit PredictionPlacedEvent + C-->>U: Success +``` + +### Key Validations + +- Pool must not be resolved (`pool.resolved == false`) +- Current time must be before `pool.end_time` +- Amount must be positive (`amount > 0`) +- User must have sufficient token balance + +### Staking Mechanism + +When a user places a prediction: + +1. **Token Transfer**: Tokens are transferred from user to contract +2. **Stake Tracking**: Total stake and outcome-specific stake are updated +3. **Prediction Storage**: User's prediction is stored with amount and outcome +4. **Indexing**: Prediction is added to user's prediction list for quick lookup + +### Example + +```rust +contract.place_prediction( + env, + user_address, + pool_id, + 1000000000, // 100 tokens (in smallest unit) + 1 // Outcome: "Yes" +); +``` + +:::tip +**Multiple Predictions**: Users can place multiple predictions on the same pool, including different outcomes. Each prediction is tracked separately. +::: + +## Phase 3: Resolution + +After `end_time` passes, an operator (with role 1) calls `resolve_pool()` to set the winning outcome. + +### Resolution Process + +```mermaid +graph LR + A[Pool End Time Reached] --> B[Operator Calls resolve_pool] + B --> C{Validate Operator Role} + C -->|Authorized| D[Set pool.resolved = true] + C -->|Unauthorized| E[Error: Unauthorized] + D --> F[Set pool.outcome = winning_outcome] + F --> G[Emit PoolResolvedEvent] + G --> H[Pool Ready for Claims] + + style D fill:#e8f5e9 + style F fill:#e8f5e9 + style E fill:#ffebee +``` + +### Oracle Integration + +PrediFi uses **Stork Network** for verifiable, objective market resolution: + +1. **Oracle Query**: Off-chain service queries Stork Network for outcome data +2. **Verification**: Outcome is verified against on-chain criteria +3. **Resolution**: Operator calls `resolve_pool()` with verified outcome +4. **Immutability**: Once resolved, outcome cannot be changed + +See [Verifiable Oracles](./oracles.md) for detailed oracle mechanics. + +### Example + +```rust +// Operator resolves pool with outcome 1 ("Yes") +contract.resolve_pool( + env, + operator_address, + pool_id, + 1 // Winning outcome +)?; +``` + +:::warning +**Irreversible**: Once a pool is resolved, the outcome cannot be changed. Ensure oracle data is accurate before resolving. +::: + +## Phase 4: Settlement & Claims + +Winners call `claim_winnings()` to receive their proportional share of the total pool. + +### Reward Calculation + +Rewards are calculated using a **proportional distribution** model: + +``` +winnings = (user_stake / winning_outcome_total_stake) × total_pool_stake +``` + +### Claim Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as Contract + participant T as Token Contract + + U->>C: claim_winnings(pool_id) + C->>C: Check pool.resolved == true + C->>C: Check not already claimed + C->>C: Get user prediction + C->>C: Check prediction.outcome == pool.outcome + C->>C: Calculate winnings proportionally + C->>T: transfer(contract, user, winnings) + C->>C: Mark as claimed + C->C: Emit WinningsClaimedEvent + C-->>U: Return winnings amount +``` + +### Example + +```rust +let winnings = contract.claim_winnings( + env, + user_address, + pool_id +)?; + +// Returns 0 if user didn't win or already claimed +if winnings > 0 { + println!("Claimed {} tokens", winnings); +} +``` + +### Edge Cases + +- **Losers**: Users who predicted the wrong outcome receive 0 tokens +- **No Prediction**: Users who never placed a prediction receive 0 tokens +- **Already Claimed**: Subsequent calls return `AlreadyClaimed` error +- **Unresolved Pool**: Returns `PoolNotResolved` error + +:::info +**Fee Structure**: Protocol fees are deducted from the total pool before distribution. Winners receive their proportional share of the net pool (after fees). +::: + +## Incentive Alignment + +The protocol aligns incentives through transparent, on-chain mechanics: + +```mermaid +graph TB + A[Market Participants] --> B{Incentive Type} + B -->|Liquidity Providers| C[Earn from Trading Fees] + B -->|Correct Predictors| D[Win Proportional Rewards] + B -->|Operators| E[Maintain Oracle Integrity] + + C --> F[Protocol Sustainability] + D --> F + E --> F + + style F fill:#e8f5e9 +``` + +### Key Principles + +1. **Transparency**: All pool data, predictions, and resolutions are on-chain +2. **Proportional Rewards**: Winners share pool proportionally to their stake +3. **No Central Authority**: Resolution relies on verifiable oracles, not trusted parties +4. **Immutability**: Once resolved, outcomes cannot be manipulated + +## State Transitions + +```mermaid +stateDiagram-v2 + [*] --> Created: create_pool() + Created --> Active: Users place predictions + Active --> Active: More predictions + Active --> Closed: end_time reached + Closed --> Resolved: resolve_pool() + Resolved --> Claimed: claim_winnings() + Claimed --> [*] + + note right of Created + resolved: false + total_stake: 0 + end note + + note right of Resolved + resolved: true + outcome: set + end note +``` + +## Events Timeline + +Every phase emits events for off-chain indexing and monitoring: + +| Event | Phase | Emitted When | +|-------|-------|--------------| +| `PoolCreatedEvent` | Creation | Pool is created | +| `PredictionPlacedEvent` | Trading | User places prediction | +| `PoolResolvedEvent` | Resolution | Operator resolves pool | +| `WinningsClaimedEvent` | Settlement | Winner claims rewards | + +See [Contract Reference](./contract-reference.md) for complete event schemas. + +## Next Steps + +- Learn about [Oracle Resolution](./oracles.md) +- Explore [Contract Methods](./contract-reference.md) +- Review [Error Handling](./troubleshooting.md) diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..469541e1 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,199 @@ +# Quickstart: Make Your First Prediction in 5 Minutes + +Get started with PrediFi by placing your first prediction. This guide walks you through connecting your wallet, finding a market, and placing a bet. + +## Prerequisites + +- A Stellar wallet (e.g., [Freighter](https://freighter.app/)) +- XLM or supported token for gas fees +- Basic familiarity with Stellar/Soroban + +:::tip +**Testnet First**: Start on Stellar testnet to experiment without real funds. Get testnet XLM from the [Stellar Laboratory](https://laboratory.stellar.org/#account-creator?network=test). +::: + +## Step 1: Install the Soroban SDK + +```bash +npm install @stellar/stellar-sdk +``` + +Or with TypeScript: + +```bash +npm install @stellar/stellar-sdk @types/node +``` + +## Step 2: Connect Your Wallet + +```typescript +import { Contract, Networks, nativeToScVal } from '@stellar/stellar-sdk'; + +// Connect to Stellar network +const network = Networks.TESTNET; // or Networks.PUBLIC for mainnet +const server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); + +// Contract address (replace with actual deployed contract) +const contractId = 'YOUR_CONTRACT_ID_HERE'; + +// Initialize contract client +const contract = new Contract(contractId); +``` + +:::info +**Contract Addresses**: Contract addresses differ between testnet and mainnet. Check the [deployment guide](./deployment.md) for current addresses. +::: + +## Step 3: Find an Active Pool + +```typescript +// Get pool details +async function getPool(poolId: number) { + const result = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) + }); + + return { + endTime: result.end_time, + resolved: result.resolved, + outcome: result.outcome, + totalStake: result.total_stake, + description: result.description + }; +} + +// Example: Get pool #1 +const pool = await getPool(1); +console.log('Pool:', pool.description); +``` + +## Step 4: Place Your Prediction + +```typescript +import { Keypair, TransactionBuilder, Operation } from '@stellar/stellar-sdk'; + +async function placePrediction( + poolId: number, + amount: number, // in smallest unit (e.g., stroops for XLM) + outcome: number, // 0 for "No", 1 for "Yes", etc. + sourceKeypair: Keypair +) { + // Build transaction + const account = await server.loadAccount(sourceKeypair.publicKey()); + + const transaction = new TransactionBuilder(account, { + fee: '100', // Base fee + networkPassphrase: network + }) + .addOperation( + contract.call('place_prediction', { + user: sourceKeypair.publicKey(), + pool_id: nativeToScVal(poolId, { type: 'u64' }), + amount: nativeToScVal(amount, { type: 'i128' }), + outcome: nativeToScVal(outcome, { type: 'u32' }) + }) + ) + .setTimeout(30) + .build(); + + // Sign and submit + transaction.sign(sourceKeypair); + const result = await server.submitTransaction(transaction); + + return result; +} + +// Example: Predict "Yes" (outcome 1) with 100 tokens +const keypair = Keypair.fromSecret('YOUR_SECRET_KEY'); +await placePrediction(1, 1000000000, 1, keypair); +``` + +:::warning +**Amount Format**: Amounts are in the smallest unit of the token. For XLM, use stroops (1 XLM = 10,000,000 stroops). For other tokens, check the token's decimal precision. +::: + +## Step 5: Check Your Prediction Status + +```typescript +async function getUserPredictions(userAddress: string, offset = 0, limit = 10) { + const result = await contract.call('get_user_predictions', { + user: userAddress, + offset: nativeToScVal(offset, { type: 'u32' }), + limit: nativeToScVal(limit, { type: 'u32' }) + }); + + return result.map((pred: any) => ({ + poolId: pred.pool_id, + amount: pred.amount, + outcome: pred.user_outcome, + poolResolved: pred.pool_resolved, + poolOutcome: pred.pool_outcome + })); +} + +const predictions = await getUserPredictions(keypair.publicKey()); +console.log('Your predictions:', predictions); +``` + +## Complete Example + +Here's a complete working example: + +```typescript +import { + Contract, + Networks, + Keypair, + Server, + TransactionBuilder, + nativeToScVal +} from '@stellar/stellar-sdk'; + +const network = Networks.TESTNET; +const server = new Server('https://horizon-testnet.stellar.org'); +const contractId = 'YOUR_CONTRACT_ID'; +const contract = new Contract(contractId); + +// Load your wallet +const keypair = Keypair.fromSecret('YOUR_SECRET_KEY'); +const account = await server.loadAccount(keypair.publicKey()); + +// Get pool info +const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(1, { type: 'u64' }) +}); + +console.log(`Pool: ${pool.description}`); +console.log(`Ends at: ${new Date(pool.end_time * 1000)}`); + +// Place prediction +const tx = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: network +}) + .addOperation( + contract.call('place_prediction', { + user: keypair.publicKey(), + pool_id: nativeToScVal(1, { type: 'u64' }), + amount: nativeToScVal(1000000000, { type: 'i128' }), // 100 tokens + outcome: nativeToScVal(1, { type: 'u32' }) // "Yes" + }) + ) + .setTimeout(30) + .build(); + +tx.sign(keypair); +const result = await server.submitTransaction(tx); +console.log('Transaction hash:', result.hash); +``` + +## Next Steps + +- Learn about the [Prediction Lifecycle](./prediction-lifecycle.md) +- Explore [Contract Reference](./contract-reference.md) +- Understand [Oracle Resolution](./oracles.md) +- Check [Troubleshooting](./troubleshooting.md) for common issues + +:::tip +**Need Help?** Join our [Telegram community](https://t.me/predifi_onchain_build/1) for support and updates. +::: From 95fe67a6e4176b6fe8702d04fe99b20f7bc71ba6 Mon Sep 17 00:00:00 2001 From: Zintarh <35270183+zintarh@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:47:56 +0100 Subject: [PATCH 2/3] docs: add oracle, contract reference, and troubleshooting documentation --- docs/contract-reference.md | 478 +++++++++++++++++++++++++++++++++++++ docs/oracles.md | 243 +++++++++++++++++++ 2 files changed, 721 insertions(+) create mode 100644 docs/contract-reference.md create mode 100644 docs/oracles.md diff --git a/docs/contract-reference.md b/docs/contract-reference.md new file mode 100644 index 00000000..ef960ddf --- /dev/null +++ b/docs/contract-reference.md @@ -0,0 +1,478 @@ +# Contract Reference + +Complete reference for all PrediFi contract methods, events, and data structures. + +## Core Functions + +### `init` + +Initialize the contract with configuration parameters. + +```rust +pub fn init( + env: Env, + access_control: Address, + treasury: Address, + fee_bps: u32 +) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `access_control` | `Address` | Access control contract address | +| `treasury` | `Address` | Treasury address for fee collection | +| `fee_bps` | `u32` | Fee in basis points (max 10000 = 100%) | + +**Returns:** None + +**Events:** `InitEvent` + +**Notes:** +- Idempotent - safe to call multiple times +- Only sets config if not already initialized + +--- + +### `create_pool` + +Create a new prediction market pool. + +```rust +pub fn create_pool( + env: Env, + end_time: u64, + token: Address, + description: String, + metadata_url: String +) -> u64 +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `end_time` | `u64` | Unix timestamp after which predictions close | +| `token` | `Address` | Stellar token contract for staking | +| `description` | `String` | Event description (max 256 bytes) | +| `metadata_url` | `String` | Extended metadata URL (max 512 bytes) | + +**Returns:** `u64` - New pool ID + +**Events:** `PoolCreatedEvent` + +**Validations:** +- `end_time` must be in the future +- `description` length ≤ 256 bytes +- `metadata_url` length ≤ 512 bytes + +**Example:** + +```rust +let pool_id = contract.create_pool( + env, + 1735689600, // Dec 31, 2024 + token_address, + String::from_str(&env, "Will BTC hit $100k?"), + String::from_str(&env, "ipfs://QmXxx...") +); +``` + +--- + +### `place_prediction` + +Place a prediction on an active pool. + +```rust +pub fn place_prediction( + env: Env, + user: Address, + pool_id: u64, + amount: i128, + outcome: u32 +) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User placing the prediction | +| `pool_id` | `u64` | Pool to predict on | +| `amount` | `i128` | Prediction amount (in token's smallest unit) | +| `outcome` | `u32` | Outcome index (0, 1, 2, etc.) | + +**Returns:** None + +**Events:** `PredictionPlacedEvent` + +**Validations:** +- Pool must exist +- Pool must not be resolved +- Current time < pool.end_time +- Amount > 0 +- User must have sufficient token balance + +**Token Transfer:** +- Transfers `amount` tokens from user to contract + +**Example:** + +```rust +contract.place_prediction( + env, + user_address, + pool_id, + 1000000000, // 100 tokens + 1 // Outcome: "Yes" +); +``` + +--- + +### `resolve_pool` + +Resolve a pool with the winning outcome. Requires Operator role (1). + +```rust +pub fn resolve_pool( + env: Env, + operator: Address, + pool_id: u64, + outcome: u32 +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `operator` | `Address` | Operator address (must have role 1) | +| `pool_id` | `u64` | Pool to resolve | +| `outcome` | `u32` | Winning outcome index | + +**Returns:** `Result<(), PredifiError>` + +**Events:** `PoolResolvedEvent` + +**Validations:** +- Operator must have role 1 +- Pool must exist +- Pool must not already be resolved + +**Errors:** +- `Unauthorized` - Operator lacks required role +- `PoolNotFound` - Pool doesn't exist +- `PoolAlreadyResolved` - Pool already resolved + +**Example:** + +```rust +contract.resolve_pool( + env, + operator_address, + pool_id, + 1 // Winning outcome +)?; +``` + +--- + +### `claim_winnings` + +Claim winnings from a resolved pool. + +```rust +pub fn claim_winnings( + env: Env, + user: Address, + pool_id: u64 +) -> Result +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User claiming winnings | +| `pool_id` | `u64` | Pool to claim from | + +**Returns:** `Result` - Amount claimed (0 if didn't win) + +**Events:** `WinningsClaimedEvent` + +**Validations:** +- Pool must be resolved +- User must not have already claimed +- User must have placed a prediction + +**Reward Calculation:** + +``` +winnings = (user_stake / winning_outcome_total_stake) × total_pool_stake +``` + +**Errors:** +- `PoolNotResolved` - Pool not yet resolved +- `AlreadyClaimed` - User already claimed winnings + +**Example:** + +```rust +let winnings = contract.claim_winnings( + env, + user_address, + pool_id +)?; + +if winnings > 0 { + // User won and received winnings +} +``` + +--- + +### `get_user_predictions` + +Get a paginated list of a user's predictions. + +```rust +pub fn get_user_predictions( + env: Env, + user: Address, + offset: u32, + limit: u32 +) -> Vec +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User address | +| `offset` | `u32` | Pagination offset | +| `limit` | `u32` | Maximum results to return | + +**Returns:** `Vec` + +**Example:** + +```rust +let predictions = contract.get_user_predictions( + env, + user_address, + 0, // offset + 10 // limit +); +``` + +--- + +## Admin Functions + +### `pause` + +Pause all contract operations. Requires Admin role (0). + +```rust +pub fn pause(env: Env, admin: Address) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address (must have role 0) | + +**Events:** `PauseEvent` + +--- + +### `unpause` + +Resume contract operations. Requires Admin role (0). + +```rust +pub fn unpause(env: Env, admin: Address) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address (must have role 0) | + +**Events:** `UnpauseEvent` + +--- + +### `set_fee_bps` + +Update protocol fee. Requires Admin role (0). + +```rust +pub fn set_fee_bps( + env: Env, + admin: Address, + fee_bps: u32 +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address | +| `fee_bps` | `u32` | New fee in basis points (max 10000) | + +**Events:** `FeeUpdateEvent` + +--- + +### `set_treasury` + +Update treasury address. Requires Admin role (0). + +```rust +pub fn set_treasury( + env: Env, + admin: Address, + treasury: Address +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address | +| `treasury` | `Address` | New treasury address | + +**Events:** `TreasuryUpdateEvent` + +--- + +## Data Structures + +### `Pool` + +```rust +pub struct Pool { + pub end_time: u64, + pub resolved: bool, + pub outcome: u32, + pub token: Address, + pub total_stake: i128, + pub description: String, + pub metadata_url: String, +} +``` + +### `Prediction` + +```rust +pub struct Prediction { + pub amount: i128, + pub outcome: u32, +} +``` + +### `UserPredictionDetail` + +```rust +pub struct UserPredictionDetail { + pub pool_id: u64, + pub amount: i128, + pub user_outcome: u32, + pub pool_end_time: u64, + pub pool_resolved: bool, + pub pool_outcome: u32, +} +``` + +### `Config` + +```rust +pub struct Config { + pub fee_bps: u32, + pub treasury: Address, + pub access_control: Address, +} +``` + +--- + +## Events + +### `PoolCreatedEvent` + +Emitted when a new pool is created. + +```rust +pub struct PoolCreatedEvent { + pub pool_id: u64, + pub end_time: u64, + pub token: Address, + pub metadata_url: String, +} +``` + +### `PredictionPlacedEvent` + +Emitted when a user places a prediction. + +```rust +pub struct PredictionPlacedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, + pub outcome: u32, +} +``` + +### `PoolResolvedEvent` + +Emitted when a pool is resolved. + +```rust +pub struct PoolResolvedEvent { + pub pool_id: u64, + pub operator: Address, + pub outcome: u32, +} +``` + +### `WinningsClaimedEvent` + +Emitted when a user claims winnings. + +```rust +pub struct WinningsClaimedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, +} +``` + +--- + +## Error Codes + +See [Troubleshooting](./troubleshooting.md) for complete error reference. + +| Code | Error | Description | +|------|-------|-------------| +| 10 | `Unauthorized` | Caller lacks required role | +| 22 | `PoolNotResolved` | Pool not yet resolved | +| 60 | `AlreadyClaimed` | User already claimed winnings | + +--- + +## Next Steps + +- Start with [Quickstart](./quickstart.md) +- Understand [Prediction Lifecycle](./prediction-lifecycle.md) +- Learn about [Oracles](./oracles.md) +- Review [Troubleshooting](./troubleshooting.md) diff --git a/docs/oracles.md b/docs/oracles.md new file mode 100644 index 00000000..9c4f32c8 --- /dev/null +++ b/docs/oracles.md @@ -0,0 +1,243 @@ +# Verifiable Oracles + +PrediFi uses **Stork Network** to ensure objective, verifiable market outcomes. This document explains how oracle resolution works and how to verify outcomes. + +## Overview + +PrediFi markets resolve based on verifiable, on-chain data from Stork Network oracles. This eliminates the need for trusted third parties and ensures outcomes are objective and auditable. + +## Oracle Resolution Flow + +```mermaid +sequenceDiagram + participant P as Pool + participant O as Operator + participant S as Stork Network + participant C as Contract + + P->>P: end_time reached + P->>O: Pool ready for resolution + O->>S: Query oracle data + S-->>O: Return verified outcome + O->>O: Verify outcome against criteria + O->>C: resolve_pool(pool_id, outcome) + C->>C: Validate operator role + C->>C: Set pool.resolved = true + C->>C: Set pool.outcome = outcome + C->>C: Emit PoolResolvedEvent + C-->>O: Success +``` + +## Stork Network Integration + +Stork Network provides decentralized oracle services for Stellar/Soroban: + +1. **Data Sources**: Aggregates data from multiple trusted sources +2. **Verification**: Cryptographic proofs ensure data integrity +3. **On-Chain**: Outcomes are verifiable on-chain via contract calls + +### Querying Oracle Data + +Operators query Stork Network before resolving pools: + +```typescript +// Example: Query Stork Network for outcome +async function queryOracle(poolId: number, metadataUrl: string) { + // Parse metadata URL to get oracle query parameters + const oracleParams = parseMetadata(metadataUrl); + + // Query Stork Network + const response = await fetch(`https://stork.network/api/query`, { + method: 'POST', + body: JSON.stringify({ + pool_id: poolId, + query: oracleParams.query, + timestamp: oracleParams.end_time + }) + }); + + const data = await response.json(); + return data.outcome; // Returns outcome index (0, 1, 2, etc.) +} +``` + +## Resolution Process + +### Step 1: Pool End Time Reached + +Once `pool.end_time` passes, the pool is closed to new predictions: + +```rust +// Pool is closed when: +env.ledger().timestamp() >= pool.end_time +``` + +### Step 2: Operator Queries Oracle + +An operator (with role 1) queries Stork Network for the verified outcome: + +```typescript +const outcome = await queryOracle(poolId, pool.metadata_url); +``` + +### Step 3: Operator Resolves Pool + +The operator calls `resolve_pool()` with the verified outcome: + +```rust +contract.resolve_pool( + env, + operator_address, + pool_id, + outcome // Verified outcome from oracle +)?; +``` + +### Step 4: Contract Validation + +The contract validates: + +- Operator has role 1 (Operator role) +- Pool exists and is not already resolved +- Pool's `end_time` has passed + +### Step 5: Outcome Set + +Once validated, the contract: + +1. Sets `pool.resolved = true` +2. Sets `pool.outcome = outcome` +3. Emits `PoolResolvedEvent` +4. Makes pool eligible for claims + +## Verifying Outcomes + +### On-Chain Verification + +All outcomes are stored on-chain and can be verified: + +```typescript +async function verifyOutcome(poolId: number) { + const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) + }); + + if (!pool.resolved) { + throw new Error('Pool not yet resolved'); + } + + return { + resolved: pool.resolved, + outcome: pool.outcome, + resolvedAt: pool.resolved_at // If available + }; +} +``` + +### Off-Chain Verification + +You can verify outcomes against Stork Network data: + +```typescript +async function verifyAgainstOracle(poolId: number, contractOutcome: number) { + const oracleOutcome = await queryOracle(poolId); + + if (oracleOutcome !== contractOutcome) { + throw new Error('Outcome mismatch between contract and oracle'); + } + + return true; // Verified +} +``` + +## Oracle Data Sources + +Stork Network aggregates data from multiple sources: + +| Source Type | Examples | Use Case | +|-------------|----------|----------| +| **APIs** | Sports APIs, Financial APIs | Real-time event data | +| **Blockchain** | Other chains, Cross-chain data | Multi-chain events | +| **Feeds** | Price feeds, News feeds | Market data | + +## Security Considerations + +### Operator Trust + +Operators are required to have role 1, but they cannot: + +- Change outcomes after resolution +- Resolve pools before `end_time` +- Manipulate oracle data (Stork Network prevents this) + +### Oracle Reliability + +Stork Network provides: + +- **Redundancy**: Multiple data sources +- **Verification**: Cryptographic proofs +- **Transparency**: All queries are logged + +### Dispute Resolution + +If an outcome seems incorrect: + +1. Check the `PoolResolvedEvent` for the operator +2. Verify against Stork Network data +3. Review the pool's `metadata_url` for resolution criteria +4. Contact the protocol team if discrepancies are found + +## Best Practices + +### For Operators + +- Always verify oracle data before resolving +- Wait for sufficient confirmation from Stork Network +- Double-check outcome indices match pool structure +- Monitor for oracle updates before resolution deadline + +### For Users + +- Review pool `metadata_url` to understand resolution criteria +- Verify outcomes on-chain after resolution +- Check `PoolResolvedEvent` for resolution details +- Report any discrepancies immediately + +## Example: Resolving a Sports Market + +```typescript +// Pool: "Will Team A win the match?" +// Metadata URL contains match ID and resolution criteria + +async function resolveSportsMarket(poolId: number) { + // 1. Get pool details + const pool = await getPool(poolId); + + // 2. Wait for end_time + await waitUntil(pool.end_time); + + // 3. Query Stork Network for match result + const matchResult = await queryStorkNetwork({ + type: 'sports', + match_id: pool.metadata.match_id, + source: 'sports_api' + }); + + // 4. Map result to outcome index + // Outcome 0: "No", Outcome 1: "Yes" + const outcome = matchResult.team_a_won ? 1 : 0; + + // 5. Resolve pool + await contract.resolve_pool( + operatorAddress, + poolId, + outcome + ); +} +``` + +## Next Steps + +- Learn about [Pool Resolution](./prediction-lifecycle.md#phase-3-resolution) +- Explore [Contract Methods](./contract-reference.md#admin-functions) +- Review [Error Handling](./troubleshooting.md#oracle-errors) From ee14f6dac7b2dc2004ede219cdffdfedaa110929 Mon Sep 17 00:00:00 2001 From: Zintarh <35270183+zintarh@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:48:14 +0100 Subject: [PATCH 3/3] docs: add comprehensive troubleshooting guide with error codes --- docs/troubleshooting.md | 508 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 508 insertions(+) create mode 100644 docs/troubleshooting.md diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..b5057d0b --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,508 @@ +# Troubleshooting + +Common errors, solutions, and debugging tips for PrediFi integration. + +## Error Code Reference + +PrediFi uses comprehensive error codes for precise error handling. All errors implement the `PredifiError` enum. + +### Error Categories + +| Range | Category | Description | +|-------|----------|-------------| +| 1-5 | Initialization | Contract setup errors | +| 10-15 | Authorization | Access control errors | +| 20-30 | Pool State | Pool lifecycle errors | +| 40-50 | Prediction | Betting errors | +| 60-70 | Claiming | Reward claim errors | +| 80-85 | Timestamp | Time validation errors | +| 90-100 | Validation | Input validation errors | +| 110-118 | Arithmetic | Math operation errors | +| 120-129 | Storage | Data persistence errors | +| 150-159 | Token | Token transfer errors | +| 160-169 | Oracle | Oracle resolution errors | +| 180-189 | Admin | Admin operation errors | + +--- + +## Common Errors + +### Initialization Errors + +#### `NotInitialized` (Code: 1) + +**Message:** "Contract has not been initialized yet" + +**Cause:** Contract `init()` function hasn't been called. + +**Solution:** + +```rust +// Call init before using contract +contract.init( + env, + access_control_address, + treasury_address, + 100 // 1% fee (100 basis points) +); +``` + +--- + +### Authorization Errors + +#### `Unauthorized` (Code: 10) + +**Message:** "The caller is not authorized to perform this action" + +**Cause:** User lacks required role for the operation. + +**Solution:** + +- For `resolve_pool()`: Ensure caller has Operator role (1) +- For admin functions: Ensure caller has Admin role (0) +- Check access control contract for role assignments + +**Example:** + +```rust +// Check if user has operator role +let has_role = access_control.has_role(user, 1); +if !has_role { + return Err(PredifiError::Unauthorized); +} +``` + +--- + +### Pool State Errors + +#### `PoolNotFound` (Code: 20) + +**Message:** "The specified pool was not found" + +**Cause:** Pool ID doesn't exist. + +**Solution:** + +```typescript +// Verify pool exists before operations +const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) +}); + +if (!pool) { + throw new Error('Pool not found'); +} +``` + +#### `PoolAlreadyResolved` (Code: 21) + +**Message:** "The pool has already been resolved" + +**Cause:** Attempting to resolve or modify an already resolved pool. + +**Solution:** Check pool state before operations: + +```rust +let pool = get_pool(&env, pool_id)?; +if pool.resolved { + return Err(PredifiError::PoolAlreadyResolved); +} +``` + +#### `PoolNotResolved` (Code: 22) + +**Message:** "The pool has not been resolved yet" + +**Cause:** Attempting to claim winnings from unresolved pool. + +**Solution:** Wait for pool resolution: + +```typescript +// Check if pool is resolved +const pool = await getPool(poolId); +if (!pool.resolved) { + console.log('Pool not yet resolved. Waiting...'); + return; +} + +// Now safe to claim +await claimWinnings(poolId); +``` + +--- + +### Prediction Errors + +#### `InvalidPredictionAmount` (Code: 42) + +**Message:** "The prediction amount is invalid (e.g., zero or negative)" + +**Cause:** Amount is zero or negative. + +**Solution:** + +```rust +// Validate amount before calling +if amount <= 0 { + return Err(PredifiError::InvalidPredictionAmount); +} +``` + +#### `PredictionTooLate` (Code: 43) + +**Message:** "Cannot place prediction after pool end time" + +**Cause:** Pool's `end_time` has passed. + +**Solution:** + +```typescript +// Check pool end time +const pool = await getPool(poolId); +const now = Date.now() / 1000; // Unix timestamp + +if (now >= pool.end_time) { + throw new Error('Pool has closed for predictions'); +} +``` + +#### `InsufficientBalanceOrStakeLimit` (Code: 44) + +**Message:** "The user has insufficient balance or stake limit violation" + +**Cause:** User doesn't have enough tokens or exceeds stake limit. + +**Solution:** + +```typescript +// Check balance before prediction +const balance = await tokenContract.balance(userAddress); +if (balance < amount) { + throw new Error('Insufficient balance'); +} + +// Check stake limits if applicable +const totalStake = await getUserTotalStake(userAddress); +if (totalStake + amount > MAX_STAKE) { + throw new Error('Stake limit exceeded'); +} +``` + +--- + +### Claiming Errors + +#### `AlreadyClaimed` (Code: 60) + +**Message:** "The user has already claimed winnings for this pool" + +**Cause:** User already claimed winnings from this pool. + +**Solution:** + +```typescript +// Check if already claimed before calling +const hasClaimed = await checkIfClaimed(userAddress, poolId); +if (hasClaimed) { + console.log('Already claimed'); + return; +} + +// Safe to claim +await claimWinnings(poolId); +``` + +#### `NotAWinner` (Code: 61) + +**Message:** "The user did not win this pool" + +**Cause:** User's prediction outcome doesn't match pool outcome. + +**Note:** This doesn't throw an error - `claim_winnings()` returns 0 for losers. + +**Solution:** + +```rust +let winnings = contract.claim_winnings(env, user, pool_id)?; +if winnings == 0 { + // User didn't win or already claimed +} +``` + +--- + +### Timestamp Errors + +#### `InvalidTimestamp` (Code: 80) + +**Message:** "The provided timestamp is invalid or time constraints not met" + +**Cause:** Timestamp validation failed (e.g., end_time in the past). + +**Solution:** + +```rust +// Validate timestamp +let current_time = env.ledger().timestamp(); +if end_time <= current_time { + return Err(PredifiError::InvalidTimestamp); +} +``` + +--- + +### Validation Errors + +#### `InvalidData` (Code: 90) + +**Message:** "The provided data is invalid" + +**Cause:** General data validation failure. + +**Solution:** Check all input parameters match expected types and constraints. + +#### `InvalidAddressOrToken` (Code: 91) + +**Message:** "The provided address or token is invalid" + +**Cause:** Invalid Stellar address or token contract. + +**Solution:** + +```typescript +// Validate address format +function isValidAddress(address: string): boolean { + // Stellar addresses are 56 characters, start with G + return /^G[A-Z0-9]{55}$/.test(address); +} + +if (!isValidAddress(tokenAddress)) { + throw new Error('Invalid token address'); +} +``` + +--- + +### Arithmetic Errors + +#### `ArithmeticError` (Code: 110) + +**Message:** "An arithmetic overflow, underflow, or division by zero occurred" + +**Cause:** Math operation failed (overflow, underflow, or division by zero). + +**Solution:** Use checked arithmetic: + +```rust +// Use checked operations +let total = stake_a + .checked_add(stake_b) + .ok_or(PredifiError::ArithmeticError)?; + +let winnings = amount + .checked_mul(pool.total_stake) + .ok_or(PredifiError::ArithmeticError)? + .checked_div(winning_stake) + .ok_or(PredifiError::ArithmeticError)?; +``` + +--- + +### Token Errors + +#### `TokenError` (Code: 150) + +**Message:** "Token transfer, approval, or contract call failed" + +**Cause:** Token contract call failed (insufficient balance, approval, etc.). + +**Solution:** + +```typescript +// Check balance and approval before transfer +const balance = await token.balance(userAddress); +if (balance < amount) { + throw new Error('Insufficient balance'); +} + +// Ensure contract has approval (if needed) +await token.approve(userAddress, contractAddress, amount); +``` + +--- + +### Oracle Errors + +#### `OracleError` (Code: 160) + +**Message:** "Oracle error or stale data detected" + +**Cause:** Oracle data is unavailable or stale. + +**Solution:** + +```typescript +// Verify oracle data freshness +const oracleData = await queryOracle(poolId); +const dataAge = Date.now() - oracleData.timestamp; + +if (dataAge > MAX_DATA_AGE) { + throw new Error('Oracle data is stale'); +} +``` + +#### `ResolutionError` (Code: 161) + +**Message:** "Resolution error or unauthorized resolver" + +**Cause:** Resolution attempt failed or unauthorized. + +**Solution:** Ensure operator has role 1 and pool is ready for resolution. + +--- + +## RPC & Network Issues + +### Transaction Timeout + +**Symptom:** Transaction hangs or times out. + +**Solutions:** + +1. **Increase timeout:** +```typescript +const tx = new TransactionBuilder(account, { + timeout: 60 // Increase from default 30 +}) +``` + +2. **Check network status:** +```typescript +const server = new Server('https://horizon-testnet.stellar.org'); +const health = await server.health(); +console.log('Network status:', health); +``` + +3. **Retry with exponential backoff:** +```typescript +async function retryWithBackoff(fn, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (i === maxRetries - 1) throw error; + await sleep(2 ** i * 1000); // Exponential backoff + } + } +} +``` + +### Connection Errors + +**Symptom:** Cannot connect to Stellar network. + +**Solutions:** + +- Verify network endpoint is correct +- Check firewall/proxy settings +- Try alternative Horizon server +- Verify internet connection + +### Gas/Fee Estimation + +**Symptom:** Transaction fails with insufficient fee. + +**Solution:** + +```typescript +// Get recommended fee +const feeStats = await server.feeStats(); +const recommendedFee = feeStats.fee_charged.mode; + +const tx = new TransactionBuilder(account, { + fee: recommendedFee.toString() +}); +``` + +--- + +## Debugging Tips + +### 1. Enable Verbose Logging + +```typescript +// Enable detailed logging +const server = new Server('https://horizon-testnet.stellar.org', { + allowHttp: true +}); + +server.on('request', (req) => { + console.log('Request:', req); +}); + +server.on('response', (res) => { + console.log('Response:', res); +}); +``` + +### 2. Check Contract State + +```typescript +// Verify contract is initialized +const config = await contract.call('get_config'); +console.log('Config:', config); + +// Check if paused +const paused = await contract.call('is_paused'); +console.log('Paused:', paused); +``` + +### 3. Validate Pool State + +```typescript +// Get full pool state +const pool = await getPool(poolId); +console.log('Pool state:', { + id: poolId, + endTime: new Date(pool.end_time * 1000), + resolved: pool.resolved, + outcome: pool.outcome, + totalStake: pool.total_stake +}); +``` + +### 4. Monitor Events + +```typescript +// Listen for contract events +const events = await server.effects() + .forAccount(contractAddress) + .order('desc') + .limit(10) + .call(); + +events.records.forEach(event => { + console.log('Event:', event); +}); +``` + +--- + +## Getting Help + +If you encounter issues not covered here: + +1. **Check Error Codes:** Review the [Error Code Reference](#error-code-reference) above +2. **Review Documentation:** See [Contract Reference](./contract-reference.md) +3. **Community Support:** Join [Telegram](https://t.me/predifi_onchain_build/1) +4. **Open an Issue:** [GitHub Issues](https://github.com/Web3Novalabs/predifi/issues) + +--- + +## Next Steps + +- Review [Quickstart](./quickstart.md) for basic usage +- Explore [Contract Reference](./contract-reference.md) for API details +- Understand [Prediction Lifecycle](./prediction-lifecycle.md) for flow