Oracle-based P2P escrow on Solana (Anchor 1.0.2).
A buyer locks SPL tokens in a program-owned vault. A designated oracle issues a binary verdict — tokens either go to the seller (verdict true) or are refunded to the buyer (verdict false or timeout). The oracle never touches the funds.
Pending ── oracle_verify(true) ──→ Released (vault → seller, accounts closed)
│
├─ oracle_verify(false) ──→ Failed
│
└─ refund (expired & no verdict, or Failed) ──→ Refunded (vault → buyer, accounts closed)
| Role | Description |
|---|---|
| Buyer | Locks tokens into the vault. Signs initialize and refund. |
| Seller | Receives tokens on successful outcome. Signs initialize. |
| Oracle | Binary verifier — sets the verdict via oracle_verify. Never holds funds. |
- Escrow:
[b"escrow", buyer, seller, mint, nonce.to_le_bytes()] - Vault:
[b"vault", escrow.key()]
The nonce allows multiple escrows between the same buyer/seller/mint pair.
Creates an escrow PDA and a vault token account. Transfers amount tokens from buyer → vault via CPI transfer_checked.
- Signers: buyer, seller
- Args:
amount,nonce,expires_at,oracle
The oracle submits a binary verdict:
-
true→ vault tokens transferred to seller, both accounts closed -
false→ state set toFailed, tokens remain in vault -
Signer: oracle
-
Guard:
has_one = oracle,state == Pending,oracle_verdict.is_none()
Returns vault tokens to the buyer and closes both accounts. Allowed when:
-
State is
Failed(oracle saidfalse), or -
State is
Pendingandexpires_athas passed -
Signer: buyer
-
Guards:
has_one = buyer,state == Pending || Failed, expiry check
13 guards from the Protocol Design Review are implemented:
- State machine validation
- PDA seed verification (Anchor
seedsconstraint) - Signed seeds for CPI transfers
- Oracle authority (
has_one = oracle) - Buyer/seller signer checks
closeon vault + escrow in final states- CPI
transfer_checked(mint + decimals verified) - Timeout guard in
refund - Double-verdict prevention
- Non-zero amount
- Future expiration
- Nonce-based PDA uniqueness
- Checked math (Anchor overflow protection)
| # | Test | Type |
|---|---|---|
| 1 | Happy path: initialize → verify(true) → seller receives | Success |
| 2 | Oracle false: initialize → verify(false) → refund → buyer receives | Success |
| 3 | Timeout refund: initialize → expire → refund → buyer receives | Success |
| 4 | Wrong oracle signer | Failure |
| 5 | Double oracle_verify | Failure |
| 6 | Refund before expiry | Failure |
| 7 | Zero amount initialize | Failure |
| 8 | Past expiration initialize | Failure |
| 9 | Attacker (not buyer) calls refund | Failure |
| 10 | Missing seller signer | Failure |
Tests run in two stacks:
- Rust/litesvm — fast, unit-level (3 tests)
- TypeScript/Mocha/Chai — full integration via
anchor test(11 tests: the 10 scenarios above plus a validator-connectivity sanity check)
Deployed on Solana devnet:
DuGYyHub8HMAXgeiA46bU1kapjLEaqLH7XxWtSWVWiqW
- Rust 1.81+ with
solanatoolchain - Solana CLI 3.x / 4.x
- Anchor 1.0.2
- Node.js 18+
git clone <repo>
cd magican-oracle-escrow
npm install
anchor build
anchor test├── Anchor.toml
├── programs/
│ └── magican-oracle-escrow/
│ ├── src/
│ │ ├── lib.rs # Entrypoint and instruction dispatch
│ │ ├── contexts.rs # Account structs (Initialize, OracleVerify, Refund)
│ │ ├── instructions/
│ │ │ ├── initialize.rs
│ │ │ ├── oracle_verify.rs
│ │ │ └── refund.rs
│ │ ├── state.rs # Escrow and EscrowState
│ │ ├── error.rs # Custom error codes
│ │ └── constants.rs # PDA seed strings
│ └── tests/
│ └── test_initialize.rs # Rust/litesvm tests
├── tests/
│ └── escrow.ts # TypeScript integration tests
└── docs/
├── IDEA.md # Original concept
├── PDR.md # Protocol Design Review
└── PROGRESS.md # Build progress (per-phase)