diff --git a/README.md b/README.md index 44ebc5d..b086a83 100644 --- a/README.md +++ b/README.md @@ -96,120 +96,11 @@ Documentation of all methods provided by the derived implementation of `FullAcce Allow contracts to implement an emergency stop mechanism that can be triggered by an authorized account. Pauses can be used granularly to only limit certain features. -Contract example using _Pausable_ plugin. Note that it requires the contract to be _AccessControllable_ in order to manage permissions. Roles allowing accounts to call certain methods can be granted and revoked via the _AccessControllable_ plugin. +Using the `Pausable` plugin requires the contract to be _AccessControllable_ in order to manage permissions. Roles allowing accounts to call certain methods can be granted and revoked via the _AccessControllable_ plugin. -```rust - -/// Define roles for access control of `Pausable` features. Accounts which are -/// granted a role are authorized to execute the corresponding action. -#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] -#[serde(crate = "near_sdk::serde")] -pub enum Role { - /// May pause and unpause features. - PauseManager, - /// May call `increase_4` even when it is paused. - Unrestricted4Increaser, - /// May call `decrease_4` even when `increase_4` is not paused. - Unrestricted4Decreaser, - /// May always call both `increase_4` and `decrease_4`. - Unrestricted4Modifier, -} - -#[access_control(role_type(Role))] -#[near_bindgen] -#[derive(Pausable)] -#[pausable(manager_roles(Role::PauseManager))] -struct Counter { - counter: u64, -} - -#[near_bindgen] -impl Counter { - /// Initialize access control in the constructor. - #[init] - fn new() -> Self { - let mut contract = Self { - counter: 0, - __acl: Default::default(), - }; - - // Make the contract itself access control super admin. This enables - // granting roles below. - near_sdk::require!( - contract.acl_init_super_admin(near_sdk::env::predecessor_account_id()), - "Failed to initialize super admin", - ); - - // Grant access control roles. - let grants: Vec<(Role, near_sdk::AccountId)> = vec![ - (Role::PauseManager, "anna.test".parse().unwrap()), - (Role::Unrestricted4Increaser, "brenda.test".parse().unwrap()), - (Role::Unrestricted4Decreaser, "chris.test".parse().unwrap()), - (Role::Unrestricted4Modifier, "daniel.test".parse().unwrap()), - ]; - for (role, account_id) in grants { - let result = contract.acl_grant_role(role.into(), account_id); - near_sdk::require!(Some(true) == result, "Failed to grant role"); - } - - contract - } - - /// Function can be paused using feature name "increase_1" or "ALL" like: - /// `contract.pa_pause_feature("increase_1")` or `contract.pa_pause_feature("ALL")` - /// - /// If the function is paused, all calls to it will fail. Even calls - /// initiated by accounts which are access control super admin or role - /// grantee. - #[pause] - fn increase_1(&mut self) { - self.counter += 1; - } - - /// Similar to `#[pause]` but use an explicit name for the feature. In this - /// case the feature to be paused is named "Increase by two". Note that - /// trying to pause it using "increase_2" will not have any effect. - /// - /// This can be used to pause a subset of the methods at once without - /// requiring to use "ALL". - #[pause(name = "Increase by two")] - fn increase_2(&mut self) { - self.counter += 2; - } - - /// Similar to `#[pause]` but roles passed as argument may still - /// successfully call this method. - #[pause(except(roles(Role::Unrestricted4Increaser, Role::Unrestricted4Modifier)))] - fn increase_4(&mut self) { - self.counter += 4; - } - - /// This method can only be called when "increase_1" is paused. Use this - /// macro to create escape hatches when some features are paused. Note that - /// if "ALL" is specified the "increase_1" is considered to be paused. - #[if_paused(name = "increase_1")] - fn decrease_1(&mut self) { - self.counter -= 1; - } - - /// Custom use of pause features. Only allow increasing the counter using `careful_increase` if it is below 10. - - /// Custom use of pause features. Only allow increasing the counter using - /// `careful_increase` if it is below 10. - fn careful_increase(&mut self) { - if self.counter >= 10 { - assert!( - !self.pa_is_paused("INCREASE_BIG".to_string()), - "Method paused for large values of counter" - ); - } - - self.counter += 1; - } -} -``` +[This contract](/near-plugins/tests/contracts/pausable/src/lib.rs) provides an example of using `Pausable`. It is compiled, deployed on chain and interacted with in [integration tests](/near-plugins/tests/pausable.rs). -Documentation of all methods provided by the derived implementation of `Pausable` is available in the [definition of the trait](/near-plugins/src/pausable.rs). More examples and guidelines for interacting with a `Pausable` contract can be found [here](/examples/pausable-examples/README.md). +Documentation of all methods provided by `Pausable` is available in the [definition of the trait](/near-plugins/src/pausable.rs). ### [Upgradable](/near-plugins/src/upgradable.rs) diff --git a/examples/pausable-examples/README.md b/examples/pausable-examples/README.md deleted file mode 100644 index e0f2439..0000000 --- a/examples/pausable-examples/README.md +++ /dev/null @@ -1,269 +0,0 @@ -# Example of using Pausable plugin -Allows contracts to implement an emergency stop mechanism that can be triggered by an authorized account. Pauses can be used granular to only limit certain features. - -Contract example using Pausable plugin. Note that it requires the contract to be Ownable. - -```rust -use near_plugins::Ownable; -use near_plugins::Pausable; -use near_sdk::near_bindgen; -use near_plugins_derive::{pause, if_paused}; -use borsh::{BorshSerialize, BorshDeserialize}; - -#[near_bindgen] -#[derive(Ownable, Pausable, Default, BorshSerialize, BorshDeserialize)] -struct Counter { - counter: u64, -} - -#[near_bindgen] -impl Counter { - /// Specify the owner of the contract in the constructor - #[init] - pub fn new() -> Self { - let mut contract = Self { counter: 0 }; - contract.owner_set(Some(near_sdk::env::predecessor_account_id())); - contract - } - - /// Function can be paused using feature name "increase_1" or "ALL" like: - /// `contract.pa_pause_feature("increase_1")` or `contract.pa_pause_feature("ALL")` - /// - /// If the function is paused, all calls to it will fail. Even calls started from owner or self. - #[pause] - pub fn increase_1(&mut self) { - self.counter += 1; - } - - /// Similar to `#[pause]` but use an explicit name for the feature. In this case the feature to be paused - /// is named "Increase by two". Note that trying to pause it using "increase_2" will not have any effect. - /// - /// This can be used to pause a subset of the methods at once without requiring to use "ALL". - #[pause(name = "Increase by two")] - pub fn increase_2(&mut self) { - self.counter += 2; - } - - /// Similar to `#[pause]` but owner or self can still call this method. Any subset of {self, owner} can be specified. - #[pause(except(owner, self))] - pub fn increase_4(&mut self) { - self.counter += 4; - } - - /// This method can only be called when "increase_1" is paused. Use this macro to create escape hatches when some - /// features are paused. Note that if "ALL" is specified the "increase_1" is considered to be paused. - #[if_paused(name = "increase_1")] - pub fn decrease_1(&mut self) { - self.counter -= 1; - } - - /// Custom use of pause features. Only allow increasing the counter using `careful_increase` if it is below 10. - pub fn careful_increase(&mut self) { - if self.counter >= 10 { - assert!( - !self.pa_is_paused("INCREASE_BIG".to_string()), - "Method paused for large values of counter" - ); - } - - self.counter += 1; - } - - pub fn get_counter(&self) -> u64 { - self.counter - } -} -``` - -## The contract methods description -### pa_storage_key -`pa_storage_key` is a _view_ method that returns the key of the storage slot with a list of paused features. -By default, `b"__PAUSED__"` is used. For changing, the attribute `pausable` can be used. - -```shell -$ near view pa_storage_key -View call: .pa_storage_key() -[ - 95, 95, 80, 65, 85, - 83, 69, 68, 95, 95 -] -$ python3 ->>> print(' '.join(str(b) for b in bytes("__PAUSED__", 'utf8'))) -95 95 80 65 85 83 69 68 95 95 -``` - -Example of changing paused storage key: -```rust -#[near_bindgen] -#[derive(Ownable, Pausable, Default, BorshSerialize, BorshDeserialize)] -#[pausable(paused_storage_key="OTHER_PAUSED_STORAGE_KEY")] -struct Counter { - counter: u64, -} -``` - -### pa_is_paused -`pa_is_paused` is a _view_ method that returns if a feature is paused. - -```shell -$ near view pa_is_paused '{"key": "increase_1"}' -View call: .pa_is_paused({"key": "increase_1"}) -true -``` - -### pa_all_paused -`pa_all_paused` is a _view_ method that returns the list of all paused features. - -```shell -$ near view pa_all_paused -View call: .pa_all_paused() -[ 'increase_1', 'increase_2' ] -``` - -### pa_pause_feature -`pa_pause_feature` is a method for pausing specified features. Can be run only by the owner or self. - -```shell -$ near call pa_pause_feature '{"key": "increase_1"}' --accountId -``` - -If the method succeeds, the following event will be emitted: -```json -{ - "standard":"Pausable", - "version":"1.0.0", - "event":"pause", - "data": - { - "by":"", - "key":"increase_1" - } -} -``` - -### pa_unpause_feature -`pa_unpause_feature` is a method for unpausing specified features. Can be run only by the owner or self. - -```shell -$ near call pa_unpause_feature '{"key": "increase_1"}' --accountId -``` -If the method succeeds, the following event will be emitted: -```json -{ - "standard":"Pausable", - "version":"1.0.0", - "event":"unpause", - "data": - { - "by":"", - "key":"increase_1" - } -} -``` - -## Preparation steps for demonstration -In that document, we are providing some examples of using a contract with access control plugins. You also can explore the usage examples in the tests in `./pausable_base/src/lib.rs`. For running tests, please take a look at the **Test running instruction** section. - -1. **Creating an account on testnet** - For demonstration let's create 2 accounts: ``, `` - ```shell - $ near create-account ..testnet --masterAccount .testnet --initialBalance 10 - $ near create-account ..testnet --masterAccount .testnet --initialBalance 10 - ``` - - In the next section we will refer to the `..testnet` as ``, - to the `..testnet` as ``. - -2. **Compile Contract to wasm file** - For compiling the contract just run the `./pausable_base/build.sh` script. The target file with compiled contract will be `../target/wasm32-unknown-unknown/release/pausable_base.wasm` - - ```shell - $ cd pausable_base - $ ./build.sh - $ .. - ``` - -3. **Deploy and init a contract** - ```shell - $ near deploy --accountId --wasmFile ../target/wasm32-unknown-unknown/release/pausable_base.wasm --initFunction new --initArgs '{}' - ``` - -## Example of using the contract with pausable plugin -### Simple pause and unpause function without name specification -In the beginning, the `` is both the self and the owner. -`` doesn't have any specific rights. - -No features on pause: -```shell -$ near view pa_all_paused -View call: .pa_all_paused() -null -``` - -Alice can call `increase_1` function: -```shell -$ near call increase_1 --accountId -$ near view get_counter -1 -``` - -#### Pause function -Self(or owner) pause function: -```shell -$ near call pa_pause_feature '{"key": "increase_1"}' --accountId -$ near view pa_all_paused -View call: .pa_all_paused() -[ 'increase_1' ] -$ near view pa_is_paused '{"key": "increase_1"}' -View call: .pa_is_paused({"key": "increase_1"}) -true -``` - -Now Alice or even self can't run `increase_1` function -```shell -$ near view get_counter -1 -$ near call increase_1 --accountId -ERROR -$ near call increase_1 --accountId -ERROR -$ near view get_counter -1 -``` - -#### Unpause function -Self(or owner) can unpause feature: -```shell -$ near call pa_unpause_feature '{"key": "increase_1"}' --accountId -$ near view pa_all_paused -View call: .pa_all_paused() -null -$ near view pa_is_paused '{"key": "increase_1"}' -View call: .pa_is_paused({"key": "increase_1"}) -false -``` - -Now Alice can continue use the `increase_1` function -```shell -$ near view get_counter -1 -$ near call increase_1 --accountId -$ near call increase_1 --accountId -$ near view get_counter -3 -``` - -## Tests running instruction -Tests in `pausable_base/src/lib.rs` contain examples of interaction with a contract. - -For running test: -1. Generate `wasm` file by running `build.sh` script. The target file will be `../target/wasm32-unknown-unknown/release/pausable_base.wasm` -2. Run tests `cargo test` - -```shell -$ cd pausable_base -$ ./build.sh -$ cargo test -``` - -For tests, we use `workspaces` library and `sandbox` environment. For details, you can explore `../near-plugins-test-utils` crate. \ No newline at end of file diff --git a/examples/pausable-examples/pausable_base/Cargo.toml b/examples/pausable-examples/pausable_base/Cargo.toml deleted file mode 100644 index b42375c..0000000 --- a/examples/pausable-examples/pausable_base/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "pausable_base" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -near-sdk = "4.0.0" -near-plugins = { path = "../../../near-plugins" } -near-plugins-derive = { path = "../../../near-plugins-derive" } -borsh = "0.9.3" - -[dev-dependencies] -near-primitives = "0.14.0" -workspaces = "0.6" -tokio = { version = "1.1", features = ["rt", "macros"] } -serde_json = "1.0.74" -near-plugins-test-utils = { path = "../../near-plugins-test-utils" } - - diff --git a/examples/pausable-examples/pausable_base/build.sh b/examples/pausable-examples/pausable_base/build.sh deleted file mode 100755 index 86f59f2..0000000 --- a/examples/pausable-examples/pausable_base/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -mkdir -p ../../res - -RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release - -cp ../../target/wasm32-unknown-unknown/release/pausable_base.wasm ../../res/ \ No newline at end of file diff --git a/examples/pausable-examples/pausable_base/src/lib.rs b/examples/pausable-examples/pausable_base/src/lib.rs deleted file mode 100644 index e0038a1..0000000 --- a/examples/pausable-examples/pausable_base/src/lib.rs +++ /dev/null @@ -1,111 +0,0 @@ -use near_plugins::Ownable; -use near_plugins::Pausable; -use near_sdk::near_bindgen; -use near_plugins_derive::{pause, if_paused}; -use borsh::{BorshSerialize, BorshDeserialize}; - -#[near_bindgen] -#[derive(Ownable, Pausable, Default, BorshSerialize, BorshDeserialize)] -struct Counter { - counter: u64, -} - -#[near_bindgen] -impl Counter { - /// Specify the owner of the contract in the constructor - #[init] - pub fn new() -> Self { - let mut contract = Self { counter: 0 }; - contract.owner_set(Some(near_sdk::env::predecessor_account_id())); - contract - } - - /// Function can be paused using feature name "increase_1" or "ALL" like: - /// `contract.pa_pause_feature("increase_1")` or `contract.pa_pause_feature("ALL")` - /// - /// If the function is paused, all calls to it will fail. Even calls started from owner or self. - #[pause] - pub fn increase_1(&mut self) { - self.counter += 1; - } - - /// Similar to `#[pause]` but use an explicit name for the feature. In this case the feature to be paused - /// is named "Increase by two". Note that trying to pause it using "increase_2" will not have any effect. - /// - /// This can be used to pause a subset of the methods at once without requiring to use "ALL". - #[pause(name = "Increase by two")] - pub fn increase_2(&mut self) { - self.counter += 2; - } - - /// Similar to `#[pause]` but owner or self can still call this method. Any subset of {self, owner} can be specified. - #[pause(except(owner, self))] - pub fn increase_4(&mut self) { - self.counter += 4; - } - - /// This method can only be called when "increase_1" is paused. Use this macro to create escape hatches when some - /// features are paused. Note that if "ALL" is specified the "increase_1" is considered to be paused. - #[if_paused(name = "increase_1")] - pub fn decrease_1(&mut self) { - self.counter -= 1; - } - - /// Custom use of pause features. Only allow increasing the counter using `careful_increase` if it is below 10. - pub fn careful_increase(&mut self) { - if self.counter >= 10 { - assert!( - !self.pa_is_paused("INCREASE_BIG".to_string()), - "Method paused for large values of counter" - ); - } - - self.counter += 1; - } - - pub fn get_counter(&self) -> u64 { - self.counter - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - use near_sdk::AccountId; - use near_plugins_test_utils::*; - - const WASM_FILEPATH: &str = "../../target/wasm32-unknown-unknown/release/pausable_base.wasm"; - - #[tokio::test] - async fn base_scenario() { - let (contract_holder, contract) = get_contract(WASM_FILEPATH).await; - - assert!(call!(contract,"new").await); - - let next_owner = get_subaccount(&contract_holder, "next_owner").await; - assert!(call!(contract, "owner_set", &json!({"owner": next_owner.id()})).await); - let current_owner: Option:: = view!(contract, "owner_get"); - assert_eq!(current_owner.unwrap().as_str(), next_owner.id().as_str()); - - let alice = get_subaccount(&contract_holder, "alice").await; - - assert!(call!(&alice, contract, "increase_1").await); - check_counter(&contract, 1).await; - - assert!(!call!(&alice, contract, "pa_pause_feature", &json!({"key": "increase_1"})).await); - assert!(call!(&next_owner, contract, "pa_pause_feature", &json!({"key": "increase_1"})).await); - - assert!(!call!(&alice, contract, "increase_1").await); - check_counter(&contract, 1).await; - - assert!(!call!(&next_owner, contract, "increase_1").await); - check_counter(&contract, 1).await; - - assert!(!call!(&contract_holder, contract, "pa_unpause_feature", &json!({"key": "increase_1"})).await); - assert!(call!(&next_owner, contract, "pa_unpause_feature", &json!({"key": "increase_1"})).await); - - assert!(call!(&alice, contract, "increase_1").await); - - check_counter(&contract, 2).await; - } -} diff --git a/near-plugins/src/pausable.rs b/near-plugins/src/pausable.rs index 8007644..3258679 100644 --- a/near-plugins/src/pausable.rs +++ b/near-plugins/src/pausable.rs @@ -30,20 +30,59 @@ use serde::Serialize; use std::collections::HashSet; pub trait Pausable { - /// Key of storage slot with list of paused features. - /// By default b"__PAUSED__" is used. + /// Returns the key of the storage slot which contains the list of features that are paused. By + /// default `b"__PAUSED__"` is used. + /// + /// Attribute `paused_storage_key` can be used to set a different key: + /// + /// ```ignore + /// #[pausable(paused_storage_key="CUSTOM_KEY")] + /// struct Contract { /* ... */} + /// ``` fn pa_storage_key(&self) -> Vec; - /// Check if a feature is paused + /// Returns whether feature `key` is paused. fn pa_is_paused(&self, key: String) -> bool; - /// List of all current paused features + /// Returns all features that are currently paused. fn pa_all_paused(&self) -> Option>; - /// Pause specified feature. + /// Pauses feature `key`. This method fails if the caller has not been granted one of the access + /// control `manager_roles` passed to the `Pausable` plugin. + /// + /// If the method succeeds, the following event will be emitted: + /// + /// ```json + /// { + /// "standard":"Pausable", + /// "version":"1.0.0", + /// "event":"pause", + /// "data": + /// { + /// "by":"", + /// "key":"" + /// } + /// } + /// ``` fn pa_pause_feature(&mut self, key: String); - /// Unpause specified feature + /// Unpauses feature `key`. This method fails if the caller has not been granted one of the + /// access control `manager_roles` passed to the `Pausable` plugin. + /// + /// If the method succeeds, the following event will be emitted: + /// + /// ```json + /// { + /// "standard":"Pausable", + /// "version":"1.0.0", + /// "event":"unpause", + /// "data": + /// { + /// "by":"", + /// "key":"" + /// } + /// } + /// ``` fn pa_unpause_feature(&mut self, key: String); } diff --git a/near-plugins/tests/contracts/pausable/src/lib.rs b/near-plugins/tests/contracts/pausable/src/lib.rs index 6d598cc..30ba875 100644 --- a/near-plugins/tests/contracts/pausable/src/lib.rs +++ b/near-plugins/tests/contracts/pausable/src/lib.rs @@ -5,6 +5,8 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault}; +/// Define roles for access control of `Pausable` features. Accounts which are +/// granted a role are authorized to execute the corresponding action. #[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] #[serde(crate = "near_sdk::serde")] pub enum Role { @@ -55,6 +57,7 @@ impl Counter { contract } + /// Returns the value of the counter. pub fn get_counter(&self) -> u64 { self.counter } @@ -79,7 +82,8 @@ impl Counter { self.counter += 2; } - /// Similar to `#[pause]` but roles passed as argument may still successfully call this method. + /// Similar to `#[pause]` but roles passed as argument may still successfully call this method + /// even when the corresponding feature is paused. #[pause(except(roles(Role::Unrestricted4Increaser, Role::Unrestricted4Modifier)))] pub fn increase_4(&mut self) { self.counter += 4;