← ReliefRail

ReliefRail: Solana/USDC escrow design (Anchor), v0.1

This is a design spec for the refinement-phase prototype. It is not deployed. Target: Solana devnet, USDC (SPL, 6 decimals).

Accounts

#[account] pub struct Campaign {
    pub organiser: Pubkey,        // creates campaign, can propose reassign
    pub grantee: Pubkey,          // relief group wallet that receives tranches
    pub mint: Pubkey,             // USDC mint
    pub vault_bump: u8,           // vault = ATA owned by PDA ["vault", campaign]
    pub verifiers: Vec<Pubkey>,   // N <= 7
    pub threshold: u8,            // M
    pub tranches: Vec<Tranche>,   // <= 12
    pub total_donated: u64,
    pub total_released: u64,
    pub total_refunded: u64,
    pub state: CampaignState,     // Active | Paused | Closed
}
pub struct Tranche { pub amount: u64, pub deadline: i64, pub attest_count: u8, pub status: TrancheStatus } // Pending|Released|Expired
#[account] pub struct DonorReceipt { pub campaign: Pubkey, pub donor: Pubkey, pub donated: u64, pub refunded: u64 }   // PDA ["receipt", campaign, donor]
#[account] pub struct Attestation { pub campaign: Pubkey, pub tranche: u8, pub verifier: Pubkey, pub evidence_hash: [u8;32], pub ts: i64 } // PDA ["att", campaign, tranche, verifier]

Instructions and rules

Ix Signer Checks / effect
create_campaign(grantee, verifiers, M, tranches) organiser 1 ≤ M ≤ N ≤ 7; deadlines increasing; vault ATA created with PDA authority
donate(amount) donor SPL transfer donor→vault; init/update DonorReceipt; emit Donated
attest(tranche, evidence_hash) verifier ∈ set tranche Pending, now ≤ deadline, the previous tranche is Released; one Attestation PDA per verifier (init fails on duplicates); attest_count += 1
release_tranche(i) anyone (crank) attest_count ≥ M and vault balance ≥ amount → PDA-signed transfer vault→grantee; status Released
expire_tranche(i) anyone now > deadline and attest_count < M → status Expired; later tranches are also frozen
refund() donor at least one tranche Expired, or campaign Closed → donor share = donated × unreleased / total_donated − refunded; transfer vault→donor
reassign(new_grantee or new_campaign) organiser + ≥ 50% donor-weighted approvals (vote PDAs) moves the unreleased balance; emits Reassigned
close() organiser all tranches finished and vault empty → reclaim rent

Invariants (tests)

Off-chain evidence bundle

{campaign, tranche, meal_count, photo_cids[], voice_cid, lat_lng_coarse (≈10 km), ts} → canonical JSON → sha256 → evidence_hash. Media is face-blurred before upload and the location is deliberately coarse (conflict-sensitive).