Skip to main content

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}