stellar_macros/
lib.rs

1mod access_control;
2mod helpers;
3mod pausable;
4mod upgradeable;
5
6use access_control::{generate_any_role_check, generate_role_check};
7use helpers::*;
8use pausable::generate_pause_check;
9use proc_macro::TokenStream;
10use quote::quote;
11use syn::{parse_macro_input, DeriveInput, ItemFn};
12use upgradeable::*;
13
14/* ACCESS CONTROL MACROS */
15
16/// A procedural macro that retrieves the admin from storage and requires
17/// authorization from the admin before executing the function body.
18///
19/// # Usage
20///
21/// ```rust
22/// #[only_admin]
23/// pub fn restricted_function(e: &Env, other_param: u32) {
24///     // Function body
25/// }
26/// ```
27///
28/// This will expand to:
29///
30/// ```rust
31/// pub fn restricted_function(e: &Env, other_param: u32) {
32///     stellar_access::access_control::enforce_admin_auth(e);
33///     // Function body
34/// }
35/// ```
36#[proc_macro_attribute]
37pub fn only_admin(attrs: TokenStream, input: TokenStream) -> TokenStream {
38    assert!(attrs.is_empty(), "This macro does not accept any arguments");
39
40    let input_fn = parse_macro_input!(input as ItemFn);
41
42    // Generate the function with the admin authorization check
43    let auth_check_path = quote! { stellar_access::access_control::enforce_admin_auth };
44    let expanded = generate_auth_check(&input_fn, auth_check_path);
45
46    TokenStream::from(expanded)
47}
48
49/// A procedural macro that ensures the parameter has the specified role.
50///
51/// # Security Warning
52///
53/// **IMPORTANT**: This macro checks role membership but does NOT enforce
54/// authorization. This design prevents duplicate `require_auth()` calls which
55/// would cause panics in Stellar contracts. Use this macro when:
56///
57/// 1. Your function already contains a `require_auth()` call
58/// 2. You need additional role-based access control
59///
60/// If you need both role checking AND authorization, use `#[only_role]`
61/// instead.
62///
63/// # Usage
64///
65/// ```rust
66/// #[has_role(account, "minter")]
67/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
68///     // Function body
69/// }
70/// ```
71///
72/// This will expand to:
73///
74/// ```rust
75/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
76///     stellar_access::access_control::ensure_role(
77///         e,
78///         &account,
79///         &soroban_sdk::Symbol::new(e, "minter"),
80///     );
81///     // Function body
82/// }
83/// ```
84#[proc_macro_attribute]
85pub fn has_role(args: TokenStream, input: TokenStream) -> TokenStream {
86    generate_role_check(args, input, false)
87}
88
89/// A procedural macro that ensures the parameter has the specified role and
90/// requires authorization.
91///
92/// **IMPORTANT**: This macro both checks role membership AND enforces
93/// authorization. Be aware that in Stellar contracts, duplicate
94/// `require_auth()` calls for the same account will cause panics. If your
95/// function already contains a `require_auth()` call for the same account, use
96/// `#[has_role]` instead to avoid duplicate authorization checks.
97///
98/// # Usage
99///
100/// ```rust
101/// #[only_role(account, "minter")]
102/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
103///     // Function body
104/// }
105/// ```
106///
107/// This will expand to:
108///
109/// ```rust
110/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
111///     stellar_access::access_control::ensure_role(
112///         e,
113///         &account,
114///         &soroban_sdk::Symbol::new(e, "minter"),
115///     );
116///     account.require_auth();
117///     // Function body
118/// }
119/// ```
120#[proc_macro_attribute]
121pub fn only_role(args: TokenStream, input: TokenStream) -> TokenStream {
122    generate_role_check(args, input, true)
123}
124
125/// A procedural macro that ensures the parameter has any of the specified
126/// roles.
127///
128/// # Security Warning
129///
130/// **IMPORTANT**: This macro checks role membership but does NOT enforce
131/// authorization. This design prevents duplicate `require_auth()` calls which
132/// would cause panics in Stellar contracts. Use this macro when:
133///
134/// 1. Your function already contains a `require_auth()` call
135/// 2. You need additional role-based access control
136///
137/// If you need both role checking AND authorization, use `#[only_any_role]`
138/// instead.
139///
140/// # Usage
141///
142/// ```rust
143/// #[has_any_role(account, ["minter", "admin", "operator"])]
144/// pub fn manage_tokens(e: &Env, amount: u32, account: Address) {
145///     // Function body
146/// }
147/// ```
148///
149/// This will expand to code that checks if the account has any of the specified
150/// roles.
151#[proc_macro_attribute]
152pub fn has_any_role(args: TokenStream, input: TokenStream) -> TokenStream {
153    generate_any_role_check(args, input, false)
154}
155
156/// A procedural macro that ensures the parameter has any of the specified roles
157/// and requires authorization.
158///
159/// **IMPORTANT**: This macro both checks role membership AND enforces
160/// authorization. Be aware that in Stellar contracts, duplicate
161/// `require_auth()` calls for the same account will cause panics. If your
162/// function already contains a `require_auth()` call for the same account, use
163/// `#[has_any_role]` instead to avoid duplicate authorization checks.
164///
165/// # Usage
166///
167/// ```rust
168/// #[only_any_role(account, ["minter", "admin", "operator"])]
169/// pub fn manage_tokens(e: &Env, amount: u32, account: Address) {
170///     // Function body
171/// }
172/// ```
173///
174/// This will expand to code that checks if the account has any of the specified
175/// roles and requires authorization from the account.
176#[proc_macro_attribute]
177pub fn only_any_role(args: TokenStream, input: TokenStream) -> TokenStream {
178    generate_any_role_check(args, input, true)
179}
180
181/// A procedural macro that retrieves the owner from storage and requires
182/// authorization from the owner before executing the function body.
183///
184/// # Usage
185///
186/// ```rust
187/// #[only_owner]
188/// pub fn restricted_function(e: &Env, other_param: u32) {
189///     // Function body
190/// }
191/// ```
192///
193/// This will expand to:
194///
195/// ```rust
196/// pub fn restricted_function(e: &Env, other_param: u32) {
197///     let owner: soroban_sdk::Address =
198///         e.storage().instance().get(&stellar_access::ownable::OwnableStorageKey::Owner).unwrap();
199///     owner.require_auth();
200///     // Function body
201/// }
202/// ```
203#[proc_macro_attribute]
204pub fn only_owner(attrs: TokenStream, input: TokenStream) -> TokenStream {
205    assert!(attrs.is_empty(), "This macro does not accept any arguments");
206
207    let input_fn = parse_macro_input!(input as ItemFn);
208
209    // Generate the function with the owner authorization check
210    let auth_check_path = quote! { stellar_access::ownable::enforce_owner_auth };
211    let expanded = generate_auth_check(&input_fn, auth_check_path);
212
213    TokenStream::from(expanded)
214}
215
216/// Adds a pause check at the beginning of the function that ensures the
217/// contract is not paused.
218///
219/// This macro will inject a `when_not_paused` check at the start of the
220/// function body. If the contract is paused, the function will return early
221/// with a panic.
222///
223/// # Requirement:
224///
225/// - The first argument of the decorated function must be of type `Env` or
226///   `&Env`
227///
228/// # Example:
229///
230/// ```ignore
231/// #[when_not_paused]
232/// pub fn my_function(e: &Env) {
233///     // This code will only execute if the contract is not paused
234/// }
235/// ```
236#[proc_macro_attribute]
237pub fn when_not_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
238    assert!(attrs.is_empty(), "This macro does not accept any arguments");
239
240    generate_pause_check(item, "when_not_paused")
241}
242
243/* PAUSABLE MACROS */
244
245/// Adds a pause check at the beginning of the function that ensures the
246/// contract is paused.
247///
248/// This macro will inject a `when_paused` check at the start of the function
249/// body. If the contract is not paused, the function will return early with a
250/// panic.
251///
252/// # Requirement:
253///
254/// - The first argument of the decorated function must be of type `Env` or
255///   `&Env`
256///
257/// # Example:
258///
259/// ```ignore
260/// #[when_paused]
261/// pub fn my_function(e: &Env) {
262///     // This code will only execute if the contract is paused
263/// }
264/// ```
265#[proc_macro_attribute]
266pub fn when_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
267    assert!(attrs.is_empty(), "This macro does not accept any arguments");
268
269    generate_pause_check(item, "when_paused")
270}
271
272/* UPGRADEABLE MACROS */
273
274/// 1. Derives `Upgradeable` a) implements the interface; requires only the auth
275///    to be defined b) sets wasm version by taking the version from Cargo.toml
276///
277/// 2. Derives `UpgradeableMigratable` when both an upgrade and a migration are
278///    needed a) implements the interface; requires the auth and the migration
279///    logic to be defined b) sets wasm version by taking the version from
280///    Cargo.toml
281///
282/// Example for upgrade only:
283/// ```rust,ignore
284/// #[derive(Upgradeable)]
285/// #[contract]
286/// pub struct ExampleContract;
287///
288/// impl UpgradeableInternal for ExampleContract {
289///     fn _require_auth(e: &Env, operator: &Address) {
290///         operator.require_auth();
291///         let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
292///         if *operator != owner {
293///             panic_with_error!(e, ExampleContractError::Unauthorized)
294///         }
295///     }
296/// }
297/// ```
298///
299/// Example for upgrade and migration:
300/// ```rust,ignore
301/// #[contracttype]
302/// pub struct Data {
303///     pub num1: u32,
304///     pub num2: u32,
305/// }
306///
307/// #[derive(UpgradeableMigratable)]
308/// #[contract]
309/// pub struct ExampleContract;
310///
311///
312/// impl UpgradeableMigratableInternal for ExampleContract {
313///     type MigrationData = Data;
314///
315///     fn _require_auth(e: &Env, operator: &Address) {
316///         operator.require_auth();
317///         let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
318///         if *operator != owner {
319///             panic_with_error!(e, ExampleContractError::Unauthorized)
320///         }
321///     }
322///
323///     fn _migrate(e: &Env, data: &Self::MigrationData) {
324///         e.storage().instance().set(&DATA_KEY, data);
325///     }
326/// }
327/// ```
328#[proc_macro_derive(Upgradeable)]
329pub fn upgradeable_derive(input: TokenStream) -> TokenStream {
330    let input = parse_macro_input!(input as DeriveInput);
331
332    derive_upgradeable(&input).into()
333}
334
335#[proc_macro_derive(UpgradeableMigratable)]
336pub fn upgradeable_migratable_derive(input: TokenStream) -> TokenStream {
337    let input = parse_macro_input!(input as DeriveInput);
338
339    derive_upgradeable_migratable(&input).into()
340}