On-Chain Patterns Blueprint
On-chain program patterns are reusable designs for how a Solana program creates state, decides who may change it, moves value, advances lifecycle, and reacts to time. They are not optional style guides. On Solana, every durable field lives in a program-owned account you must pass explicitly, so security and product rules show up as account layouts, signer checks, init guards, and transition tables rather than as hidden server-side middleware.
Program Patterns Basics is the hands-on entry; Access Control & Authorities, Account Initialization Patterns, Fee & Treasury Patterns, State Machines On-Chain, and Time & Clock each zoom into one mechanism. This page sits underneath: how those patterns form one blueprint you can apply when designing or reviewing any program.
Summary
- Safe Solana programs combine access control, safe initialization, value routing (fees and treasuries), explicit lifecycle state, and clock-backed time rules so every mutation has a known actor, a known account shape, and a known legal next step.
- Insight: The runtime enforces ownership and locks; it does not enforce your product. Patterns encode "who, when, how much, and in which order" so clients cannot invent transitions, reinit accounts, or drain treasuries through missing checks.
- Key Concepts: authority vs owner, signer and role checks, init / reinit guards, discriminators, treasury PDAs and fee bps, status enums and transition tables, Clock sysvar, slots vs unix timestamps.
- When to Use: Designing a new program, reviewing instruction surfaces, encoding escrow or order flows, adding protocol fees, rotating admins, or explaining why "just a bool" is not a lifecycle.
- Limitations/Trade-offs: Patterns add account fields, instructions, and CU; over-centralized admins trade flexibility for trust; time rules need grace for clock drift; fees and state machines must stay simple enough to audit.
- Related Topics: access control, account init, fees and treasuries, state machines, time and clock, program patterns basics.
Foundations
A Solana program is a stateless executable. Durable rules live in accounts: config, user records, vaults, orders, and treasuries. Patterns answer five recurring questions every instruction must settle before it mutates anything:
- Who is allowed? Access control and authorities.
- Does this account already mean something? Initialization and reinit protection.
- Where does value go? Fees, treasuries, and custody paths.
- What phase is this object in? State machines and legal transitions.
- Is it too early or too late? Clock, slots, and deadlines.
Those questions compose. A fill instruction might require the maker or taker as signer, an order account that was initialized with the correct discriminator, a status of Open, a non-expired deadline from Clock, and a fee split into a treasury ATA - all before any token CPI.
Instruction arrives
|
v
+------------------+
| Access control | <- signer? role? PDA authority?
+--------+---------+
|
v
+------------------+
| Account shape | <- initialized? right disc? right owner?
+--------+---------+
|
v
+------------------+
| Lifecycle / time | <- legal status? deadline? pause flag?
+--------+---------+
|
v
+------------------+
| Value movement | <- fee math, treasury, vault CPI
+--------+---------+
|
v
Commit new bytes (and events)Owner in account metadata is always a program ID: only that program may rewrite data. Authority is usually an application pubkey (or PDA) stored inside data that your program compares to a transaction signer (or authorizes via invoke_signed). Confusing the two is the root of many access-control bugs: the runtime does not know your admin field exists until you check it.
Initialization is the moment an empty (or newly allocated) buffer becomes typed state. Until a discriminator and fields are written under a reinit guard, nothing that follows (deposits, roles, fees) is trustworthy. Fees and treasuries are just value-routing patterns: checked basis-point math and a destination account whose mint and owner you verify. State machines replace ad hoc booleans with an enum and a transition table so completed or cancelled objects cannot be reused. Time comes from the Clock sysvar (slot, epoch, unix_timestamp), not from an arbitrary client argument you never re-validate on-chain.
Mechanics & Interactions
Access control and authorities
Every privileged path needs a concrete check: the expected pubkey is present, is a signer when a wallet must approve, and matches the stored role field (admin, operator, owner). Role separation keeps pause-and-config powers distinct from user deposit rights. Production admin design favors multisig or governance as the admin pubkey, two-step admin transfer (set pending, then accept), and the smallest possible admin surface so a compromised key cannot empty user vaults.
Program-controlled authorities use PDAs and invoke_signed: the program proves seeds, not a private key. User custody of personal state is usually "owner field must equal signer." Protocol custody of vaults is "PDA must sign the token transfer after business rules pass."
Account initialization patterns
Init is a one-way gate. Typical flow: System Program (or Anchor init) allocates space and assigns your program as owner; your logic writes a discriminator and default fields; later instructions reject accounts that already show that discriminator or that fail "already initialized" guards. Prefer explicit create-then-init when UX allows; treat init_if_needed as high risk unless the initialized check is airtight.
Order matters for safety: initialize structure before the account holds meaningful value. A reinit attack rewrites authority or vault pointers after funds arrived. Unique discriminators per account type prevent type confusion (passing a config where a vault is expected).
Fee and treasury patterns
Protocol fees skim basis points (bps) of an amount, then route the fee to a treasury (often a PDA-owned ATA per mint) and the remainder to the recipient. All arithmetic should use checked ops; cap configurable fee_bps on set-config (for example never above a documented max); document rounding (floor toward protocol or user). Verify treasury mint and owner on every fee path so fees do not land on a wrong ATA.
amount
|
+-- fee = amount * bps / 10_000 --> treasury
|
+-- net = amount - fee --> user / counterpartyTake fees on successful economic actions (or on clearly defined inputs), not on failed half-paths that leave users charged without the promised state change.
State machines on-chain
Lifecycle objects (orders, escrows, auctions, proposals) store a status enum. Each instruction either implements one transition or consults a shared transition map: only Open -> Filled or Open -> Cancelled, never Filled -> Open unless you intentionally design a rare admin path. Terminal states accept close (or similar cleanup) and reject business mutations. Emit events on transitions so indexers and clients do not guess from partial fields.
Centralize status checks in helpers so one forgotten instruction cannot bypass the machine. Keep the enum small; reserve values or version fields if you expect future states.
Time and clock
Read Clock::get() (or the Clock sysvar account) for binding time. Use unix_timestamp for human deadlines (vesting cliffs, escrow expiry); use slot when you care about chain progress relative to leader schedule; use epoch for stake-period style rules. Validators' timestamps have bounded drift, so production designs use grace periods rather than one-slot precision for wall-clock UX. Never trust a client-supplied "now" alone; store deadlines on init and compare them to Clock on each gated instruction.
How the patterns stack in one flow
Escrow release is a full blueprint instance:
- Init created escrow with disc, parties, amount, deadline, status
Funded. - Access control requires seller or buyer (or PDA rules) as signer depending on release vs cancel.
- State machine allows only
Funded -> ReleasedorFunded -> Cancelled/Expired. - Clock rejects release after deadline if cancel-on-expiry is the rule (or the reverse for time locks).
- Fees (if any) split on release; treasury and token accounts verified; vault PDA signs the transfer.
Miss any layer and the rest is theater: a perfect fee formula does not help if reinit resets the authority field.
Advanced Considerations & Applications
Patterns scale from a counter program to a DeFi venue; the difference is how strictly you compose them and how you trade admin power for immutability.
| Pattern cluster | Strength | Weakness | Best fit |
|---|---|---|---|
| Access control / authorities | Clear roles; rotatable admin; PDA custody | Over-broad admin is a trust sink | Config, pause, vaults, mint authority |
| Init / reinit guards | Stable typed state; stops account substitution | Extra instructions and rent discipline | Any program-owned account that holds value or roles |
| Fee / treasury | Transparent protocol revenue; on-chain audit trail | Math bugs and unbounded bps hurt users | Markets, mints with fees, withdrawal skims |
| State machines | Auditable lifecycle; blocks replay of done work | Over-fine states bloat logic | Escrow, orders, auctions, governance |
| Time / clock | Enforceable deadlines without off-chain cron | Drift and slot variance need grace | Vesting, expiry, cooldowns, epochs |
Pause flags are a narrow state machine over the whole protocol: admin sets paused, user instructions fail while true, resume is a separate privilege. Combine pause with role separation so a hot operator key cannot also change fee recipients.
Upgrade-safe design interacts with these patterns: version fields and reserved space let you add roles or states without breaking discriminators; see upgrade-oriented data design in this section when you plan long-lived accounts. Vault and escrow specializations are fee + PDA authority + state machine + clock applied to custody.
For design reviews, list every instruction and fill in: required signers and roles, init assumptions, status before/after, clock predicates, and value sinks. Gaps in that matrix are the usual exploit surface.
Common Misconceptions
- "The runtime enforces my admin role." It enforces program ownership of data. Admin and operator roles exist only if your program checks stored pubkeys and signers on every privileged instruction.
- "Init once at deploy is enough for all accounts." Deploy initializes the program binary, not user vaults or orders. Each program-owned account needs its own create/init path and reinit guard.
- "Fees can use ordinary integer math." Unchecked multiply/divide and uncapped bps are classic drain vectors. Use checked math and validate fee bounds when config changes.
- "A few booleans are the same as a state machine." Independent flags allow illegal combinations (
filledandcancelledboth true). An enum plus transition checks makes illegal states unrepresentable or rejected. - "Client timestamps are fine if the UI is honest." Anyone can build a transaction. Deadlines and cooldowns must compare to Clock (or slot) on-chain.
- "One global admin key is simpler and therefore safer." It is simpler until key loss or compromise. Multisig, two-step transfer, and minimal privileges reduce blast radius.
FAQs
What is an "on-chain program pattern" in this section?
A reusable way to structure accounts and instructions so a Solana program repeatedly solves access control, initialization, value routing, lifecycle, or time in a reviewable, secure way.
How do owner and authority differ?
Owner is the program ID in account metadata (who may write data). Authority is usually an app-level pubkey or PDA stored in data that your program checks against a signer or uses with invoke_signed.
Why is initialization treated as a security boundary?
Before init, the buffer is untyped or attacker-shaped. Without discriminators and reinit guards, an attacker can reset authorities or confuse account types after value has been deposited.
When should I use init_if_needed?
Only when the initialized check is strict (discriminator or equivalent) and reinit is impossible. Prefer plain init or separate create when the account must exist exactly once under known seeds.
How should protocol fees be implemented?
Compute fee with checked math from a capped bps, verify the treasury account (mint, owner, PDA seeds), transfer fee then net (or document order), and never allow config to set unbounded fees.
Where should a treasury live?
Typically a dedicated PDA-controlled token account (or SOL PDA) per mint or asset class, separate from user vaults, so withdrawals and fee accrual stay auditable and role-gated.
What belongs in an on-chain state machine?
Any multi-step lifecycle where some actions are illegal after others: open/fill/cancel, fund/release/refund, propose/vote/execute. Encode status, validate transitions, treat terminals as closed for business ops.
Should each transition be its own instruction?
Often yes for clarity and least privilege (fill vs cancel as separate handlers). Shared helpers can still centralize the status match so no path skips the table.
What is the Clock sysvar used for?
Binding on-chain time: unix_timestamp for wall-clock deadlines, slot for chain-relative progress, epoch for epoch-scoped rules. Read it in the program; do not trust client-only "now."
Slots or unix timestamps for escrow expiry?
Prefer unix timestamps when users reason in calendar time; use slots when you care about discrete chain progress. Allow small grace where validator clock drift matters for UX.
How do PDAs fit into these patterns?
PDAs give programs deterministic addresses and signing without private keys. They power vault authorities, config accounts, fee treasuries, and per-user state seeds that clients can derive offline.
What is two-step admin transfer?
Current admin proposes a pending_admin; the new key must sign an accept instruction. That prevents a typo from permanently handing control to an unusable address.
Can I skip state machines for a simple vault?
If the vault only holds balance and never has phases, balance plus authority may suffice. As soon as you have fund/release/cancel/expire paths, an explicit status prevents contradictory actions.
How do these patterns relate to Anchor constraints?
Anchor encodes many of them: has_one and signer constraints for access control, init / seeds / space for initialization, and account types for ownership. You still design the product rules; macros implement checks you specify.
What should a design review checklist include?
For each instruction: required signers and roles, init assumptions, status transitions, clock predicates, fee and treasury destinations, and which accounts must be PDAs with verified seeds.
Related
- Program Patterns Basics - hands-on pattern examples and building blocks
- Access Control & Authorities - roles, signer checks, admin rotation
- Account Initialization Patterns - init, reinit guards, discriminators
- Fee & Treasury Patterns - bps fees, treasuries, checked math
- State Machines On-Chain - status enums and legal transitions
- Time & Clock - Clock sysvar, slots, deadlines, cooldowns
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.