Skip to content
VOUCHVOUCHDocs

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

One attestation, one transactionTransactionix 0 · Ed25519 precompileverify(attestor pubkey, message, signature)every offset = u16::MAX: the data is inside this instructiona bad signature fails the whole transactionix 1 · pof-gate::submit_attestation(subject, message)reads ix 0 from the Instructions sysvarcounts distinct allowlisted signers of exactly this messagechecks domain, verdict, slot age, expiry, thresholdcreates ClaimReceipt ["receipt", subject]introspectsConfig PDA · [config]attestors (up to 8)threshold k · max_age_slotsClaimReceipt PDA["receipt", subject]one per proof: replay failsbeneficiary = bound walletreadscreates
Solana's Ed25519 precompile checks the signature; pof-gate then reads that instruction back through the Instructions sysvar and checks that its signer is allowlisted and its message is exactly the one being submitted.
Program-derived accountspof-gateConfig[config]ClaimReceipt[receipt, subject]consumed · consumed_bypof-creditPool[pool, mint]Vault[vault, pool]CreditLine[line, subject, borrower][consumer] · signs the CPIpof-reserveConfig[config]Feed[feed, audience]total · heights · slotMint[mint, feed] · authority: feedopened againstmint authority
Seeds in brackets. A receipt is keyed by the proof id, so one proof makes one receipt; a credit line is keyed by the receipt and the borrower; a feed is keyed by its audience, and its mint's authority is the feed itself.
ProgramRoleTests
pof-gateEd25519 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-creditPools, 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-reserveAn 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 attestation message (POF-ATTEST-v1)domain"POF-ATTEST-v1"13 Bsubjectproof id32 Bbeneficiarybound wallet32 Baudienceblake2b(id)32 Bkindclaim1 Bvaluezatoshi8 Banchorheight4 Bexpiresunix s8 BverdictValid1 Bslotfreshness8 B139 bytes
139 bytes, little-endian, domain-separated. The attestor signs it with Ed25519; pof-gate parses exactly this layout.

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)

AccountFlagsSeeds / notes
configwritable · PDA["config"]
adminwritable · signerMust be the program's upgrade authority: otherwise anyone could create the config first and make themselves admin.
programfixedA6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C
program_data—
system_programfixed11111111111111111111111111111111

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.

AccountFlagsSeeds / notes
receiptwritable · PDA["receipt", receipt.subject]
beneficiarysigner
consumersigner · PDA["consumer"]
consumer_program—

pof_gate::set_attestors(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)

AccountFlagsSeeds / notes
configwritable · PDA["config"]
adminsigner

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).

AccountFlagsSeeds / notes
configPDA["config"]
receiptwritable · PDA["receipt", subject]
payerwritable · signer
instructionsfixedSysvar1nstructions1111111111111111111111111
system_programfixed11111111111111111111111111111111

Accounts

ClaimReceipt

FieldTypeNotes
subject[u8; 32]blake2b(statement): one proof, one receipt.
beneficiarypubkeyThe Solana account the proof is bound to.
audience[u8; 32]blake2b of the verifier identifier the proof was made for.
claim_kindu8
claim_valueu64
anchor_heightu32
expires_ati64
attested_slotu64
signersu8
created_slotu64
consumedbool
consumed_bypubkeyThe consumer program that spent it.
bumpu8

Config

FieldTypeNotes
adminpubkey
attestorsVec<pubkey>
thresholdu8
max_age_slotsu64
bumpu8

Events

EventFields
AttestorsChangedthreshold, count
ReceiptConsumedsubject, consumer
ReceiptCreatedsubject, beneficiary, claim_value, anchor_height

Errors

CodeNameMessage
6000BadLengthattestation must be 139 bytes
6001BadDomainattestation is not domain-separated as POF-ATTEST-v1
6002BadExpiryexpiry out of range
6003NotValidonly Valid verdicts are accepted
6004UnknownClaimunknown claim kind
6005Staleattestation is older than max_age_slots, or from the future
6006Expiredthe proof behind this attestation has expired
6007BadEd25519malformed Ed25519 instruction
6008ForeignOffsetsan Ed25519 signature entry points outside its own instruction
6009NotEnoughSignaturesnot enough allowlisted attestors signed this exact message
6010AlreadyConsumedreceipt already consumed
6011WrongBeneficiarysigner is not the account the proof is bound to
6012BadAttestorSetattestor set must be 1–8 distinct keys with 1 ≤ threshold ≤ count
6013SubjectMismatchthe subject argument does not match the attestation
6014NotUpgradeAuthorityonly 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)

AccountFlagsSeeds / notes
poolPDA["pool", pool.mint]
linewritable · PDA["line", line.opened_against, borrower]
borrowersigner
vaultwritable
borrower_tokenwritable
token_programfixedTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

pof_credit::init_pool(audience: [u8; 32], required_zatoshi: u64, line_limit: u64)

AccountFlagsSeeds / notes
poolwritable · PDA["pool", mint]
mint—The pool's creator must control the mint: nobody can open a pool under someone else's asset.
vaultwritable · PDA["vault", pool]
authoritywritable · signer
token_programfixedTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
system_programfixed11111111111111111111111111111111

pof_credit::open_line()

AccountFlagsSeeds / notes
poolPDA["pool", pool.mint]
receiptwritable
linewritable · PDA["line", receipt.subject, borrower]
borrowersigner
payerwritable · signerPays the line's rent, so a borrower needs no SOL (in the demo, the relayer).
consumerPDA["consumer"]
consumer_programfixedThis program, named to pof-gate as the consumer.
gate_programfixedA6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C
system_programfixed11111111111111111111111111111111

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.

AccountFlagsSeeds / notes
poolPDA["pool", pool.mint]
linewritable · PDA["line", line.opened_against, borrower]
receiptwritable
borrowersigner
consumerPDA["consumer"]
consumer_programfixedThis program, named to pof-gate as the consumer.
gate_programfixedA6WmRTgEAHC9GHstaj9oD8woVNRw9jgu31bpKVREXn8C

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.

AccountFlagsSeeds / notes
poolwritable · PDA["pool", pool.mint]
authoritysigner

Accounts

CreditLine

FieldTypeNotes
poolpubkey
borrowerpubkey
limitu64
drawnu64
opened_against[u8; 32]The receipt subject (proof id) this line was opened against.
anchor_heightu32The anchor of the latest proof: the one that opened the line, or the last refresh.
opened_slotu64
bumpu8
fresh_untili64Draws are refused from this time on, until a refresh.
refreshed_against[u8; 32]The receipt subject of the latest proof.
refreshesu32

Pool

FieldTypeNotes
authoritypubkey
mintpubkey
vaultpubkey
audience[u8; 32]blake2b("pof-audience:" ‖ identifier). Proofs for anyone else are refused.
required_zatoshiu64
line_limitu64Per-line limit, in the mint's base units.
bumpu8
fresh_secsi64How long a proof keeps a line drawable, in seconds.
min_anchor_heightu32The oldest Zcash anchor height accepted when a line is opened or refreshed.

Events

EventFields
FreshnessChangedpool, fresh_secs, min_anchor_height
LineOpenedline, borrower, limit, against
LineRefreshedline, against, anchor_height, fresh_until

Errors

CodeNameMessage
6000BadTermsterms must be non-zero
6001WrongAudiencethe proof was made for a different verifier
6002BelowThresholdthe proven amount is below this pool's threshold
6003NotBoundToSignerthe signer is not the account the proof is bound to
6004ReceiptConsumedthis receipt has already funded a line
6005ProofExpiredthe proof has expired
6006OverLimitdraw exceeds the line's limit
6007AnchorTooOldthe proof is older than this pool accepts: present a newer one
6008NeedsFreshProofthe line's latest proof is no longer fresh: refresh it with a new proof

#pof-reserve

The reserves message (POF-RESERVE-v1)domain"POF-RESERVE-v1"14 Bscopethe batch scope32 Baudienceblake2b(id)32 Btotalzatoshi8 Bmembersproofs2 Banchorheight4 Bnc_heightunmoved since4 Bexpiresfirst expiry8 Bslotfreshness8 B112 bytes
112 bytes, little-endian. The attestor signs it after verify_batch; pof-reserve::publish parses exactly this layout.

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.

AccountFlagsSeeds / notes
configPDA["config"]
feedwritable · PDA["feed", audience]
mintwritable · PDA["mint", feed]
adminwritable · signer
token_programfixedTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
system_programfixed11111111111111111111111111111111

pof_reserve::initialize(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)

AccountFlagsSeeds / notes
configwritable · PDA["config"]
adminwritable · signerMust be the program's upgrade authority: otherwise anyone could create the config first.
programfixed5Q26Tjcd4equ5te5hF15eG4xK6uvkbZtKkEeqdf535h9
program_data—
system_programfixed11111111111111111111111111111111

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.

AccountFlagsSeeds / notes
configPDA["config"]
feedwritable · PDA["feed", feed.audience]
instructionsfixedSysvar1nstructions1111111111111111111111111

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.

AccountFlagsSeeds / notes
feedPDA["feed", feed.audience]
mintwritable
towritable
issuersigner
token_programfixedTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

pof_reserve::set_attestors(attestors: Vec<pubkey>, threshold: u8, max_age_slots: u64)

AccountFlagsSeeds / notes
configwritable · PDA["config"]
adminsigner

Accounts

Config

FieldTypeNotes
adminpubkey
attestorsVec<pubkey>
thresholdu8
max_age_slotsu64
bumpu8

Feed

FieldTypeNotes
audience[u8; 32]blake2b of the verifier identifier the reserves were proven to.
issuerpubkeyWho may mint the feed's token.
mintpubkey
heartbeat_slotsu64
scope[u8; 32]
total_zatoshiu64
membersu16
anchor_heightu32
nc_heightu32
expires_ati64
updated_slotu64
publishesu32
bumpu8

Events

EventFields
FeedUpdatedaudience, total_zatoshi, anchor_height, signers
SecureMintedaudience, amount, supply

Errors

CodeNameMessage
6000BadLengtha reserve message is 112 bytes
6001BadDomainmessage is not domain-separated as POF-RESERVE-v1
6002BadExpiryexpiry out of range
6003BadMessagemalformed reserve message
6004BadEd25519malformed Ed25519 instruction
6005ForeignOffsetsan Ed25519 signature entry points outside its own instruction
6006NotEnoughSignaturesnot enough allowlisted attestors signed this exact message
6007Stalethe attestation is older than max_age_slots, or from the future
6008Expiredthe proofs behind these reserves have expired
6009WrongAudiencethe reserves were proven to another verifier than this feed
6010OlderAnchora feed never goes back to an older anchor
6011NeverPublishedthe feed has never been published
6012StaleFeedthe feed is stale: no publish within its heartbeat
6013OverReservesminting this would exceed the proven reserves
6014BadHeartbeatheartbeat must be at least one slot
6015BadAttestorSetattestor set must be 1–8 distinct keys with 1 ≤ threshold ≤ count
6016NotUpgradeAuthorityonly the program's upgrade authority can initialize it

#Deploying

Shell
POF_ATTEST_URL=https://<attestor> [ATTESTORS=<b58,…>] ./scripts/deploy-devnet.sh

The 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.