Skip to content

Commit

Permalink
Pausable macros (#20)
Browse files Browse the repository at this point in the history
* pausable macros

* stellar sdk update for macro fixes
  • Loading branch information
ozgunozerk authored Jan 29, 2025
1 parent cefb6e9 commit 481c97a
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 90 deletions.
174 changes: 97 additions & 77 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
[workspace]
resolver = "2"
members = [
"contracts/utils/*",
"contracts/token/*",
"examples/*",
]
members = ["contracts/utils/*", "contracts/token/*", "examples/*"]

[workspace.package]
authors = ["OpenZeppelin"]
Expand All @@ -14,10 +10,14 @@ repository = "https://github.com/OpenZeppelin/soroban-contracts"
version = "0.0.0"

[workspace.dependencies]
soroban-sdk = "22.0.1"
soroban-sdk = "22.0.6"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

# members
openzeppelin-pausable = { path = "contracts/utils/pausable" }
openzeppelin-pausable-macros = { path = "contracts/utils/pausable-macros" }

[profile.release]
opt-level = "z"
Expand Down
13 changes: 13 additions & 0 deletions contracts/utils/pausable-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "openzeppelin-pausable-macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true
doctest = false

[dependencies]
proc-macro2 = { workspace = true }
quote = { workspace = true }
syn = { workspace = true }
47 changes: 47 additions & 0 deletions contracts/utils/pausable-macros/src/helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use syn::{FnArg, ItemFn, PatType, Type};

pub fn check_env_arg(input_fn: &ItemFn) -> (syn::Ident, bool) {
// Get the first argument
let first_arg = input_fn.sig.inputs.first().unwrap_or_else(|| {
panic!("function '{}' must have at least one argument", input_fn.sig.ident)
});

// Extract the pattern and type from the argument
let (pat, ty) = match first_arg {
FnArg::Typed(PatType { pat, ty, .. }) => (pat, ty),
_ =>
panic!("first argument of function '{}' must be a typed parameter", input_fn.sig.ident),
};

// Get the identifier from the pattern
let ident = match &**pat {
syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(),
_ => panic!("first argument of function '{}' must be an identifier", input_fn.sig.ident),
};

// Check if the type is Env or &Env
let is_ref = match &**ty {
Type::Reference(type_ref) => match &*type_ref.elem {
Type::Path(path) => {
check_is_env(path, &input_fn.sig.ident);
true
}
_ => panic!("first argument of function '{}' must be Env or &Env", input_fn.sig.ident),
},
Type::Path(path) => {
check_is_env(path, &input_fn.sig.ident);
false
}
_ => panic!("first argument of function '{}' must be Env or &Env", input_fn.sig.ident),
};

(ident, is_ref)
}

fn check_is_env(path: &syn::TypePath, fn_name: &syn::Ident) {
let is_env = path.path.segments.last().map(|seg| seg.ident == "Env").unwrap_or(false);

if !is_env {
panic!("first argument of function '{}' must be Env or &Env", fn_name);
}
}
99 changes: 99 additions & 0 deletions contracts/utils/pausable-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

use crate::helper::check_env_arg;

mod helper;

/// Adds a pause check at the beginning of the function that ensures the
/// contract is not paused.
///
/// This macro will inject a `when_not_paused` check at the start of the
/// function body. If the contract is paused, the function will return early
/// with a panic.
///
/// # Requirement:
///
/// - The first argument of the decorated function must be of type `Env` or
/// `&Env`
///
/// # Example:
///
/// ```ignore
/// #[when_not_paused]
/// pub fn my_function(env: &Env) {
/// // This code will only execute if the contract is not paused
/// }
/// ```
#[proc_macro_attribute]
pub fn when_not_paused(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let (env_ident, is_ref) = check_env_arg(&input_fn);

let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_block = &input_fn.block;

let env_arg = if is_ref {
quote! { #env_ident }
} else {
quote! { &#env_ident }
};

let output = quote! {
#fn_vis #fn_sig {
openzeppelin_pausable::when_not_paused(#env_arg);

#fn_block
}
};

output.into()
}

/// Adds a pause check at the beginning of the function that ensures the
/// contract is paused.
///
/// This macro will inject a `when_paused` check at the start of the function
/// body. If the contract is not paused, the function will return early with a
/// panic.
///
/// # Requirement:
///
/// - The first argument of the decorated function must be of type `Env` or
/// `&Env`
///
/// # Example:
///
/// ```ignore
/// #[when_paused]
/// pub fn my_function(env: &Env) {
/// // This code will only execute if the contract is paused
/// }
/// ```
#[proc_macro_attribute]
pub fn when_paused(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let (env_ident, is_ref) = check_env_arg(&input_fn);

let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
let fn_block = &input_fn.block;

let env_arg = if is_ref {
quote! { #env_ident }
} else {
quote! { &#env_ident }
};

let output = quote! {
#fn_vis #fn_sig {
openzeppelin_pausable::when_paused(#env_arg);

#fn_block
}
};

output.into()
}
5 changes: 2 additions & 3 deletions contracts/utils/pausable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
//! multiple extensions or utilities for your contract, the code will be better
//! organized.
//!
//! We also provide two macros `when_paused` and `when_not_paused` (will be
//! implemented later). These macros act as guards for your functions. For
//! example:
//! We also provide two macros `when_paused` and `when_not_paused`.
//! These macros act as guards for your functions. For example:
//!
//! ```ignore
//! #[when_not_paused]
Expand Down
1 change: 1 addition & 0 deletions contracts/utils/pausable/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![cfg(test)]

extern crate std;

use soroban_sdk::{
Expand Down
1 change: 1 addition & 0 deletions examples/pausable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ doctest = false
[dependencies]
soroban-sdk = { workspace = true }
openzeppelin-pausable = { workspace = true }
openzeppelin-pausable-macros = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
7 changes: 3 additions & 4 deletions examples/pausable/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! `paused`.
use openzeppelin_pausable::{self as pausable, Pausable};
use openzeppelin_pausable_macros::{when_not_paused, when_paused};
use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, panic_with_error, Address, Env,
};
Expand All @@ -34,9 +35,8 @@ impl ExampleContract {
e.storage().instance().set(&DataKey::Counter, &0);
}

#[when_not_paused]
pub fn increment(e: &Env) -> i32 {
pausable::when_not_paused(e);

let mut counter: i32 =
e.storage().instance().get(&DataKey::Counter).expect("counter should be set");

Expand All @@ -47,9 +47,8 @@ impl ExampleContract {
counter
}

#[when_paused]
pub fn emergency_reset(e: &Env) {
pausable::when_paused(e);

e.storage().instance().set(&DataKey::Counter, &0);
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/pausable/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![cfg(test)]

extern crate std;

use soroban_sdk::{testutils::Address as _, Address, Env};
Expand Down

0 comments on commit 481c97a

Please sign in to comment.