Skip to content

Commit

Permalink
feat(pausable): add Pausable module and example contract
Browse files Browse the repository at this point in the history
* feat(pausable): add contract module

* fix(pausable): lib exports

* feat(pausable): add pausable example

* fix(pausable): rename unit test and fmt

* refactor(pausable): combine clients.rs and errors.rs

* fix(pausable): fmt

* fix(pausable): missing no_std in examples

* refactor(pausable): move events

* fmt
  • Loading branch information
brozorec authored Jan 10, 2025
1 parent b09cb72 commit 0075f68
Show file tree
Hide file tree
Showing 34 changed files with 5,170 additions and 0 deletions.
1,626 changes: 1,626 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

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

[workspace.package]
authors = ["OpenZeppelin"]
edition = "2021"
license = "MIT"
repository = "https://github.com/OpenZeppelin/soroban-contracts"
version = "0.0.0"

[workspace.dependencies]
soroban-sdk = "22.0.1"

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

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

# For more information about this profile see https://soroban.stellar.org/docs/basic-tutorials/logging#cargotoml-profile
[profile.release-with-logs]
inherits = "release"
debug-assertions = true
17 changes: 17 additions & 0 deletions contracts/utils/pausable/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "openzeppelin-pausable"
edition.workspace = true
license.workspace = true
repository.workspace = true
publish = false
version.workspace = true

[lib]
crate-type = ["lib", "cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
16 changes: 16 additions & 0 deletions contracts/utils/pausable/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
default: build

all: test

test: build
cargo test

build:
stellar contract build
@ls -l target/wasm32-unknown-unknown/release/*.wasm

fmt:
cargo fmt --all

clean:
cargo clean
11 changes: 11 additions & 0 deletions contracts/utils/pausable/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_std]

mod pausable;
mod storage;

pub use crate::{
pausable::{emit_paused, emit_unpaused, Pausable, PausableClient},
storage::{pause, paused, unpause, when_not_paused, when_paused},
};

mod test;
104 changes: 104 additions & 0 deletions contracts/utils/pausable/src/pausable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Pausable Contract Module.
//!
//! Contract module which allows implementing an emergency stop mechanism
//! that can be triggered by an authorized account.
//!
//! It provides functions [`pausable::when_not_paused`]
//! and [`pausable::when_paused`],
//! which can be added to the functions of your contract.
//!
//! Note that your contract will NOT be pausable by simply including this
//! module only once and where you use [`pausable::when_not_paused`].
use soroban_sdk::{contractclient, contracterror, symbol_short, Address, Env};

#[contractclient(name = "PausableClient")]
pub trait Pausable {
/// Returns true if the contract is paused, and false otherwise.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
fn paused(e: Env) -> bool;

/// Triggers `Paused` state.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Errors
///
/// If the contract is in `Paused` state, then the error
/// [`PausableError::EnforcedPause`] is thrown.
///
/// # Events
///
/// * topics - `["paused"]`
/// * data - `[caller: Address]`
fn pause(e: Env, caller: Address);

/// Triggers `Unpaused` state.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Errors
///
/// If the contract is in `Unpaused` state, then the error
/// [`PausableError::ExpectedPause`] is thrown.
///
/// # Events
///
/// * topics - `["unpaused"]`
/// * data - `[caller: Address]`
fn unpause(e: Env, caller: Address);
}

// ################## ERRORS ##################

#[contracterror]
#[repr(u32)]
pub enum PausableError {
/// The operation failed because the contract is paused.
EnforcedPause = 1,
/// The operation failed because the contract is not paused.
ExpectedPause = 2,
}

// ################## EVENTS ##################

/// Emits an event when `Paused` state is triggered.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Events
///
/// * topics - `["paused"]`
/// * data - `[caller: Address]`
pub fn emit_paused(e: &Env, caller: &Address) {
let topics = (symbol_short!("paused"),);
e.events().publish(topics, caller)
}

/// Emits an event when `Unpaused` state is triggered.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Events
///
/// * topics - `["unpaused"]`
/// * data - `[caller: Address]`
pub fn emit_unpaused(e: &Env, caller: &Address) {
let topics = (symbol_short!("unpaused"),);
e.events().publish(topics, caller)
}
96 changes: 96 additions & 0 deletions contracts/utils/pausable/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use soroban_sdk::{panic_with_error, symbol_short, Address, Env, Symbol};

use crate::{emit_paused, emit_unpaused, pausable::PausableError};

/// Indicates whether the contract is in `Paused` state.
pub(crate) const PAUSED: Symbol = symbol_short!("PAUSED");

/// Returns true if the contract is paused, and false otherwise.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
pub fn paused(e: &Env) -> bool {
// if not paused, consider default false (unpaused)
e.storage().instance().get(&PAUSED).unwrap_or(false)
}

/// Triggers `Paused` state.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Errors
///
/// If the contract is in `Paused` state, then the error
/// [`PausableError::EnforcedPause`] is thrown.
///
/// # Events
///
/// * topics - `["paused"]`
/// * data - `[caller: Address]`
pub fn pause(e: &Env, caller: &Address) {
caller.require_auth();
when_not_paused(e);
e.storage().instance().set(&PAUSED, &true);
emit_paused(e, caller);
}

/// Triggers `Unpaused` state.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
/// * `caller` - The address of the caller.
///
/// # Errors
///
/// If the contract is in `Unpaused` state, then the error
/// [`PausableError::ExpectedPause`] is thrown.
///
/// # Events
///
/// * topics - `["unpaused"]`
/// * data - `[caller: Address]`
pub fn unpause(e: &Env, caller: &Address) {
caller.require_auth();
when_paused(e);
e.storage().instance().set(&PAUSED, &false);
emit_unpaused(e, caller);
}

/// Helper to make a function callable only when the contract is NOT
/// paused.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
///
/// # Errors
///
/// If the contract is in the `Paused` state, then the error
/// [`PausableError::EnforcedPause`] is thrown.
pub fn when_not_paused(e: &Env) {
if paused(e) {
panic_with_error!(e, PausableError::EnforcedPause)
}
}

/// Helper to make a function callable
/// only when the contract is paused.
///
/// # Arguments
///
/// * `e` - Access to Soroban environment.
///
/// # Errors
///
/// If the contract is in `Unpaused` state, then the error
/// [`PausableError::ExpectedPause`] is thrown.
pub fn when_paused(e: &Env) {
if !paused(e) {
panic_with_error!(e, PausableError::ExpectedPause)
}
}
Loading

0 comments on commit 0075f68

Please sign in to comment.