Security Model
Pina’s safety posture is built around explicit validation and predictable state transitions.
Core invariants
- Type correctness: account bytes must match expected discriminator and layout.
- Authority correctness: signer/owner checks must precede mutation.
- PDA correctness: seed and bump checks must gate PDA-bound operations.
- Value correctness: arithmetic and balance mutations must be checked.
Version-safe binary layout and compatibility
The discriminator-first model makes byte layout part of protocol compatibility. Treat every #[account] struct as ABI:
- Do not reorder fields.
- Do not change existing discriminator values.
- Do not alter field types in-place without migration.
- If a struct grows, treat it as a new versioned shape and migrate state explicitly.
Discriminator and payload versioning
| Change | Compatibility impact |
|---|---|
| Add a new enum variant | Usually backward-compatible if old clients ignore unknown variants |
| Change an existing variant value | Breaking for every historical byte slice |
| Reorder or remove struct fields | Breaking (offsets change) |
| Append fields to a struct | Mostly non-breaking, but consumers must accept the larger size |
Switch primitive width (u8 → u16, etc.) | Breaking for serialized payloads at that boundary |
For on-chain accounts, treat layout as part of protocol ABI:
- Keep field order stable.
- Introduce optional
versionfields at the tail for in-place migration strategies. - Never change existing discriminator values in place.
- When incompatible layout changes are required, perform explicit migration with a new account version and an operator upgrade flow.
For instruction payloads:
- Prefer additive migration: add a new variant and keep legacy handlers for a release cycle.
- Reject stale payload shapes with explicit errors rather than silently reinterpreting bytes.
High-priority guardrails
- Prefer checked arithmetic (
checked_add,checked_sub) for all user-facing or balance-affecting values. - Ensure all token account types used by helper traits implement
AccountValidation. - Keep close/transfer helpers conservation-safe (no temporary double-crediting).
Best practices
- Always call
assert_signer()before trusting authority accounts - Always call
assert_owner()/assert_owners()beforeas_token_*()methods - Always call
assert_empty()before account initialization to prevent reinitialization attacks - Always verify program accounts with
assert_address()/assert_program()before CPI invocations - Use
assert_type::<T>()to prevent type cosplay — it checks discriminator, owner, and data size - Use
close_with_recipient()withzeroed()to safely close accounts and prevent revival attacks - Prefer
assert_seeds()/assert_canonical_bump()overassert_seeds_with_bump()to enforce canonical PDA bumps - Namespace PDA seeds with type-specific prefixes to prevent PDA sharing across account types
Testing strategy
- Unit tests for negative validation cases.
- Regression tests for every previously fixed bug class.
- Integration tests for cross-account invariants where mutation order matters.