From 0d4af958562e502df15dcd6bc50ec4ec66cbae46 Mon Sep 17 00:00:00 2001 From: Milap Sheth Date: Thu, 23 Jan 2025 20:58:35 -0500 Subject: [PATCH] feat(axelar-std)!: add pausable interface (#204) --- contracts/stellar-example/src/tests/test.rs | 6 +- .../src/contract.rs | 36 +--- .../src/event.rs | 5 - .../src/interface.rs | 19 +- .../src/storage_types.rs | 1 - .../src/tests/deploy_interchain_token.rs | 12 +- .../tests/deploy_remote_interchain_token.rs | 2 +- .../src/tests/execute.rs | 2 +- .../src/tests/interchain_transfer.rs | 2 +- .../src/tests/pause.rs | 23 ++- .../src/tests/register_canonical_token.rs | 2 +- .../src/tests/testdata/pause_succeeds.golden | 7 +- .../tests/testdata/unpause_succeeds.golden | 7 +- packages/stellar-axelar-std-derive/src/lib.rs | 22 +++ .../stellar-axelar-std-derive/src/pausable.rs | 23 +++ .../stellar-axelar-std/src/interfaces/mod.rs | 11 ++ .../src/interfaces/pausable.rs | 171 ++++++++++++++++++ .../pause_succeeds_when_not_paused.golden | 6 + .../unpause_succeeds_when_paused.golden | 6 + packages/stellar-axelar-std/src/lib.rs | 4 +- packages/stellar-axelar-std/src/tests/test.rs | 34 ++++ 21 files changed, 319 insertions(+), 82 deletions(-) create mode 100644 packages/stellar-axelar-std-derive/src/pausable.rs create mode 100644 packages/stellar-axelar-std/src/interfaces/pausable.rs create mode 100644 packages/stellar-axelar-std/src/interfaces/testdata/pause_succeeds_when_not_paused.golden create mode 100644 packages/stellar-axelar-std/src/interfaces/testdata/unpause_succeeds_when_paused.golden diff --git a/contracts/stellar-example/src/tests/test.rs b/contracts/stellar-example/src/tests/test.rs index bece24b3..d484b89e 100644 --- a/contracts/stellar-example/src/tests/test.rs +++ b/contracts/stellar-example/src/tests/test.rs @@ -201,7 +201,7 @@ fn its_example() { let trusted_chain_set_event = events::fmt_last_emitted_event::(&env); - let data = Some(Address::generate(&env).to_string_bytes()); + let data = Address::generate(&env).to_string_bytes(); let msg = stellar_interchain_token_service::types::HubMessage::ReceiveFromHub { source_chain: original_source_chain, @@ -211,7 +211,7 @@ fn its_example() { source_address: user, destination_address: example_app.address.to_string_bytes(), amount, - data: data.clone(), + data: Some(data.clone()), }, ), }; @@ -249,6 +249,6 @@ fn its_example() { let token = token::TokenClient::new(&env, &its_client.token_address(&token_id)); assert_eq!(token.balance(&example_app.address), 0); - let recipient = Address::from_string_bytes(&data.unwrap()); + let recipient = Address::from_string_bytes(&data); assert_eq!(token.balance(&recipient), amount); } diff --git a/contracts/stellar-interchain-token-service/src/contract.rs b/contracts/stellar-interchain-token-service/src/contract.rs index b8148db9..900c052c 100644 --- a/contracts/stellar-interchain-token-service/src/contract.rs +++ b/contracts/stellar-interchain-token-service/src/contract.rs @@ -11,14 +11,14 @@ use stellar_axelar_std::address::AddressExt; use stellar_axelar_std::events::Event; use stellar_axelar_std::ttl::{extend_instance_ttl, extend_persistent_ttl}; use stellar_axelar_std::types::Token; -use stellar_axelar_std::{ensure, interfaces, Operatable, Ownable, Upgradable}; +use stellar_axelar_std::{ensure, interfaces, Operatable, Ownable, Pausable, Upgradable}; use stellar_interchain_token::InterchainTokenClient; use crate::error::ContractError; use crate::event::{ InterchainTokenDeployedEvent, InterchainTokenDeploymentStartedEvent, InterchainTokenIdClaimedEvent, InterchainTransferReceivedEvent, InterchainTransferSentEvent, - PauseStatusSetEvent, TrustedChainRemovedEvent, TrustedChainSetEvent, + TrustedChainRemovedEvent, TrustedChainSetEvent, }; use crate::flow_limit::FlowDirection; use crate::interface::InterchainTokenServiceInterface; @@ -36,7 +36,7 @@ const PREFIX_CANONICAL_TOKEN_SALT: &str = "canonical-token-salt"; const EXECUTE_WITH_TOKEN: &str = "execute_with_interchain_token"; #[contract] -#[derive(Operatable, Ownable, Upgradable)] +#[derive(Operatable, Ownable, Pausable, Upgradable)] pub struct InterchainTokenService; #[contractimpl] @@ -195,10 +195,6 @@ impl InterchainTokenServiceInterface for InterchainTokenService { .token_manager_type } - fn is_paused(env: &Env) -> bool { - env.storage().instance().has(&DataKey::Paused) - } - fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option { flow_limit::flow_limit(env, token_id) } @@ -221,20 +217,6 @@ impl InterchainTokenServiceInterface for InterchainTokenService { flow_limit::set_flow_limit(env, token_id, flow_limit) } - fn set_pause_status(env: &Env, paused: bool) -> Result<(), ContractError> { - Self::owner(env).require_auth(); - - if paused { - env.storage().instance().set(&DataKey::Paused, &()); - } else { - env.storage().instance().remove(&DataKey::Paused); - } - - PauseStatusSetEvent { paused }.emit(env); - - Ok(()) - } - fn deploy_interchain_token( env: &Env, caller: Address, @@ -243,7 +225,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { initial_supply: i128, minter: Option
, ) -> Result, ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); caller.require_auth(); @@ -300,7 +282,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { destination_chain: String, gas_token: Token, ) -> Result, ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); caller.require_auth(); @@ -313,7 +295,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { env: &Env, token_address: Address, ) -> Result, ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); let deploy_salt = Self::canonical_token_deploy_salt(env, token_address.clone()); let token_id = Self::interchain_token_id(env, Address::zero(env), deploy_salt.clone()); @@ -351,7 +333,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { spender: Address, gas_token: Token, ) -> Result, ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); let deploy_salt = Self::canonical_token_deploy_salt(env, token_address); @@ -371,7 +353,7 @@ impl InterchainTokenServiceInterface for InterchainTokenService { data: Option, gas_token: Token, ) -> Result<(), ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); ensure!(amount > 0, ContractError::InvalidAmount); @@ -502,7 +484,7 @@ impl InterchainTokenService { source_address: String, payload: Bytes, ) -> Result<(), ContractError> { - ensure!(!Self::is_paused(env), ContractError::ContractPaused); + ensure!(!Self::paused(env), ContractError::ContractPaused); let (source_chain, message) = Self::get_execute_params(env, source_chain, source_address, payload)?; diff --git a/contracts/stellar-interchain-token-service/src/event.rs b/contracts/stellar-interchain-token-service/src/event.rs index c5a89116..b040aa2a 100644 --- a/contracts/stellar-interchain-token-service/src/event.rs +++ b/contracts/stellar-interchain-token-service/src/event.rs @@ -20,11 +20,6 @@ pub struct FlowLimitSetEvent { pub flow_limit: Option, } -#[derive(Debug, PartialEq, Eq, IntoEvent)] -pub struct PauseStatusSetEvent { - pub paused: bool, -} - #[derive(Debug, PartialEq, Eq, IntoEvent)] pub struct InterchainTokenDeployedEvent { pub token_id: BytesN<32>, diff --git a/contracts/stellar-interchain-token-service/src/interface.rs b/contracts/stellar-interchain-token-service/src/interface.rs index 291427a6..b542473e 100644 --- a/contracts/stellar-interchain-token-service/src/interface.rs +++ b/contracts/stellar-interchain-token-service/src/interface.rs @@ -1,6 +1,9 @@ use soroban_sdk::{contractclient, Address, Bytes, BytesN, Env, String}; use soroban_token_sdk::metadata::TokenMetadata; use stellar_axelar_gateway::executable::AxelarExecutableInterface; +use stellar_axelar_std::interfaces::{ + OperatableInterface, OwnableInterface, PausableInterface, UpgradableInterface, +}; use stellar_axelar_std::types::Token; use crate::error::ContractError; @@ -8,7 +11,13 @@ use crate::types::TokenManagerType; #[allow(dead_code)] #[contractclient(name = "InterchainTokenServiceClient")] -pub trait InterchainTokenServiceInterface: AxelarExecutableInterface { +pub trait InterchainTokenServiceInterface: + AxelarExecutableInterface + + OwnableInterface + + OperatableInterface + + PausableInterface + + UpgradableInterface +{ /// Returns the address of the Gas Service contract. fn gas_service(env: &Env) -> Address; @@ -81,9 +90,6 @@ pub trait InterchainTokenServiceInterface: AxelarExecutableInterface { /// Returns the type of the token manager associated with the specified token ID. fn token_manager_type(env: &Env, token_id: BytesN<32>) -> TokenManagerType; - /// Returns whether the contract is paused. - fn is_paused(env: &Env) -> bool; - /// Returns the flow limit for the token associated with the specified token ID. /// Returns `None` if no limit is set. fn flow_limit(env: &Env, token_id: BytesN<32>) -> Option; @@ -117,11 +123,6 @@ pub trait InterchainTokenServiceInterface: AxelarExecutableInterface { flow_limit: Option, ) -> Result<(), ContractError>; - /// Sets the pause status of the contract. - /// # Authorization - /// - Must be called by [`Self::owner`]. - fn set_pause_status(env: &Env, paused: bool) -> Result<(), ContractError>; - /// Deploys a new interchain token on the current chain with specified metadata and optional /// initial supply. If initial supply is provided, it is minted to the caller. The /// caller can also specify an optional minter address for the interchain token. diff --git a/contracts/stellar-interchain-token-service/src/storage_types.rs b/contracts/stellar-interchain-token-service/src/storage_types.rs index 05b7454b..0602525f 100644 --- a/contracts/stellar-interchain-token-service/src/storage_types.rs +++ b/contracts/stellar-interchain-token-service/src/storage_types.rs @@ -11,7 +11,6 @@ pub enum DataKey { ItsHubAddress, NativeTokenAddress, InterchainTokenWasmHash, - Paused, TrustedChain(String), TokenIdConfig(BytesN<32>), FlowLimit(BytesN<32>), diff --git a/contracts/stellar-interchain-token-service/src/tests/deploy_interchain_token.rs b/contracts/stellar-interchain-token-service/src/tests/deploy_interchain_token.rs index eb2c9034..f09c15b4 100644 --- a/contracts/stellar-interchain-token-service/src/tests/deploy_interchain_token.rs +++ b/contracts/stellar-interchain-token-service/src/tests/deploy_interchain_token.rs @@ -40,7 +40,7 @@ fn deploy_interchain_token_succeeds() { fn deploy_interchain_token_fails_when_paused() { let (env, client, _, _, _) = setup_env(); - client.mock_all_auths().set_pause_status(&true); + client.mock_all_auths().pause(); assert_contract_err!( client.try_deploy_interchain_token( @@ -117,7 +117,7 @@ fn deploy_interchain_token_check_token_id_and_token_manager_type() { let (env, client, _, _, _) = setup_env(); let (sender, salt, token_metadata) = dummy_token_params(&env); - let minter = Address::generate(&env); + let minter = Some(Address::generate(&env)); let initial_supply = 100; let deploy_salt = client.interchain_token_deploy_salt(&sender, &salt); @@ -125,13 +125,7 @@ fn deploy_interchain_token_check_token_id_and_token_manager_type() { let token_id = assert_auth!( &sender, - client.deploy_interchain_token( - &sender, - &salt, - &token_metadata, - &initial_supply, - &Some(minter.clone()), - ) + client.deploy_interchain_token(&sender, &salt, &token_metadata, &initial_supply, &minter,) ); goldie::assert!(events::fmt_emitted_event_at_idx::< diff --git a/contracts/stellar-interchain-token-service/src/tests/deploy_remote_interchain_token.rs b/contracts/stellar-interchain-token-service/src/tests/deploy_remote_interchain_token.rs index 88b9ac7b..719383f5 100644 --- a/contracts/stellar-interchain-token-service/src/tests/deploy_remote_interchain_token.rs +++ b/contracts/stellar-interchain-token-service/src/tests/deploy_remote_interchain_token.rs @@ -97,7 +97,7 @@ fn deploy_remote_interchain_token_succeeds() { fn deploy_remote_interchain_token_fails_when_paused() { let (env, client, _, _, _) = setup_env(); - client.mock_all_auths().set_pause_status(&true); + client.mock_all_auths().pause(); assert_contract_err!( client.try_deploy_remote_interchain_token( diff --git a/contracts/stellar-interchain-token-service/src/tests/execute.rs b/contracts/stellar-interchain-token-service/src/tests/execute.rs index 5a136211..ec45fe51 100644 --- a/contracts/stellar-interchain-token-service/src/tests/execute.rs +++ b/contracts/stellar-interchain-token-service/src/tests/execute.rs @@ -154,7 +154,7 @@ fn execute_fails_when_paused() { ]; approve_gateway_messages(&env, &gateway_client, signers, messages); - client.mock_all_auths().set_pause_status(&true); + client.mock_all_auths().pause(); assert_contract_err!( client.try_execute(&source_chain, &message_id, &source_address, &payload,), diff --git a/contracts/stellar-interchain-token-service/src/tests/interchain_transfer.rs b/contracts/stellar-interchain-token-service/src/tests/interchain_transfer.rs index 45978e98..defbdc52 100644 --- a/contracts/stellar-interchain-token-service/src/tests/interchain_transfer.rs +++ b/contracts/stellar-interchain-token-service/src/tests/interchain_transfer.rs @@ -62,7 +62,7 @@ fn interchain_transfer_send_succeeds() { fn interchain_transfer_send_fails_when_paused() { let (env, client, _, _, _) = setup_env(); - client.mock_all_auths().set_pause_status(&true); + client.mock_all_auths().pause(); assert_contract_err!( client.try_interchain_transfer( diff --git a/contracts/stellar-interchain-token-service/src/tests/pause.rs b/contracts/stellar-interchain-token-service/src/tests/pause.rs index 994322b0..fa82f827 100644 --- a/contracts/stellar-interchain-token-service/src/tests/pause.rs +++ b/contracts/stellar-interchain-token-service/src/tests/pause.rs @@ -1,40 +1,39 @@ use soroban_sdk::testutils::Address as _; use soroban_sdk::Address; +use stellar_axelar_std::interfaces::{PausedEvent, UnpausedEvent}; use stellar_axelar_std::{assert_auth, assert_auth_err, events}; use super::utils::setup_env; -use crate::event::PauseStatusSetEvent; #[test] fn pause_succeeds() { let (env, client, _, _, _) = setup_env(); - assert!(!client.is_paused()); + assert!(!client.paused()); - assert_auth!(client.owner(), client.set_pause_status(&true)); - goldie::assert!(events::fmt_last_emitted_event::(&env)); + assert_auth!(client.owner(), client.pause()); + goldie::assert!(events::fmt_last_emitted_event::(&env)); - assert!(client.is_paused()); + assert!(client.paused()); } #[test] fn unpause_succeeds() { let (env, client, _, _, _) = setup_env(); - assert_auth!(client.owner(), client.set_pause_status(&true)); + assert_auth!(client.owner(), client.pause()); - assert!(client.is_paused()); - assert_auth!(client.owner(), client.set_pause_status(&false)); + assert!(client.paused()); + assert_auth!(client.owner(), client.unpause()); - goldie::assert!(events::fmt_last_emitted_event::(&env)); + goldie::assert!(events::fmt_last_emitted_event::(&env)); - assert!(!client.is_paused()); + assert!(!client.paused()); } #[test] fn pause_fails_with_invalid_auth() { let (env, client, _, _, _) = setup_env(); - let user = Address::generate(&env); - assert_auth_err!(user, client.set_pause_status(&true)); + assert_auth_err!(Address::generate(&env), client.pause()); } diff --git a/contracts/stellar-interchain-token-service/src/tests/register_canonical_token.rs b/contracts/stellar-interchain-token-service/src/tests/register_canonical_token.rs index cbb34947..e7c03939 100644 --- a/contracts/stellar-interchain-token-service/src/tests/register_canonical_token.rs +++ b/contracts/stellar-interchain-token-service/src/tests/register_canonical_token.rs @@ -34,7 +34,7 @@ fn register_canonical_token_succeeds() { fn register_canonical_token_fails_when_paused() { let (env, client, _, _, _) = setup_env(); - client.mock_all_auths().set_pause_status(&true); + client.mock_all_auths().pause(); assert_contract_err!( client.try_register_canonical_token(&Address::generate(&env)), diff --git a/contracts/stellar-interchain-token-service/src/tests/testdata/pause_succeeds.golden b/contracts/stellar-interchain-token-service/src/tests/testdata/pause_succeeds.golden index e378c9b1..962db45a 100644 --- a/contracts/stellar-interchain-token-service/src/tests/testdata/pause_succeeds.golden +++ b/contracts/stellar-interchain-token-service/src/tests/testdata/pause_succeeds.golden @@ -1,9 +1,6 @@ -PauseStatusSetEvent { - paused: true, -} +PausedEvent Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5) -pause_status_set { - #[topic] paused: bool, +paused { } \ No newline at end of file diff --git a/contracts/stellar-interchain-token-service/src/tests/testdata/unpause_succeeds.golden b/contracts/stellar-interchain-token-service/src/tests/testdata/unpause_succeeds.golden index 0b6b9dd9..1bceeb13 100644 --- a/contracts/stellar-interchain-token-service/src/tests/testdata/unpause_succeeds.golden +++ b/contracts/stellar-interchain-token-service/src/tests/testdata/unpause_succeeds.golden @@ -1,9 +1,6 @@ -PauseStatusSetEvent { - paused: false, -} +UnpausedEvent Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVAX5) -pause_status_set { - #[topic] paused: bool, +unpaused { } \ No newline at end of file diff --git a/packages/stellar-axelar-std-derive/src/lib.rs b/packages/stellar-axelar-std-derive/src/lib.rs index c796ed1d..ea72adea 100644 --- a/packages/stellar-axelar-std-derive/src/lib.rs +++ b/packages/stellar-axelar-std-derive/src/lib.rs @@ -6,6 +6,7 @@ mod event; mod its_executable; mod operatable; mod ownable; +mod pausable; mod upgradable; use proc_macro::TokenStream; @@ -68,6 +69,27 @@ pub fn derive_ownable(input: TokenStream) -> TokenStream { ownable::ownable(name).into() } +/// Implements the Pausable interface for a Soroban contract. +/// +/// # Example +/// ```rust,ignore +/// # mod test { +/// # use soroban_sdk::{contract, contractimpl, Address, Env}; +/// use stellar_axelar_std_derive::Pausable; +/// +/// #[contract] +/// #[derive(Pausable)] +/// pub struct Contract; +/// # } +/// ``` +#[proc_macro_derive(Pausable)] +pub fn derive_pausable(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + pausable::pausable(name).into() +} + /// Implements the Upgradable and Migratable interfaces for a Soroban contract. /// /// A `ContractError` error type must be defined in scope, and have a `MigrationNotAllowed` variant. diff --git a/packages/stellar-axelar-std-derive/src/pausable.rs b/packages/stellar-axelar-std-derive/src/pausable.rs new file mode 100644 index 00000000..2a3e0ea5 --- /dev/null +++ b/packages/stellar-axelar-std-derive/src/pausable.rs @@ -0,0 +1,23 @@ +use proc_macro2::{Ident, TokenStream as TokenStream2}; +use quote::quote; + +pub fn pausable(name: &Ident) -> TokenStream2 { + quote! { + use stellar_axelar_std::interfaces::PausableInterface as _; + + #[soroban_sdk::contractimpl] + impl stellar_axelar_std::interfaces::PausableInterface for #name { + fn paused(env: &Env) -> bool { + stellar_axelar_std::interfaces::paused(env) + } + + fn pause(env: &Env) { + stellar_axelar_std::interfaces::pause::(env); + } + + fn unpause(env: &Env) { + stellar_axelar_std::interfaces::unpause::(env); + } + } + } +} diff --git a/packages/stellar-axelar-std/src/interfaces/mod.rs b/packages/stellar-axelar-std/src/interfaces/mod.rs index 8eaf75c3..1a111a3e 100644 --- a/packages/stellar-axelar-std/src/interfaces/mod.rs +++ b/packages/stellar-axelar-std/src/interfaces/mod.rs @@ -1,11 +1,13 @@ mod operatable; mod ownable; +mod pausable; #[cfg(test)] mod testdata; mod upgradable; pub use operatable::*; pub use ownable::*; +pub use pausable::*; pub use upgradable::*; /// Marker trait for interfaces that should not be implemented by using `contractimpl`. @@ -43,6 +45,15 @@ mod storage { } } + pub mod pausable { + use soroban_sdk::contracttype; + + #[contracttype] + pub enum DataKey { + Interfaces_Paused, + } + } + pub mod migrating { use soroban_sdk::contracttype; diff --git a/packages/stellar-axelar-std/src/interfaces/pausable.rs b/packages/stellar-axelar-std/src/interfaces/pausable.rs new file mode 100644 index 00000000..933494c0 --- /dev/null +++ b/packages/stellar-axelar-std/src/interfaces/pausable.rs @@ -0,0 +1,171 @@ +use core::fmt::Debug; + +use soroban_sdk::{contractclient, Env}; + +use crate as stellar_axelar_std; +use crate::events::Event; +use crate::interfaces::{storage, OwnableInterface}; +use crate::IntoEvent; + +#[contractclient(name = "PausableClient")] +pub trait PausableInterface: OwnableInterface { + /// Returns whether the contract is currently paused. + fn paused(env: &Env) -> bool; + + /// Pauses the contract. Only callable by the owner. + fn pause(env: &Env); + + /// Unpauses the contract. Only callable by the owner. + fn unpause(env: &Env); +} + +/// Default implementation of the [`PausableInterface`] trait. +pub fn paused(env: &Env) -> bool { + env.storage() + .instance() + .has(&storage::pausable::DataKey::Interfaces_Paused) +} + +/// Default implementation of the [`PausableInterface`] trait. +pub fn pause(env: &Env) { + T::owner(env).require_auth(); + + env.storage() + .instance() + .set(&storage::pausable::DataKey::Interfaces_Paused, &()); + + PausedEvent {}.emit(env); +} + +/// Default implementation of the [`PausableInterface`] trait. +pub fn unpause(env: &Env) { + T::owner(env).require_auth(); + + env.storage() + .instance() + .remove(&storage::pausable::DataKey::Interfaces_Paused); + + UnpausedEvent {}.emit(env); +} + +#[derive(Clone, Debug, PartialEq, Eq, IntoEvent)] +pub struct PausedEvent {} + +#[derive(Clone, Debug, PartialEq, Eq, IntoEvent)] +pub struct UnpausedEvent {} + +#[cfg(test)] +mod test { + use soroban_sdk::testutils::Address as _; + use soroban_sdk::{contract, contractimpl, Address, Env}; + + use super::{PausedEvent, UnpausedEvent}; + use crate::interfaces::{OwnableInterface, PausableInterface}; + use crate::{assert_auth, assert_auth_err, events}; + + #[contract] + pub struct Contract; + + #[contractimpl] + impl Contract { + pub fn __constructor(env: &Env, owner: Address) { + crate::interfaces::ownable::set_owner(env, &owner); + } + } + + #[contractimpl] + impl PausableInterface for Contract { + fn paused(env: &Env) -> bool { + super::paused(env) + } + + fn pause(env: &Env) { + super::pause::(env) + } + + fn unpause(env: &Env) { + super::unpause::(env) + } + } + + #[contractimpl] + impl OwnableInterface for Contract { + fn owner(env: &Env) -> Address { + crate::interfaces::ownable::owner(env) + } + + fn transfer_ownership(env: &Env, new_owner: Address) { + crate::interfaces::ownable::transfer_ownership::(env, new_owner) + } + } + + fn setup<'a>() -> (Env, ContractClient<'a>) { + let env = Env::default(); + let owner = Address::generate(&env); + let contract_id = env.register(Contract, (owner,)); + let client = ContractClient::new(&env, &contract_id); + (env, client) + } + + #[test] + fn paused_returns_false_by_default() { + let (_, client) = setup(); + assert!(!client.paused()); + } + + #[test] + fn pause_succeeds_when_not_paused() { + let (env, client) = setup(); + + assert!(!client.paused()); + + assert_auth!(client.owner(), client.pause()); + goldie::assert!(events::fmt_last_emitted_event::(&env)); + + assert!(client.paused()); + } + + #[test] + fn pause_succeeds_when_already_paused() { + let (_, client) = setup(); + + assert_auth!(client.owner(), client.pause()); + assert_auth!(client.owner(), client.pause()); + assert!(client.paused()); + } + + #[test] + fn pause_fails_when_not_owner() { + let (env, client) = setup(); + + assert_auth_err!(Address::generate(&env), client.pause()); + } + + #[test] + fn unpause_succeeds_when_paused() { + let (env, client) = setup(); + + assert_auth!(client.owner(), client.pause()); + assert!(client.paused()); + + assert_auth!(client.owner(), client.unpause()); + goldie::assert!(events::fmt_last_emitted_event::(&env)); + + assert!(!client.paused()); + } + + #[test] + fn unpause_fails_when_not_paused() { + let (_, client) = setup(); + + assert_auth!(client.owner(), client.unpause()); + assert!(!client.paused()); + } + + #[test] + fn unpause_fails_when_not_owner() { + let (env, client) = setup(); + + assert_auth_err!(Address::generate(&env), client.unpause()); + } +} diff --git a/packages/stellar-axelar-std/src/interfaces/testdata/pause_succeeds_when_not_paused.golden b/packages/stellar-axelar-std/src/interfaces/testdata/pause_succeeds_when_not_paused.golden new file mode 100644 index 00000000..75c9c4cb --- /dev/null +++ b/packages/stellar-axelar-std/src/interfaces/testdata/pause_succeeds_when_not_paused.golden @@ -0,0 +1,6 @@ +PausedEvent + +Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4) + +paused { +} \ No newline at end of file diff --git a/packages/stellar-axelar-std/src/interfaces/testdata/unpause_succeeds_when_paused.golden b/packages/stellar-axelar-std/src/interfaces/testdata/unpause_succeeds_when_paused.golden new file mode 100644 index 00000000..01587e06 --- /dev/null +++ b/packages/stellar-axelar-std/src/interfaces/testdata/unpause_succeeds_when_paused.golden @@ -0,0 +1,6 @@ +UnpausedEvent + +Contract(CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4) + +unpaused { +} \ No newline at end of file diff --git a/packages/stellar-axelar-std/src/lib.rs b/packages/stellar-axelar-std/src/lib.rs index 28790d2a..4e310635 100644 --- a/packages/stellar-axelar-std/src/lib.rs +++ b/packages/stellar-axelar-std/src/lib.rs @@ -20,10 +20,10 @@ pub mod ttl; pub mod events; -#[cfg(feature = "derive")] +#[cfg(any(test, feature = "derive"))] pub mod interfaces; pub mod address; -#[cfg(feature = "derive")] +#[cfg(any(test, feature = "derive"))] pub use stellar_axelar_std_derive::*; diff --git a/packages/stellar-axelar-std/src/tests/test.rs b/packages/stellar-axelar-std/src/tests/test.rs index 98072ef8..679879e3 100644 --- a/packages/stellar-axelar-std/src/tests/test.rs +++ b/packages/stellar-axelar-std/src/tests/test.rs @@ -80,6 +80,40 @@ mod ownable { assert_eq!(new_owner, client.owner()); } } +mod pausable { + use stellar_axelar_std::assert_auth; + use stellar_axelar_std::interfaces::PausableClient; + use stellar_axelar_std_derive::{Ownable, Pausable}; + + use super::*; + + #[contract] + #[derive(Ownable, Pausable)] + pub struct Contract; + + #[contractimpl] + impl Contract { + pub fn __constructor(env: &Env, owner: Address) { + stellar_axelar_std::interfaces::set_owner(env, &owner); + } + } + + #[test] + fn contract_pause_unpause_succeeds() { + let env = Env::default(); + let owner = Address::generate(&env); + let contract_id = env.register(Contract, (owner.clone(),)); + let client = PausableClient::new(&env, &contract_id); + + assert!(!client.paused()); + + assert_auth!(owner, client.pause()); + assert!(client.paused()); + + assert_auth!(owner, client.unpause()); + assert!(!client.paused()); + } +} mod upgradable { use stellar_axelar_std::assert_auth;