# Descro — Protocol Flow Design _2026-05-19_ ## Vision Ein dezentrales Escrow-Protokoll für physische Waren auf Solana. `descro` ist ein universeller, agnostischer Escrow-Primitive. Darüber liegt `solisting` (Listing + Order Flow). Beide Programme sind vollständig unabhängig voneinander — keine gegenseitigen Abhängigkeiten, keine hardcoded Program-IDs mit Sonderrechten. --- ## Architektur ``` APPLICATION LAYER Zentrale Platform (custodial Wallets, normale UX) Dezentrale Clients (direkter Protokoll-Zugriff) │ PROTOCOL LAYER ┌─────────────────────┐ │ solisting │ ← Listings + Order/Consent Flow │ (descro_market) │ └──────────┬──────────┘ │ CPI (permissionless, kein Sonderrecht) ┌──────────▼──────────┐ ┌──────────────────────────┐ │ descro │───▶│ descro_ext_resolvers │ │ (Escrow + Vault) │CPI │ (Resolver Registry) │ └─────────────────────┘ └──────────────────────────┘ ``` **Kernprinzip:** `descro` kennt `solisting` nicht. Jeder kann `descro` direkt nutzen. `solisting` ist ein optionaler Komfort-Layer. --- ## Programm 1: `descro` (Escrow Program) ### State ```rust #[account] pub struct EscrowAccount { pub seller: Pubkey, pub buyer: Pubkey, pub amount: u64, pub dispute_resolver: Option, pub state: EscrowState, pub bump: u8, pub vault_bump: u8, pub escrow_id: u64, pub dispute_raised_at: Option, // gesetzt wenn dispute() aufgerufen wird } pub enum EscrowState { AwaitingDeposit, // Seller hat erstellt, Buyer noch nicht gezahlt Active, // SOL im Vault, Ware unterwegs Disputed, // Streit offen Complete, Cancelled, } ``` **PDAs:** ``` EscrowAccount: ["escrow", seller, escrow_id_le] Vault: ["vault", escrow_account_pubkey] ``` ### Instructions #### `create_escrow(amount, dispute_resolver, escrow_id)` — bestehend ``` Signer: Seller State: AwaitingDeposit Zweck: Direktnutzung ohne solisting — Seller initiiert manuell ``` #### `create_escrow_prefunded(seller, buyer, amount, dispute_resolver, escrow_id)` — neu ``` Signer: Seller (zahlt Rent, propagiert via CPI von solisting) Precond: Vault-PDA hat bereits ≥ amount Lamports State: Active (kein AwaitingDeposit) Zweck: Wird von solisting via CPI aufgerufen NACHDEM OrderVault → EscrowVault transferiert wurde Vollständig permissionless — descro prüft nur ob Vault funded ist ``` Keine Kopplung an solisting. Jeder kann `create_escrow_prefunded` aufrufen solange der Vault-PDA die Funds hat. #### `deposit()` — bestehend ``` Signer: Buyer State: AwaitingDeposit → Active ``` #### `complete()` — bestehend ``` Signer: Buyer State: Active → Complete Aktion: Vault → Seller ``` #### `dispute()` — modifiziert ``` Signer: Buyer oder Seller State: Active → Disputed Neu: escrow.dispute_raised_at = Clock::get()?.unix_timestamp ``` #### `resolve(winner)` — bestehend ``` Signer: dispute_resolver (Wallet ODER Program via CPI) State: Disputed → Complete Side: CPI → Registry.update_stats() ``` #### `cancel()` — bestehend ``` Signer: Seller State: AwaitingDeposit → Cancelled ``` #### `emergency_resolve(winner)` — neu ``` Signer: Buyer UND Seller (beide müssen signieren) State: Disputed → Complete Guards: dispute_raised_at + DISPUTE_TIMEOUT (14 Tage) < now Zweck: Wenn Resolver verschwindet nach Commitment — beide Parteien können sich einigen ohne Resolver. Requires echten bilateralen Consent. ``` --- ## Programm 2: `descro_ext_resolvers` (Resolver Registry) Weitgehend unverändert. Einzige Ergänzung: `acceptance_policy` Feld. ```rust #[account] pub struct ResolverEntry { pub authority: Pubkey, pub resolver_type: ResolverType, pub acceptance_policy: AcceptancePolicy, // neu pub name: String, pub description: String, pub fee_bps: u16, pub fee_recipient: Pubkey, pub metadata_uri: String, pub total_resolved: u64, pub ruled_for_buyer: u64, pub ruled_for_seller: u64, pub registered_at: i64, } pub enum AcceptancePolicy { Open, // Jeder kann diesen Resolver nutzen, keine Signatur bei Creation SignatureGated, // Resolver muss bei accept_order() co-signieren ProgramGated, // Resolver ist ein Program mit eigener validate()-Logik via CPI } ``` ### Resolver Signature Mechanics Solana-Transaktionen haben binäre Signaturen — es gibt keine konditionalen `Signer<>` Constraints. Die Lösung: Der Resolver-Account wird als `AccountInfo<'info>` (nicht `Signer<'info>`) deklariert, und die Policy bestimmt ob `is_signer` geprüft wird: ```rust pub struct AcceptOrder<'info> { /// CHECK: Signiert nur wenn Policy == SignatureGated pub resolver: AccountInfo<'info>, pub resolver_entry: Option>, } // Im Handler: let policy = ctx.accounts.resolver_entry .as_ref() .map(|e| e.acceptance_policy) .unwrap_or(AcceptancePolicy::SignatureGated); // kein Registry-Eintrag → muss signieren match policy { Open => {} SignatureGated => require!(resolver.is_signer, SolistingError::ResolverSignatureRequired), ProgramGated => { /* CPI zu resolver_program.accept_escrow() */ } } ``` **Policy-Bedeutung:** | Policy | Mechanismus | Typischer Use Case | |---|---|---| | `Open` | Kein Signature-Check | JuryDAO, MAD — permissionless Programs | | `SignatureGated` | `resolver.is_signer == true` | Zentrale Plattform, Treuhandservice, Privatperson | | `ProgramGated` | CPI zu `resolver.validate_escrow()` | Algorithmic, Multisig | | _(kein Eintrag)_ | Default: `SignatureGated` | Privatperson ohne Registry-Eintrag | **Das Darknet-Problem ist gelöst:** Ein `SignatureGated` Resolver (z.B. eBay-Äquivalent) co-signiert `accept_order()` nur für Kunden die sie kennen. Trägt jemand fremdes eBay als Resolver ein ohne ihr Consent → der `accept_order()` Call schlägt fehl weil eBay's Backend nicht signiert. Der Escrow wird gar nicht erst erstellt. --- ## Programm 3: `solisting` (Listing + Order Program) ### State ```rust #[account] pub struct ListingAccount { pub seller: Pubkey, pub price: u64, // Lamports — fix vom Seller gesetzt pub quantity: u32, pub resolver: Pubkey, // bevorzugter Resolver pub metadata_uri: String, // IPFS/Arweave → Titel, Bilder, Versandbedingungen, Beschreibung pub listing_id: u64, pub state: ListingState, pub bump: u8, } pub enum ListingState { Active, Closed } #[account] pub struct OrderAccount { pub listing: Pubkey, pub buyer: Pubkey, pub seller: Pubkey, pub resolver: Pubkey, pub amount: u64, pub escrow_id: u64, // vorher determiniert, für PDA-Berechnung pub state: OrderState, pub order_id: u64, pub created_at: i64, // für cancel_order Timeout pub bump: u8, pub vault_bump: u8, } pub enum OrderState { AwaitingSellerAccept, Accepted, Rejected } ``` **PDAs:** ``` Listing: ["listing", seller, listing_id_le] Order: ["order", listing, buyer, order_id_le] OrderVault: ["order_vault", order] ``` ### Instructions #### `create_listing(price, quantity, resolver, metadata_uri, listing_id)` ``` Signer: Seller Erstellt: ListingAccount ``` #### `update_listing(price, quantity, metadata_uri)` ``` Signer: Seller Guards: state == Active keine offenen Orders (quantity guard reicht als Proxy) ``` #### `close_listing()` ``` Signer: Seller Guards: Keine offenen OrderAccounts gegen dieses Listing ``` #### `create_order(listing_pda, order_id, escrow_id)` ``` Signer: Buyer Aktion: SOL (listing.price) → OrderVault State: AwaitingSellerAccept Guards: listing.state == Active listing.quantity > 0 Amount: Aus listing.price gelesen — kein user-supplied amount (verhindert Manipulation) ``` `escrow_id` wird vom Buyer-Client beim Aufruf festgelegt (z.B. random u64). Der spätere EscrowVault-PDA ist damit von Anfang an berechenbar — `accept_order` kann Vault-PDA aus `(seller, escrow_id)` ableiten ohne dass der EscrowAccount schon existiert. Bei Kollision (extrem unwahrscheinlich bei random u64) schlägt `create_escrow_prefunded` mit PDA-Already-In-Use fehl — Buyer wählt einfach eine neue ID. #### `accept_order()` ``` Signer: Seller [+ Resolver falls SignatureGated] Aktion: 1. Resolver Policy prüfen (is_signer oder CPI) 2. EscrowVault-PDA Adresse berechnen: escrow_pda = find_pda(["escrow", seller, escrow_id], DESCRO_PROGRAM_ID) vault_pda = find_pda(["vault", escrow_pda], DESCRO_PROGRAM_ID) 3. System transfer: OrderVault → vault_pda (Lamports landen auf nicht-existentem PDA) 4. CPI → descro.create_escrow_prefunded(seller, buyer, amount, resolver, escrow_id) descro erstellt EscrowAccount, verifiziert Vault-Balance ≥ amount, State = Active 5. listing.quantity -= 1 6. OrderAccount schließen (rent → seller) ``` **Kopplung:** `solisting` kennt die Program-ID von `descro` (als Konfigurationsparameter, nicht hardcoded in `descro`). `descro` kennt `solisting` nicht. #### `reject_order()` ``` Signer: Seller Aktion: OrderVault → Buyer, OrderAccount schließen ``` #### `cancel_order()` ``` Signer: Buyer Guards: state == AwaitingSellerAccept Clock::get()?.unix_timestamp > order.created_at + ORDER_TIMEOUT (z.B. 3 Tage) Aktion: OrderVault → Buyer, OrderAccount schließen Zweck: Verhindert SOL-Lockup wenn Seller nie reagiert ``` --- ## Vollständiger Flow: End-to-End ### Normalfall (kein Dispute) ``` SELLER BUYER RESOLVER │ │ │ │ create_listing() │ │ │ price=1 SOL, qty=5 │ │ │ resolver=eBay-Pubkey │ │ │────────────────────────▶ Chain │ │ │ │ │ │ [sieht Listing] │ │ │ create_order() │ │ │ [1 SOL → OrderVault] │ │ │────────────────────────▶ Chain │ │ │ │ [Event: neue Order] │ │ │ [prüft: Ware verfügbar?] │ │ │ │ │ ├─ accept_order() ─────────────────────────────────── ┤ │ Seller signiert │ │ Resolver co-signiert (falls SignatureGated) │ │ → OrderVault → EscrowVault │ │ → CPI: descro.create_escrow_prefunded() │ │────────────────────────▶ Chain │ │ │ │ │ [versendet Ware] │ │ │ │ │ │ │ [Paket erhalten ✓] │ │ │ complete() │ │ │────────────────────────▶ Chain │ │ │ │ [1 SOL im Wallet] │ │ ``` ### Dispute-Fall ``` │ │ dispute() │ │ │────────────────────────▶ Chain │ │ dispute_raised_at = now │ │ │ │ │ │ [Resolver prüft: Tracking, Fotos, Beweise] │ │ │ │ │ resolve(Buyer) │ │ │◀────────────────────────┤ │ │ CPI → Registry.update_stats() │ │────────────────────────▶ Chain ``` ### Resolver-Ausfall nach Commitment (Notfall) ``` [14 Tage vergangen, kein resolve()] Seller UND Buyer │ signieren gemeinsam: │ emergency_resolve(winner) │ ─────────────────────────▶ Chain [SOL → winner, fertig] ``` ### Direktnutzung ohne solisting (dezentraler Client) ``` Buyer kennt Seller direkt, kein Listing nötig: Seller: create_escrow(buyer, resolver, amount, escrow_id) Buyer: deposit() ... normaler Flow ... ``` --- ## Signing-Matrix | Instruction | Seller | Buyer | Resolver | Notes | |---|---|---|---|---| | `create_listing` | ✓ | | | | | `create_order` | | ✓ | | SOL in OrderVault | | `accept_order` | ✓ | | cond. | Resolver nur wenn SignatureGated | | `reject_order` | ✓ | | | SOL zurück an Buyer | | `cancel_order` | | ✓ | | Nur nach Timeout | | `create_escrow` | ✓ | | | Direkte Nutzung | | `create_escrow_prefunded` | ✓ | | | Via CPI von solisting | | `deposit` | | ✓ | | Direkte Nutzung | | `complete` | | ✓ | | | | `dispute` | ✓/✓ | ✓/✓ | | Einer von beiden | | `resolve` | | | ✓ | Wallet oder Program via CPI | | `emergency_resolve` | ✓ | ✓ | | Beide müssen signieren | --- ## Was diese Programme bewusst NICHT definieren - **`descro`:** Listing-Format, Matching-Logik, wer solisting ist, interne Resolver-Logik - **`solisting`:** Wie der Resolver den Dispute löst, Identity/KYC, Shipping-Verifizierung - **Beide:** Privacy, USDC-Support (spätere Phase), EU-Compliance (Application Layer) --- ## Offene Fragen (spätere Phasen) - **Mehrere simultane Orders auf letztes Item:** Seller kann nur eine accepten. Die anderen warten auf `cancel_order` nach Timeout. Verbesserung möglich: Seller kann `reject_order` auf alle anderen aktiv aufrufen sobald Lager leer. - **Listing-Preis in USDC:** Erfordert SPL Token Integration in beiden Programmen. - **Shipping-Adresse:** Muss off-chain encrypted übermittelt werden (on-chain wäre Privacy-Desaster). Nur für Seller sichtbar, z.B. via Buyer's Public Key verschlüsselt. - **Events / Indexing:** `emit!()` in allen State-ändernden Instructions damit Indexer kein vollständiges Chain-Scanning brauchen. - **Listing-Referenz im EscrowAccount:** Aktuell hat `descro` kein `listing_pda` Feld. Für Nachvollziehbarkeit könnte es Optional aufgenommen werden — `descro` validiert es aber nicht.