This is a design spec for the refinement-phase prototype. It is not deployed. Target: Solana devnet, USDC (SPL, 6 decimals).
#[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]
| 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 |
vault_balance == total_donated − total_released − total_refunded at all timesrelease_tranche, refund, reassign{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).