hopper_runtime/policy.rs
1//! Program-level safety policy.
2//!
3//! Hopper's "policy-driven zero-copy runtime" model exposes each
4//! safety lever as a bit in a compile-time const struct. The
5//! `#[hopper::program(...)]` macro parses the attribute args and
6//! emits `pub const HOPPER_PROGRAM_POLICY: HopperProgramPolicy = ...;`
7//! inside the annotated module. Users read it back through
8//! [`HopperProgramPolicy`] to specialize handler paths.
9//!
10//! ## Named modes
11//!
12//! | Mode | Levers |
13//! |---|---|
14//! | [`HopperProgramPolicy::STRICT`] | `strict`, `enforce_token_checks`, `allow_unsafe` all on. Recommended default. |
15//! | [`HopperProgramPolicy::SEALED`] | `strict` + `enforce_token_checks` on, `allow_unsafe` off. Adds a default unsafe-code denial on handler items. |
16//! | [`HopperProgramPolicy::RAW`] | Typed-validation and token-check intent off; unsafe code permitted. Typed handlers still bind. |
17//! | [`HopperProgramProfile::TINY`] | Binary-size profile for compact programs: one-byte instruction discriminators and no handler-level modifier instrumentation. |
18//!
19//! ## Zero runtime cost
20//!
21//! The policy is consumed by the program macro at compile time.
22//! `allow_unsafe = false` emits `#[deny(unsafe_code)]` on each
23//! handler so unsafe code in that item is denied by default. Called helpers
24//! and dependencies are outside this lint's scope. The handler's parameter
25//! type determines whether `ContextSpec::bind(ctx)?` runs: typed handlers
26//! bind regardless of `strict`, and raw handlers receive the raw context.
27//! `strict` and `enforce_token_checks` are author intent markers, not checks
28//! automatically inserted into arbitrary handler code. Authors can consult
29//! the constants when selecting explicit token helpers such as
30//! `invoke_strict()` and `invoke_signed_strict()`. The `*Checked` builder
31//! names describe SPL Token's mint/decimals checks; they do not mean the
32//! program policy automatically inserted Hopper authority pre-checks.
33//!
34//! No runtime flag, no thread-local, no syscall. Users who need to
35//! branch on the policy inside a handler read the const directly:
36//!
37//! ```ignore
38//! if super::HOPPER_PROGRAM_POLICY.enforce_token_checks {
39//! hopper_runtime::require!(authority.is_signer());
40//! }
41//! ```
42//!
43//! ## Per-instruction overrides
44//!
45//! A handler can override the program-level policy with
46//! `#[instruction(N, unsafe_memory, skip_token_checks, allow_arbitrary_cpi)]`. The macro
47//! emits `pub const <HANDLER>_POLICY: HopperInstructionPolicy = ...;`
48//! alongside the handler so the same const-branch pattern works at
49//! the per-instruction grain.
50
51/// Program-level safety policy emitted by `#[hopper::program(...)]`.
52///
53/// Each field is a *compile-time* lever. The const value ends up
54/// inlined at every call site the program evaluates it from, so the
55/// branches fold away when a lever is known to be on or off at
56/// compile time.
57#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58pub struct HopperProgramPolicy {
59 /// Program-level intent marker: handlers in this program run
60 /// under Hopper's full enforcement envelope.
61 ///
62 /// The actual per-handler behaviour is controlled by the
63 /// handler's context parameter type. A handler typed as
64 /// `Ctx<MyAccounts>` always runs `MyAccounts::bind(ctx)?`
65 /// (which chains into `validate(ctx)?`) regardless of policy. A
66 /// handler typed as `&mut Context<'_>` always receives the
67 /// context raw. `strict = true` is the documentation contract
68 /// that every handler in the module opts into the typed form;
69 /// `strict = false` signals the author intends to use raw
70 /// contexts and accepts the responsibility of calling
71 /// `validate()` manually where needed.
72 ///
73 /// The flag is read back by callers at compile time
74 /// (`HOPPER_PROGRAM_POLICY.strict`) to specialize code paths that
75 /// depend on whether the enforcement envelope is active.
76 pub strict: bool,
77
78 /// Author-maintained token-check intent. The macro records this flag
79 /// without inserting or removing checks from CPI calls. Explicit strict
80 /// methods on TransferChecked, BurnChecked, and ApproveChecked check the
81 /// token owner field; their direct variants also require signer privilege.
82 /// Signed strict variants accept PDA seeds, whose signing authority is
83 /// validated during CPI rather than requiring an incoming signer flag.
84 pub enforce_token_checks: bool,
85
86 /// Permit `unsafe { ... }` blocks inside handler bodies. When
87 /// false the program macro wraps each handler in
88 /// `#[deny(unsafe_code)]`. The lint covers that handler item, not called
89 /// helpers or dependencies, and ordinary Rust lint override rules apply.
90 pub allow_unsafe: bool,
91}
92
93/// Program-size/audit profile emitted by `#[hopper::program(profile = "...")]`.
94#[derive(Copy, Clone, Debug, PartialEq, Eq)]
95#[repr(u8)]
96pub enum HopperProgramProfile {
97 Tiny = 0,
98 Strict = 1,
99 Audit = 2,
100 Raw = 3,
101}
102
103impl HopperProgramProfile {
104 pub const TINY: Self = Self::Tiny;
105 pub const STRICT: Self = Self::Strict;
106 pub const AUDIT: Self = Self::Audit;
107 pub const RAW: Self = Self::Raw;
108}
109
110impl HopperProgramPolicy {
111 /// Typed-validation and token-check intent enabled; unsafe code permitted.
112 /// The shipping default. Handler types and helper calls determine checks.
113 pub const STRICT: Self = Self {
114 strict: true,
115 enforce_token_checks: true,
116 allow_unsafe: true,
117 };
118
119 /// STRICT intent plus a default unsafe-code denial on handler items.
120 /// This does not audit unsafe implementations in called helpers.
121 pub const SEALED: Self = Self {
122 strict: true,
123 enforce_token_checks: true,
124 allow_unsafe: false,
125 };
126
127 /// Typed-validation and token-check intent disabled; unsafe code permitted.
128 /// Typed handlers still bind. Raw handlers own their explicit validation.
129 pub const RAW: Self = Self {
130 strict: false,
131 enforce_token_checks: false,
132 allow_unsafe: true,
133 };
134
135 /// The shipping default, identical to [`HopperProgramPolicy::STRICT`].
136 ///
137 /// Exposed as a `const fn` so downstream macro expansion can
138 /// reach it from `const` context without an intermediate binding.
139 #[inline(always)]
140 pub const fn default_policy() -> Self {
141 Self::STRICT
142 }
143}
144
145impl Default for HopperProgramPolicy {
146 fn default() -> Self {
147 Self::default_policy()
148 }
149}
150
151/// Per-instruction policy override.
152///
153/// The `#[instruction(N, unsafe_memory, skip_token_checks, allow_arbitrary_cpi, ctx_args = K)]`
154/// attribute emits `pub const <HANDLER>_POLICY: HopperInstructionPolicy = ...;`
155/// alongside the handler. All fields default to the inherit-from-program
156/// behaviour (`false` / `0`) so handlers without overrides get the program
157/// policy unchanged.
158#[derive(Copy, Clone, Debug, PartialEq, Eq)]
159pub struct HopperInstructionPolicy {
160 /// Opt this handler out of `#[deny(unsafe_code)]` even when the
161 /// program-level `allow_unsafe` is false. Used for the one or two
162 /// "fast path" handlers in an otherwise-sealed program.
163 pub unsafe_memory: bool,
164
165 /// Declare an exception to the program-level token-check intent.
166 /// This does not remove checks from helper calls; authors document how
167 /// the handler upholds its token invariants.
168 pub skip_token_checks: bool,
169
170 /// Marks a handler as intentionally able to invoke arbitrary external
171 /// programs, for governance/proposal executors and plugin dispatchers.
172 /// Hopper does not forbid this path; the flag makes the capability visible
173 /// to generated schema, review tools, and audit-oriented explain output.
174 pub allow_arbitrary_cpi: bool,
175
176 /// Count of leading instruction args the dispatcher threads to the
177 /// typed context's `bind_with_args(...)`. `0` means the context
178 /// (if any) is bound via `bind(ctx)?` and no args participate in
179 /// constraint evaluation. which is the legacy shape and matches
180 /// Anchor's non-`#[instruction]` accounts struct. When a context
181 /// was declared with `#[instruction(name: Type, ...)]`, the handler
182 /// must set `ctx_args` equal to the number of declared args. Generated
183 /// code also pins identical names and order, so every seed, constraint,
184 /// and exact-cell selector resolves to the same wire value off chain and
185 /// on chain.
186 pub ctx_args: u8,
187}
188
189impl HopperInstructionPolicy {
190 /// Inherit every lever from the program-level policy.
191 pub const INHERIT: Self = Self {
192 unsafe_memory: false,
193 skip_token_checks: false,
194 allow_arbitrary_cpi: false,
195 ctx_args: 0,
196 };
197}
198
199impl Default for HopperInstructionPolicy {
200 fn default() -> Self {
201 Self::INHERIT
202 }
203}
204
205#[cfg(test)]
206// These tests assert the field values of `const` policy profiles; the constant
207// value of each assertion is precisely the invariant under test.
208#[allow(clippy::assertions_on_constants)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn named_modes_differ_on_every_lever() {
214 assert!(HopperProgramPolicy::STRICT.strict);
215 assert!(HopperProgramPolicy::STRICT.enforce_token_checks);
216 assert!(HopperProgramPolicy::STRICT.allow_unsafe);
217
218 assert!(HopperProgramPolicy::SEALED.strict);
219 assert!(HopperProgramPolicy::SEALED.enforce_token_checks);
220 assert!(!HopperProgramPolicy::SEALED.allow_unsafe);
221
222 assert!(!HopperProgramPolicy::RAW.strict);
223 assert!(!HopperProgramPolicy::RAW.enforce_token_checks);
224 assert!(HopperProgramPolicy::RAW.allow_unsafe);
225 }
226
227 #[test]
228 fn program_profiles_are_stable() {
229 assert_eq!(HopperProgramProfile::TINY as u8, 0);
230 assert_eq!(HopperProgramProfile::STRICT as u8, 1);
231 assert_eq!(HopperProgramProfile::AUDIT as u8, 2);
232 assert_eq!(HopperProgramProfile::RAW as u8, 3);
233 }
234
235 #[test]
236 fn default_policy_is_strict() {
237 assert_eq!(HopperProgramPolicy::default(), HopperProgramPolicy::STRICT);
238 assert_eq!(
239 HopperProgramPolicy::default_policy(),
240 HopperProgramPolicy::STRICT
241 );
242 }
243
244 #[test]
245 fn instruction_inherit_zeroes_every_lever() {
246 assert!(!HopperInstructionPolicy::INHERIT.unsafe_memory);
247 assert!(!HopperInstructionPolicy::INHERIT.skip_token_checks);
248 assert!(!HopperInstructionPolicy::INHERIT.allow_arbitrary_cpi);
249 assert_eq!(HopperInstructionPolicy::INHERIT.ctx_args, 0);
250 assert_eq!(
251 HopperInstructionPolicy::default(),
252 HopperInstructionPolicy::INHERIT
253 );
254 }
255
256 #[test]
257 fn instruction_ctx_args_round_trips() {
258 let p = HopperInstructionPolicy {
259 unsafe_memory: false,
260 skip_token_checks: false,
261 allow_arbitrary_cpi: false,
262 ctx_args: 3,
263 };
264 assert_eq!(p.ctx_args, 3);
265 assert_ne!(p, HopperInstructionPolicy::INHERIT);
266 }
267}