Skip to content
hopper
Get started
Safety / policy

Policy guarantees

Strict, sealed, and raw modes, plus what each access tier promises.

Formal reference for what each HopperProgramPolicy lever guarantees and what it drops. Read this before flipping a lever from STRICT toward RAW.

Named modes

Mode strict enforce_token_checks allow_unsafe
HopperProgramPolicy::STRICT true true true
HopperProgramPolicy::SEALED true true false
HopperProgramPolicy::RAW false false true

STRICT is the shipping default returned by HopperProgramPolicy::default_policy().

Naming is intentionally literal:

  • STRICT means validation and token-policy checks are enforced by default. It still permits explicit unsafe blocks because some high-performance programs need a reviewed escape hatch.
  • SEALED means STRICT plus allow_unsafe = false; handler bodies cannot contain unsafe code unless an instruction explicitly opts into unsafe_memory.
  • RAW means Hopper's automatic validation/token envelope is not promised. The author owns every signer, owner, layout, PDA, token, and aliasing check.

In short: choose STRICT for normal audited Hopper programs, SEALED when a module must be unsafe-free by default, and RAW only for hand-validated expert paths.

What each lever controls

strict

Documents that every normal handler in the module uses a typed context (Ctx<MyAccounts>), so MyAccounts::bind(ctx)? runs before the handler body. The bind call chains into the constraint check gauntlet:

  1. signer
  2. mut / owner / executable / address
  3. duplicate-writable / signer rules
  4. PDA derivation
  5. init / realloc / close preconditions
  6. constraint = expr

Flipping to strict = false is an intent marker: the author plans to use raw &mut Context<'_> handlers or other hand-validated paths and accepts responsibility for calling validate() where needed. Typed Ctx<T> handlers still bind. The handler's parameter type is the final word.

enforce_token_checks

Promise that every SPL token CPI in the module uses *_strict or *_signed_strict invoke variants. Those helpers pre-verify:

Check Helper Where
Authority is a transaction signer require_authority_signed_direct crates/hopper-runtime/src/token.rs
Token account's owner field matches authority require_token_authority same file

The SPL Token program itself re-validates both checks. Hopper's pre-check surfaces a Hopper-branded ProgramError::IncorrectAuthority or MissingRequiredSignature before the CPI so a misrouted signer or mismatched owner fails with a specific error instead of an opaque SPL failure. This closes the exploit class "attacker passes correct pubkey but wrong signer".

Flipping to enforce_token_checks = false drops the pre-check promise. The SPL program's checks still run. Only reach for this when the program has its own validation flow that makes the pre-check redundant.

allow_unsafe

When true (default), handler bodies can contain unsafe { ... } blocks and the hopper_unsafe_region! macro.

When false, the program macro emits #[deny(unsafe_code)] on every handler that does not carry #[instruction(N, unsafe_memory)]. Any stray unsafe { ... } fails to compile. The per-instruction override restores unsafe for a single handler without affecting the rest of the module.

What each policy drops

Policy Dropped invariant What this means
strict = false Framework guarantee that handlers are all typed Ctx<T> paths Author must call constraint checks manually on raw &mut Context<'_> paths. Typed-context handlers still bind.
enforce_token_checks = false Hopper-branded pre-check on token CPIs Only the SPL program's checks run. Any Hopper-side ownership mismatch surfaces as a generic CPI failure.
allow_unsafe = false Raw pointer access in handler bodies unsafe { ... } and hopper_unsafe_region! fail to compile unless the handler opts in via #[instruction(N, unsafe_memory)].
#[instruction(N, unsafe_memory)] Program-level #[deny(unsafe_code)] for this handler only Raw pointer access restored for this one handler. Other handlers stay sealed.
#[instruction(N, skip_token_checks)] Program-level token-check promise for this handler Author documents why the checks are upheld elsewhere (or not needed).

Zero-cost property

Every lever is a compile-time bool on a Copy + const struct. Readers call HOPPER_PROGRAM_POLICY.<lever> in const context; the branches fold to a single code path during codegen when the lever is known. There is no runtime state, no thread-local, no syscall. A program compiled with HopperProgramPolicy::RAW pays zero CU for Hopper's safety envelope.

Grep receipts

An auditor lands in the tree and wants a one-command inventory of every raw-pointer region:

grep -rn "hopper_unsafe_region!" crates/ examples/

Every Hopper-authored unsafe segment surfaces. The macro expands to unsafe { ... }, so the actual codegen is unchanged; the name is the indexing hook.

For the stricter "every unsafe region in the tree, Hopper or otherwise":

grep -rn "unsafe " crates/ examples/ tools/

Hopper's internals use unsafe for the zero-copy core (pointer casts, syscall wrappers, Pod overlays). Those regions are documented in UNSAFE_INVARIANTS.md.

Worked examples

  • examples/hopper-policy-vault/src/lib.rs::strict_vault, HopperProgramPolicy::STRICT for a conventional vault.
  • examples/hopper-policy-vault/src/lib.rs::sealed_vault::fast_sweep, SEALED program with one handler opting into unsafe_memory.
  • examples/hopper-policy-vault/src/lib.rs::raw_vault::hybrid_bump, RAW program demonstrating the safe -> unsafe -> safe mixed pattern inside one handler.