add protocol flow design spec for descro + solisting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
414
docs/superpowers/specs/2026-05-19-descro-flow-design.md
Normal file
414
docs/superpowers/specs/2026-05-19-descro-flow-design.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# 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<Pubkey>,
|
||||
pub state: EscrowState,
|
||||
pub bump: u8,
|
||||
pub vault_bump: u8,
|
||||
pub escrow_id: u64,
|
||||
pub dispute_raised_at: Option<i64>, // 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<Account<'info, ResolverEntry>>,
|
||||
}
|
||||
|
||||
// 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.
|
||||
Reference in New Issue
Block a user