Reference
Solana programs
Three Anchor programs carry Vouch verdicts on Solana: pof-gate turns attestations into receipts, pof-credit lends against receipts, and pof-reserve keeps an attested reserves feed. Every instruction, account and error below is generated from the programs’ IDLs.
On this page
#How they fit together
| Program | Role | Tests |
|---|---|---|
| pof-gate | Ed25519 attestations by introspection, k of n; one receipt per proof, bound to a wallet; consumption by consumer-program PDAs only. | replay, wrong key, other message, foreign offsets, late or truncated signatures, stale/future/expired/invalid/unknown, malformed, bound account only, threshold 2 of 3, admin and attestor-set rules, initialize by the upgrade authority only |
| pof-credit | Pools, credit lines and draws against receipts, with freshness and refreshes. | happy path, draw needs the borrower and the pool mint, one line per receipt, a line per proof, freshness, refresh rules, pool floor and window, mint authority |
| pof-reserve | An attested reserves feed and Secure Mint. | publish then mint within the reserves, only the issuer mints, a stale feed mints nothing until refreshed, publish refuses what it must |
26 LiteSVM tests run in CI against the compiled programs.
#The attestation message
The attestor signs these 139 bytes with Ed25519 only for a Valid verdict on a proof bound to a Solana account. pof-gate parses exactly this layout, checks the domain, the verdict and the claim kind, that the slot is within max_age_slots and that expires_at is in the future.
#How signatures are checked
Programs cannot run Ed25519 cheaply themselves, so the transaction carries one Ed25519 precompile instruction per attestor signature before the program’s instruction. The program reads those instructions through the Instructions sysvar and counts a signer only when:
- the instruction comes before the program’s own instruction;
- every offset in it is
u16::MAX, meaning the key, message and signature are inside that same instruction (no pointing into other data); - the key is on the allowlist and has not been counted already;
- the signed message is byte for byte the one being submitted.
Only when the count reaches the threshold does the program act. The runtime has already failed the transaction if any signature was bad.
#pof-gate
Program id A6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C on devnet. 4 instructions, 2 account types, 15 errors.
Instructions
pof_gate::initialize(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
config | writable · PDA | ["config"] |
admin | writable · signer | Must be the program's upgrade authority: otherwise anyone could create the config first and make themselves admin. |
program | fixed | A6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C |
program_data | — | |
system_program | fixed | 11111111111111111111111111111111 |
pof_gate::mark_consumed()
Called by a consumer program (by CPI) to spend a receipt exactly once. The beneficiary must sign, so nobody can burn someone else's receipt. The consumer program must sign too, through its `[b"consumer"]` PDA, so the program recorded in `consumed_by` is the one that called.
| Account | Flags | Seeds / notes |
|---|---|---|
receipt | writable · PDA | ["receipt", receipt.subject] |
beneficiary | signer | |
consumer | signer · PDA | ["consumer"] |
consumer_program | — |
pof_gate::set_attestors(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
config | writable · PDA | ["config"] |
admin | signer |
pof_gate::submit_attestation(subject: [u8; 32], message: bytes)
Checks, cheapest first: layout and domain · verdict · claim kind · freshness · expiry · signatures (introspection) · create the receipt (fails if it exists: replay protection).
| Account | Flags | Seeds / notes |
|---|---|---|
config | PDA | ["config"] |
receipt | writable · PDA | ["receipt", subject] |
payer | writable · signer | |
instructions | fixed | Sysvar1nstructions1111111111111111111111111 |
system_program | fixed | 11111111111111111111111111111111 |
Accounts
ClaimReceipt
| Field | Type | Notes |
|---|---|---|
subject | [u8; 32] | blake2b(statement): one proof, one receipt. |
beneficiary | pubkey | The Solana account the proof is bound to. |
audience | [u8; 32] | blake2b of the verifier identifier the proof was made for. |
claim_kind | u8 | |
claim_value | u64 | |
anchor_height | u32 | |
expires_at | i64 | |
attested_slot | u64 | |
signers | u8 | |
created_slot | u64 | |
consumed | bool | |
consumed_by | pubkey | The consumer program that spent it. |
bump | u8 |
Config
| Field | Type | Notes |
|---|---|---|
admin | pubkey | |
attestors | Vec<pubkey> | |
threshold | u8 | |
max_age_slots | u64 | |
bump | u8 |
Events
| Event | Fields |
|---|---|
AttestorsChanged | threshold, count |
ReceiptConsumed | subject, consumer |
ReceiptCreated | subject, beneficiary, claim_value, anchor_height |
Errors
| Code | Name | Message |
|---|---|---|
| 6000 | BadLength | attestation must be 139 bytes |
| 6001 | BadDomain | attestation is not domain-separated as POF-ATTEST-v1 |
| 6002 | BadExpiry | expiry out of range |
| 6003 | NotValid | only Valid verdicts are accepted |
| 6004 | UnknownClaim | unknown claim kind |
| 6005 | Stale | attestation is older than max_age_slots, or from the future |
| 6006 | Expired | the proof behind this attestation has expired |
| 6007 | BadEd25519 | malformed Ed25519 instruction |
| 6008 | ForeignOffsets | an Ed25519 signature entry points outside its own instruction |
| 6009 | NotEnoughSignatures | not enough allowlisted attestors signed this exact message |
| 6010 | AlreadyConsumed | receipt already consumed |
| 6011 | WrongBeneficiary | signer is not the account the proof is bound to |
| 6012 | BadAttestorSet | attestor set must be 1–8 distinct keys with 1 ≤ threshold ≤ count |
| 6013 | SubjectMismatch | the subject argument does not match the attestation |
| 6014 | NotUpgradeAuthority | only the program's upgrade authority can initialize it |
#pof-credit
Program id J6ewFZAzcTkbVsYNBrcZvzdBtTTUK5UrpqCg9jiNyra1 on devnet. 5 instructions, 2 account types, 9 errors.
Instructions
pof_credit::draw(amount: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
pool | PDA | ["pool", pool.mint] |
line | writable · PDA | ["line", line.opened_against, borrower] |
borrower | signer | |
vault | writable | |
borrower_token | writable | |
token_program | fixed | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
pof_credit::init_pool(audience: [u8; 32], required_zatoshi: u64, line_limit: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
pool | writable · PDA | ["pool", mint] |
mint | — | The pool's creator must control the mint: nobody can open a pool under someone else's asset. |
vault | writable · PDA | ["vault", pool] |
authority | writable · signer | |
token_program | fixed | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
system_program | fixed | 11111111111111111111111111111111 |
pof_credit::open_line()
| Account | Flags | Seeds / notes |
|---|---|---|
pool | PDA | ["pool", pool.mint] |
receipt | writable | |
line | writable · PDA | ["line", receipt.subject, borrower] |
borrower | signer | |
payer | writable · signer | Pays the line's rent, so a borrower needs no SOL (in the demo, the relayer). |
consumer | PDA | ["consumer"] |
consumer_program | fixed | This program, named to pof-gate as the consumer. |
gate_program | fixed | A6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C |
system_program | fixed | 11111111111111111111111111111111 |
pof_credit::refresh_line()
Present a new proof for an open line. The receipt must meet the pool's terms like the one that opened the line, and its anchor must be no older than the line's last.
| Account | Flags | Seeds / notes |
|---|---|---|
pool | PDA | ["pool", pool.mint] |
line | writable · PDA | ["line", line.opened_against, borrower] |
receipt | writable | |
borrower | signer | |
consumer | PDA | ["consumer"] |
consumer_program | fixed | This program, named to pof-gate as the consumer. |
gate_program | fixed | A6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C |
pof_credit::set_freshness(fresh_secs: i64, min_anchor_height: u32)
The pool's authority sets how long a proof keeps a line drawable, and the oldest Zcash anchor the pool accepts when a line is opened or refreshed.
| Account | Flags | Seeds / notes |
|---|---|---|
pool | writable · PDA | ["pool", pool.mint] |
authority | signer |
Accounts
CreditLine
| Field | Type | Notes |
|---|---|---|
pool | pubkey | |
borrower | pubkey | |
limit | u64 | |
drawn | u64 | |
opened_against | [u8; 32] | The receipt subject (proof id) this line was opened against. |
anchor_height | u32 | The anchor of the latest proof: the one that opened the line, or the last refresh. |
opened_slot | u64 | |
bump | u8 | |
fresh_until | i64 | Draws are refused from this time on, until a refresh. |
refreshed_against | [u8; 32] | The receipt subject of the latest proof. |
refreshes | u32 |
Pool
| Field | Type | Notes |
|---|---|---|
authority | pubkey | |
mint | pubkey | |
vault | pubkey | |
audience | [u8; 32] | blake2b("pof-audience:" ‖ identifier). Proofs for anyone else are refused. |
required_zatoshi | u64 | |
line_limit | u64 | Per-line limit, in the mint's base units. |
bump | u8 | |
fresh_secs | i64 | How long a proof keeps a line drawable, in seconds. |
min_anchor_height | u32 | The oldest Zcash anchor height accepted when a line is opened or refreshed. |
Events
| Event | Fields |
|---|---|
FreshnessChanged | pool, fresh_secs, min_anchor_height |
LineOpened | line, borrower, limit, against |
LineRefreshed | line, against, anchor_height, fresh_until |
Errors
| Code | Name | Message |
|---|---|---|
| 6000 | BadTerms | terms must be non-zero |
| 6001 | WrongAudience | the proof was made for a different verifier |
| 6002 | BelowThreshold | the proven amount is below this pool's threshold |
| 6003 | NotBoundToSigner | the signer is not the account the proof is bound to |
| 6004 | ReceiptConsumed | this receipt has already funded a line |
| 6005 | ProofExpired | the proof has expired |
| 6006 | OverLimit | draw exceeds the line's limit |
| 6007 | AnchorTooOld | the proof is older than this pool accepts: present a newer one |
| 6008 | NeedsFreshProof | the line's latest proof is no longer fresh: refresh it with a new proof |
#pof-reserve
Program id 5Q26Tjcd4equ5te5hF15eG4xK6uvkbZtKkEeqdf535h9 on devnet. 5 instructions, 2 account types, 17 errors.
Instructions
pof_reserve::init_feed(audience: [u8; 32], heartbeat_slots: u64, issuer: pubkey)
A feed for one verifier identifier (its audience hash), with its own demo token that only `issuer` may mint. Admin only.
| Account | Flags | Seeds / notes |
|---|---|---|
config | PDA | ["config"] |
feed | writable · PDA | ["feed", audience] |
mint | writable · PDA | ["mint", feed] |
admin | writable · signer | |
token_program | fixed | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
system_program | fixed | 11111111111111111111111111111111 |
pof_reserve::initialize(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
config | writable · PDA | ["config"] |
admin | writable · signer | Must be the program's upgrade authority: otherwise anyone could create the config first. |
program | fixed | 5Q26Tjcd4equ5te5hF15eG4xK6uvkbZtKkEeqdf535h9 |
program_data | — | |
system_program | fixed | 11111111111111111111111111111111 |
pof_reserve::publish(message: bytes)
Write a signed batch verdict to the feed. Anyone may submit it; only the allowlisted attestors' signatures make it count. A feed never goes back to an older anchor.
| Account | Flags | Seeds / notes |
|---|---|---|
config | PDA | ["config"] |
feed | writable · PDA | ["feed", feed.audience] |
instructions | fixed | Sysvar1nstructions1111111111111111111111111 |
pof_reserve::secure_mint(amount: u64)
Secure Mint: the feed's token mints only while the feed is fresh and the supply stays within the proven reserves. The issuer signs.
| Account | Flags | Seeds / notes |
|---|---|---|
feed | PDA | ["feed", feed.audience] |
mint | writable | |
to | writable | |
issuer | signer | |
token_program | fixed | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
pof_reserve::set_attestors(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)
| Account | Flags | Seeds / notes |
|---|---|---|
config | writable · PDA | ["config"] |
admin | signer |
Accounts
Config
| Field | Type | Notes |
|---|---|---|
admin | pubkey | |
attestors | Vec<pubkey> | |
threshold | u8 | |
max_age_slots | u64 | |
bump | u8 |
Feed
| Field | Type | Notes |
|---|---|---|
audience | [u8; 32] | blake2b of the verifier identifier the reserves were proven to. |
issuer | pubkey | Who may mint the feed's token. |
mint | pubkey | |
heartbeat_slots | u64 | |
scope | [u8; 32] | |
total_zatoshi | u64 | |
members | u16 | |
anchor_height | u32 | |
nc_height | u32 | |
expires_at | i64 | |
updated_slot | u64 | |
publishes | u32 | |
bump | u8 |
Events
| Event | Fields |
|---|---|
FeedUpdated | audience, total_zatoshi, anchor_height, signers |
SecureMinted | audience, amount, supply |
Errors
| Code | Name | Message |
|---|---|---|
| 6000 | BadLength | a reserve message is 112 bytes |
| 6001 | BadDomain | message is not domain-separated as POF-RESERVE-v1 |
| 6002 | BadExpiry | expiry out of range |
| 6003 | BadMessage | malformed reserve message |
| 6004 | BadEd25519 | malformed Ed25519 instruction |
| 6005 | ForeignOffsets | an Ed25519 signature entry points outside its own instruction |
| 6006 | NotEnoughSignatures | not enough allowlisted attestors signed this exact message |
| 6007 | Stale | the attestation is older than max_age_slots, or from the future |
| 6008 | Expired | the proofs behind these reserves have expired |
| 6009 | WrongAudience | the reserves were proven to another verifier than this feed |
| 6010 | OlderAnchor | a feed never goes back to an older anchor |
| 6011 | NeverPublished | the feed has never been published |
| 6012 | StaleFeed | the feed is stale: no publish within its heartbeat |
| 6013 | OverReserves | minting this would exceed the proven reserves |
| 6014 | BadHeartbeat | heartbeat must be at least one slot |
| 6015 | BadAttestorSet | attestor set must be 1–8 distinct keys with 1 ≤ threshold ≤ count |
| 6016 | NotUpgradeAuthority | only the program's upgrade authority can initialize it |
#Deploying
POF_ATTEST_URL=https://<attestor> [ATTESTORS=<b58,…>] ./scripts/deploy-devnet.shThe script builds and deploys under the fixed program ids, copies the IDLs to the site, allowlists the attestor (read from /v1/pubkey when ATTESTORS is unset), creates the credit pool and the reserves feed, and prints the site’s environment. The admin keeps the upgrade authority, which initialize requires. Local validators load pof-gate as an upgradeable program for the same reason.