stellar_macros/lib.rs
1mod access_control;
2mod default_impl_macro;
3mod helpers;
4mod pausable;
5mod upgradeable;
6
7use access_control::{generate_any_role_check, generate_role_check};
8use default_impl_macro::generate_default_impl;
9use helpers::*;
10use pausable::generate_pause_check;
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::{parse_macro_input, DeriveInput, ItemFn};
14use upgradeable::*;
15
16/* DEFAULT_IMPL_MACRO */
17
18/// Generates the missing default implementations for the traits provided by
19/// OpenZeppelin Stellar library.
20///
21/// `#[contractimpl]` macro requires all the default implementations to be
22/// provided under the code block:
23///
24/// ```ignore
25/// #[contractimpl]
26/// impl Trait for MyContract {
27/// /*
28/// The client generated by the `#[contractimpl]` macro will have access only
29/// to the methods specified in here. Which means, if you do not provide the
30/// default implementations of the methods, the client generated by the
31/// `#[contractimpl]` macro won't have access to those methods.
32///
33/// This is due to how macros work in Rust. They cannot access the default
34/// implementations of the methods of this trait, since they are not in the
35/// scope of the macro.
36///
37/// To overcome this, we provide a macro for our traits, that generates the
38/// missing default implementations for the methods of the trait, so
39/// you can only focus on the overrides, and leave the default implementations
40/// out as per usual.
41/// */
42/// }
43/// ```
44///
45/// # Example:
46///
47/// ```ignore
48/// #[default_impl] // IMPORTANT: place this above `#[contractimpl]`
49/// #[contractimpl]
50/// impl NonFungibleToken for MyContract {
51/// /* your overrides here */
52/// }
53/// ```
54///
55/// This macro works for the following traits:
56/// - `FungibleToken`
57/// - `FungibleBurnable`
58/// - `NonFungibleToken`
59/// - `NonFungibleBurnable`
60/// - `NonFungibleEnumerable`
61/// - `AccessControl`
62/// - `Ownable`
63///
64/// # Notes
65///
66/// This macro does not support the below traits on purpose:
67/// - `FungibleAllowList`
68/// - `FungibleBlockList`
69/// - `NonFungibleRoyalties`
70///
71/// Because, there are no default implementation to enforce how the
72/// authorization should be configured. Not providing a default implementation
73/// for these traits is a reminder for the implementor to provide the
74/// authorization logic for these traits.
75#[proc_macro_attribute]
76pub fn default_impl(attrs: TokenStream, item: TokenStream) -> TokenStream {
77 assert!(attrs.is_empty(), "This macro does not accept any arguments");
78
79 generate_default_impl(item)
80}
81
82/* ACCESS CONTROL MACROS */
83
84/// A procedural macro that retrieves the admin from storage and requires
85/// authorization from the admin before executing the function body.
86///
87/// # Usage
88///
89/// ```rust
90/// #[only_admin]
91/// pub fn restricted_function(e: &Env, other_param: u32) {
92/// // Function body
93/// }
94/// ```
95///
96/// This will expand to:
97///
98/// ```rust
99/// pub fn restricted_function(e: &Env, other_param: u32) {
100/// stellar_access::access_control::enforce_admin_auth(e);
101/// // Function body
102/// }
103/// ```
104#[proc_macro_attribute]
105pub fn only_admin(attrs: TokenStream, input: TokenStream) -> TokenStream {
106 assert!(attrs.is_empty(), "This macro does not accept any arguments");
107
108 let input_fn = parse_macro_input!(input as ItemFn);
109
110 // Generate the function with the admin authorization check
111 let auth_check_path = quote! { stellar_access::access_control::enforce_admin_auth };
112 let expanded = generate_auth_check(&input_fn, auth_check_path);
113
114 TokenStream::from(expanded)
115}
116
117/// A procedural macro that ensures the parameter has the specified role.
118///
119/// # Security Warning
120///
121/// **IMPORTANT**: This macro checks role membership but does NOT enforce
122/// authorization. This design prevents duplicate `require_auth()` calls which
123/// would cause panics in Stellar contracts. Use this macro when:
124///
125/// 1. Your function already contains a `require_auth()` call
126/// 2. You need additional role-based access control
127///
128/// If you need both role checking AND authorization, use `#[only_role]`
129/// instead.
130///
131/// # Usage
132///
133/// ```rust
134/// #[has_role(account, "minter")]
135/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
136/// // Function body
137/// }
138/// ```
139///
140/// This will expand to:
141///
142/// ```rust
143/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
144/// stellar_access::access_control::ensure_role(
145/// e,
146/// &account,
147/// &soroban_sdk::Symbol::new(e, "minter"),
148/// );
149/// // Function body
150/// }
151/// ```
152#[proc_macro_attribute]
153pub fn has_role(args: TokenStream, input: TokenStream) -> TokenStream {
154 generate_role_check(args, input, false)
155}
156
157/// A procedural macro that ensures the parameter has the specified role and
158/// requires authorization.
159///
160/// **IMPORTANT**: This macro both checks role membership AND enforces
161/// authorization. Be aware that in Stellar contracts, duplicate
162/// `require_auth()` calls for the same account will cause panics. If your
163/// function already contains a `require_auth()` call for the same account, use
164/// `#[has_role]` instead to avoid duplicate authorization checks.
165///
166/// # Usage
167///
168/// ```rust
169/// #[only_role(account, "minter")]
170/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
171/// // Function body
172/// }
173/// ```
174///
175/// This will expand to:
176///
177/// ```rust
178/// pub fn mint_tokens(e: &Env, amount: u32, account: Address) {
179/// stellar_access::access_control::ensure_role(
180/// e,
181/// &account,
182/// &soroban_sdk::Symbol::new(e, "minter"),
183/// );
184/// account.require_auth();
185/// // Function body
186/// }
187/// ```
188#[proc_macro_attribute]
189pub fn only_role(args: TokenStream, input: TokenStream) -> TokenStream {
190 generate_role_check(args, input, true)
191}
192
193/// A procedural macro that ensures the parameter has any of the specified
194/// roles.
195///
196/// # Security Warning
197///
198/// **IMPORTANT**: This macro checks role membership but does NOT enforce
199/// authorization. This design prevents duplicate `require_auth()` calls which
200/// would cause panics in Stellar contracts. Use this macro when:
201///
202/// 1. Your function already contains a `require_auth()` call
203/// 2. You need additional role-based access control
204///
205/// If you need both role checking AND authorization, use `#[only_any_role]`
206/// instead.
207///
208/// # Usage
209///
210/// ```rust
211/// #[has_any_role(account, ["minter", "admin", "operator"])]
212/// pub fn manage_tokens(e: &Env, amount: u32, account: Address) {
213/// // Function body
214/// }
215/// ```
216///
217/// This will expand to code that checks if the account has any of the specified
218/// roles.
219#[proc_macro_attribute]
220pub fn has_any_role(args: TokenStream, input: TokenStream) -> TokenStream {
221 generate_any_role_check(args, input, false)
222}
223
224/// A procedural macro that ensures the parameter has any of the specified roles
225/// and requires authorization.
226///
227/// **IMPORTANT**: This macro both checks role membership AND enforces
228/// authorization. Be aware that in Stellar contracts, duplicate
229/// `require_auth()` calls for the same account will cause panics. If your
230/// function already contains a `require_auth()` call for the same account, use
231/// `#[has_any_role]` instead to avoid duplicate authorization checks.
232///
233/// # Usage
234///
235/// ```rust
236/// #[only_any_role(account, ["minter", "admin", "operator"])]
237/// pub fn manage_tokens(e: &Env, amount: u32, account: Address) {
238/// // Function body
239/// }
240/// ```
241///
242/// This will expand to code that checks if the account has any of the specified
243/// roles and requires authorization from the account.
244#[proc_macro_attribute]
245pub fn only_any_role(args: TokenStream, input: TokenStream) -> TokenStream {
246 generate_any_role_check(args, input, true)
247}
248
249/// A procedural macro that retrieves the owner from storage and requires
250/// authorization from the owner before executing the function body.
251///
252/// # Usage
253///
254/// ```rust
255/// #[only_owner]
256/// pub fn restricted_function(e: &Env, other_param: u32) {
257/// // Function body
258/// }
259/// ```
260///
261/// This will expand to:
262///
263/// ```rust
264/// pub fn restricted_function(e: &Env, other_param: u32) {
265/// let owner: soroban_sdk::Address =
266/// e.storage().instance().get(&stellar_access::ownable::OwnableStorageKey::Owner).unwrap();
267/// owner.require_auth();
268/// // Function body
269/// }
270/// ```
271#[proc_macro_attribute]
272pub fn only_owner(attrs: TokenStream, input: TokenStream) -> TokenStream {
273 assert!(attrs.is_empty(), "This macro does not accept any arguments");
274
275 let input_fn = parse_macro_input!(input as ItemFn);
276
277 // Generate the function with the owner authorization check
278 let auth_check_path = quote! { stellar_access::ownable::enforce_owner_auth };
279 let expanded = generate_auth_check(&input_fn, auth_check_path);
280
281 TokenStream::from(expanded)
282}
283
284/// Adds a pause check at the beginning of the function that ensures the
285/// contract is not paused.
286///
287/// This macro will inject a `when_not_paused` check at the start of the
288/// function body. If the contract is paused, the function will return early
289/// with a panic.
290///
291/// # Requirement:
292///
293/// - The first argument of the decorated function must be of type `Env` or
294/// `&Env`
295///
296/// # Example:
297///
298/// ```ignore
299/// #[when_not_paused]
300/// pub fn my_function(env: &Env) {
301/// // This code will only execute if the contract is not paused
302/// }
303/// ```
304#[proc_macro_attribute]
305pub fn when_not_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
306 assert!(attrs.is_empty(), "This macro does not accept any arguments");
307
308 generate_pause_check(item, "when_not_paused")
309}
310
311/* PAUSABLE MACROS */
312
313/// Adds a pause check at the beginning of the function that ensures the
314/// contract is paused.
315///
316/// This macro will inject a `when_paused` check at the start of the function
317/// body. If the contract is not paused, the function will return early with a
318/// panic.
319///
320/// # Requirement:
321///
322/// - The first argument of the decorated function must be of type `Env` or
323/// `&Env`
324///
325/// # Example:
326///
327/// ```ignore
328/// #[when_paused]
329/// pub fn my_function(env: &Env) {
330/// // This code will only execute if the contract is paused
331/// }
332/// ```
333#[proc_macro_attribute]
334pub fn when_paused(attrs: TokenStream, item: TokenStream) -> TokenStream {
335 assert!(attrs.is_empty(), "This macro does not accept any arguments");
336
337 generate_pause_check(item, "when_paused")
338}
339
340/* UPGRADEABLE MACROS */
341
342/// 1. Derives `Upgradeable` a) implements the interface; requires only the auth
343/// to be defined b) sets wasm version by taking the version from Cargo.toml
344///
345/// 2. Derives `UpgradeableMigratable` when both an upgrade and a migration are
346/// needed a) implements the interface; requires the auth and the migration
347/// logic to be defined b) sets wasm version by taking the version from
348/// Cargo.toml
349///
350/// Example for upgrade only:
351/// ```rust,ignore
352/// #[derive(Upgradeable)]
353/// #[contract]
354/// pub struct ExampleContract;
355///
356/// impl UpgradeableInternal for ExampleContract {
357/// fn _require_auth(e: &Env, operator: &Address) {
358/// operator.require_auth();
359/// let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
360/// if *operator != owner {
361/// panic_with_error!(e, ExampleContractError::Unauthorized)
362/// }
363/// }
364/// }
365/// ```
366///
367/// Example for upgrade and migration:
368/// ```rust,ignore
369/// #[contracttype]
370/// pub struct Data {
371/// pub num1: u32,
372/// pub num2: u32,
373/// }
374///
375/// #[derive(UpgradeableMigratable)]
376/// #[contract]
377/// pub struct ExampleContract;
378///
379///
380/// impl UpgradeableMigratableInternal for ExampleContract {
381/// type MigrationData = Data;
382///
383/// fn _require_auth(e: &Env, operator: &Address) {
384/// operator.require_auth();
385/// let owner = e.storage().instance().get::<_, Address>(&OWNER).unwrap();
386/// if *operator != owner {
387/// panic_with_error!(e, ExampleContractError::Unauthorized)
388/// }
389/// }
390///
391/// fn _migrate(e: &Env, data: &Self::MigrationData) {
392/// e.storage().instance().set(&DATA_KEY, data);
393/// }
394/// }
395/// ```
396#[proc_macro_derive(Upgradeable)]
397pub fn upgradeable_derive(input: TokenStream) -> TokenStream {
398 let input = parse_macro_input!(input as DeriveInput);
399
400 derive_upgradeable(&input).into()
401}
402
403#[proc_macro_derive(UpgradeableMigratable)]
404pub fn upgradeable_migratable_derive(input: TokenStream) -> TokenStream {
405 let input = parse_macro_input!(input as DeriveInput);
406
407 derive_upgradeable_migratable(&input).into()
408}