descro
A decentralized escrow protocol on Solana for physical goods trading. Fully permissionless and resolver-agnostic — any wallet or program that implements the resolver interface can adjudicate disputes.
Overview
APPLICATION LAYER
Central Platform (own resolver, KYC, normal UX)
Decentralized Clients (raw protocol access, free resolver choice)
│
PROTOCOL LAYER
┌─────────────────┐ ┌──────────────────────┐
│ Escrow Program │────▶│ Resolver Registry │
│ (descro) │ CPI │ (descro_ext_res.) │
└─────────────────┘ └──────────────────────┘
The protocol only defines the interface. Any resolver that implements it is compatible — from a single authority wallet to a jury DAO voting contract.
Programs
| Program | ID |
|---|---|
descro |
DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3 |
descro_ext_resolvers |
GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp |
Escrow Program (descro)
Holds trade state and SOL. Two PDAs per escrow:
- EscrowAccount
["escrow", seller_pubkey, escrow_id_le_bytes]— trade state - Vault
["vault", escrow_account_pubkey]— system-owned, holds SOL only
Instruction flow:
create_escrow (seller)
└─▶ deposit (buyer)
├─▶ complete (buyer) → SOL released to seller
└─▶ dispute (buyer | seller)
└─▶ resolve (resolver) → SOL released to winner
cancel (seller, only from AwaitingDeposit)
The dispute_resolver field in the escrow can be any pubkey — a wallet signing directly or a program calling resolve() via CPI. The check is identical either way.
Emergency resolve: After 14 days in Disputed state with no resolution, either party can call emergency_resolve to unlock funds.
Resolver Registry (descro_ext_resolvers)
An on-chain directory for resolvers. Resolvers register with metadata (name, type, fee, URI); resolution stats are auto-incremented by the escrow program via CPI and cannot be manipulated externally.
update_stats is callable only by the escrow program — enforced via an ["escrow_authority"] PDA that only the escrow program can sign for.
Resolver types: CentralAuthority, JuryDAO, MAD, Algorithmic, Multisig
Repository Structure
programs/
├── descro/ — Escrow Program (Rust/Anchor)
└── descro_ext_resolvers/ — Resolver Registry (Rust/Anchor)
sdk/ — @descro/sdk TypeScript client
├── src/
│ ├── idl/ — Anchor-generated IDL JSON
│ ├── escrow.ts — EscrowClient (instruction builders + fetchers)
│ ├── registry.ts — RegistryClient
│ ├── listener.ts — WebSocket subscriptions
│ ├── pda.ts — PDA derivation helpers
│ ├── types.ts — TypeScript types matching on-chain state
│ └── index.ts — Re-exports + DescroSdk convenience class
app/ — Next.js + Tamagui playground
├── src/app/ — App Router pages + providers
├── src/components/ — EscrowDetail, ResolverPanel, …
└── src/hooks/ — useEscrows, useEscrowDetail
Getting Started
Prerequisites
- Rust +
cargo-build-sbf - Solana CLI
- Node.js + Yarn
Build
Programs must be built in order — the escrow program depends on the registry:
cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml
cargo build-sbf --manifest-path programs/descro/Cargo.toml
Install JS dependencies from the repo root:
yarn install
Test
Tests load the compiled .so files via include_bytes!, so programs must be built first:
cargo test --manifest-path programs/descro/Cargo.toml
cargo test --manifest-path programs/descro_ext_resolvers/Cargo.toml
Run a single test:
cargo test --manifest-path programs/descro/Cargo.toml -- test_create_escrow
cargo test --manifest-path programs/descro/Cargo.toml --test test_resolve
App (dev server)
yarn workspace descro-app run dev
SDK Usage
The SDK is consumed as raw TypeScript via transpilePackages in the Next.js config — no build step needed.
import { DescroSdk } from "@descro/sdk";
const sdk = new DescroSdk(provider);
// Create escrow
await sdk.escrow.createEscrow({ amount, disputeResolver, escrowId });
// Fetch all escrows for a seller
const escrows = await sdk.escrow.fetchEscrowsBySeller(sellerPubkey);
// Subscribe to state changes
sdk.listener.onEscrowUpdate(escrowPubkey, (account) => { ... });
Security Properties
| Property | Mechanism |
|---|---|
| Nobody can take SOL without consent | Network enforces signature requirement at protocol level |
| Vault can only be drained by the escrow program | Vault owner = system_program, only accessible via invoke_signed with correct seeds |
Wrong resolver cannot call resolve() |
require!(signer.key() == escrow.dispute_resolver) |
| PDAs cannot be spoofed | Program verifies seeds on-chain for every instruction |
| Registry stats cannot be manipulated | update_stats only callable via CPI from the escrow program |
Architecture Notes
- Anchor 1.0.x:
UncheckedAccount<'info>with/// CHECK:doc comments;#[derive(InitSpace)]+#[max_len(N)]for strings. @coral-xyz/anchor@0.32.x: Usenew Program(idl, provider)— the IDL'saddressfield provides the program ID. The 3-argument form is broken in 0.32.x.- Yarn workspaces:
nodeLinker: node-modules(not PnP) — required for Next.js Turbopack compatibility. - LiteSVM tests: each
tests/common/mod.rsprovidessetup(), PDA helpers,send()/try_send(),ix_*builders, andread_*deserializers.test_resolve_with_registry.rsloads both.sofiles to test the full CPI flow.