diff --git a/README.md b/README.md index d8278b8..8c307f6 100644 --- a/README.md +++ b/README.md @@ -96,58 +96,106 @@ 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 Ownable. +Contract example using _Pausable_ plugin. Note that it requires the contract to be _AccessControllable_. ```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(Ownable, Pausable)] +#[derive(Pausable)] +#[pausable(manager_roles(Role::PauseManager))] struct Counter { counter: u64, } #[near_bindgen] impl Counter { - /// Specify the owner of the contract in the constructor + /// Initialize access control in the constructor. #[init] fn new() -> Self { - let mut contract = Self { counter: 0 }; - contract.owner_set(Some(near_sdk::env::predecessor_account_id())); + 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 started from owner or self. + /// 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. + /// 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". + /// 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 owner or self can still call this method. Any subset of {self, owner} can be specified. - #[pause(except(owner, self))] + /// 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. + /// 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!( diff --git a/near-plugins-derive/src/access_control_role.rs b/near-plugins-derive/src/access_control_role.rs index e510229..10dbc10 100644 --- a/near-plugins-derive/src/access_control_role.rs +++ b/near-plugins-derive/src/access_control_role.rs @@ -176,7 +176,7 @@ pub fn derive_access_control_role(input: TokenStream) -> TokenStream { } } - ::#cratename::bitflags::bitflags! { + #cratename::bitflags::bitflags! { /// Encodes permissions for roles and admins. #[derive(BorshDeserialize, BorshSerialize, Default)] struct #bitflags_type_ident: u128 { diff --git a/near-plugins-derive/src/access_controllable.rs b/near-plugins-derive/src/access_controllable.rs index 93e871f..90bc407 100644 --- a/near-plugins-derive/src/access_controllable.rs +++ b/near-plugins-derive/src/access_controllable.rs @@ -140,11 +140,11 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.insert(flag); self.add_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::SuperAdminAdded { + let event = #cratename::access_controllable::events::SuperAdminAdded { account: account_id.clone(), by: ::near_sdk::env::predecessor_account_id(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } is_new_super_admin @@ -177,11 +177,11 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.remove(flag); self.remove_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::SuperAdminRevoked { + let event = #cratename::access_controllable::events::SuperAdminRevoked { account: account_id.clone(), by: ::near_sdk::env::predecessor_account_id(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } was_super_admin @@ -208,12 +208,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.insert(flag); self.add_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::AdminAdded { + let event = #cratename::access_controllable::events::AdminAdded { role: role.into(), account: account_id.clone(), by: ::near_sdk::env::predecessor_account_id(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } is_new_admin @@ -259,12 +259,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.remove(flag); self.remove_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::AdminRevoked { + let event = #cratename::access_controllable::events::AdminRevoked { role: role.into(), account: account_id.clone(), by: ::near_sdk::env::predecessor_account_id(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } was_admin @@ -289,12 +289,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.insert(flag); self.add_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::RoleGranted { + let event = #cratename::access_controllable::events::RoleGranted { role: role.into(), by: ::near_sdk::env::predecessor_account_id(), to: account_id.clone(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } is_new_grantee @@ -324,12 +324,12 @@ pub fn access_controllable(attrs: TokenStream, item: TokenStream) -> TokenStream permissions.remove(flag); self.remove_bearer(flag, account_id); - let event = ::#cratename::access_controllable::events::RoleRevoked { + let event = #cratename::access_controllable::events::RoleRevoked { role: role.into(), from: account_id.clone(), by: ::near_sdk::env::predecessor_account_id(), }; - ::#cratename::events::AsEvent::emit(&event); + #cratename::events::AsEvent::emit(&event); } was_grantee @@ -575,7 +575,7 @@ pub fn access_control_any(attrs: TokenStream, item: TokenStream) -> TokenStream #function_name, __acl_any_roles, ); - env::panic_str(&message); + near_sdk::env::panic_str(&message); } }; diff --git a/near-plugins-derive/src/lib.rs b/near-plugins-derive/src/lib.rs index 5df5cee..691ee64 100644 --- a/near-plugins-derive/src/lib.rs +++ b/near-plugins-derive/src/lib.rs @@ -28,7 +28,7 @@ pub fn derive_fak_fallback(input: TokenStream) -> TokenStream { full_access_key_fallback::derive_fak_fallback(input) } -#[proc_macro_derive(Pausable)] +#[proc_macro_derive(Pausable, attributes(pausable))] pub fn derive_pausable(input: TokenStream) -> TokenStream { pausable::derive_pausable(input) } diff --git a/near-plugins-derive/src/pausable.rs b/near-plugins-derive/src/pausable.rs index 3f28b98..658fdf7 100644 --- a/near-plugins-derive/src/pausable.rs +++ b/near-plugins-derive/src/pausable.rs @@ -1,5 +1,6 @@ use crate::utils; use crate::utils::{cratename, is_near_bindgen_wrapped_or_marshall}; +use darling::util::PathList; use darling::{FromDeriveInput, FromMeta}; use proc_macro::{self, TokenStream}; use quote::quote; @@ -8,7 +9,11 @@ use syn::{parse, parse_macro_input, AttributeArgs, DeriveInput, ItemFn}; #[derive(FromDeriveInput, Default)] #[darling(default, attributes(pausable), forward_attrs(allow, doc, cfg))] struct Opts { + /// Storage key under which the set of paused features is stored. If it is + /// `None` the default value will be used. paused_storage_key: Option, + /// Access control roles whose grantees may pause and unpause features. + manager_roles: PathList, } pub fn derive_pausable(input: TokenStream) -> TokenStream { @@ -21,6 +26,11 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream { let paused_storage_key = opts .paused_storage_key .unwrap_or_else(|| "__PAUSE__".to_string()); + let manager_roles = opts.manager_roles; + assert!( + manager_roles.len() > 0, + "Specify at least one role for manager_roles" + ); let output = quote! { #[near_bindgen] @@ -42,7 +52,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream { }) } - #[#cratename::only(owner)] + #[#cratename::access_control_any(roles(#(#manager_roles),*))] fn pa_pause_feature(&mut self, key: String) { let mut paused_keys = self.pa_all_paused().unwrap_or_default(); paused_keys.insert(key.clone()); @@ -63,7 +73,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream { ); } - #[#cratename::only(owner)] + #[#cratename::access_control_any(roles(#(#manager_roles),*))] fn pa_unpause_feature(&mut self, key: String) { let mut paused_keys = self.pa_all_paused().unwrap_or_default(); paused_keys.remove(&key); @@ -96,11 +106,8 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream { #[derive(Default, FromMeta, Debug)] #[darling(default)] pub struct ExceptSubArgs { - #[darling(default)] - owner: bool, - #[darling(default)] - #[darling(rename = "self")] - _self: bool, + /// Grantees of these roles are exempted and may always call the method. + roles: PathList, } #[derive(Debug, FromMeta)] @@ -158,9 +165,9 @@ pub fn if_paused(attrs: TokenStream, item: TokenStream) -> TokenStream { let bypass_condition = get_bypass_condition(&args.except); let check_pause = quote!( - let mut check_paused = true; + let mut __check_paused = true; #bypass_condition - if check_paused { + if __check_paused { ::near_sdk::require!(self.pa_is_paused(#fn_name.to_string()), "Pausable: Method must be paused"); } ); @@ -169,28 +176,16 @@ pub fn if_paused(attrs: TokenStream, item: TokenStream) -> TokenStream { } fn get_bypass_condition(args: &ExceptSubArgs) -> proc_macro2::TokenStream { - let self_condition = if args._self { - quote!( - if ::near_sdk::env::predecessor_account_id() == ::near_sdk::env::current_account_id() { - __check_paused = false; - } - ) - } else { - quote!() - }; - - let owner_condition = if args.owner { - quote!( - if Some(::near_sdk::env::predecessor_account_id()) == self.owner_get() { - __check_paused = false; - } - ) - } else { - quote!() - }; - + let except_roles = args.roles.clone(); quote!( - #self_condition - #owner_condition + let __except_roles: Vec<&str> = vec![#(#except_roles.into()),*]; + let __except_roles: Vec = __except_roles.iter().map(|&x| x.into()).collect(); + let may_bypass = self.acl_has_any_role( + __except_roles, + ::near_sdk::env::predecessor_account_id() + ); + if may_bypass { + __check_paused = false; + } ) } diff --git a/near-plugins/src/pausable.rs b/near-plugins/src/pausable.rs index 05d9e17..c3beb03 100644 --- a/near-plugins/src/pausable.rs +++ b/near-plugins/src/pausable.rs @@ -90,62 +90,111 @@ impl AsEvent for Unpause { mod tests { use crate as near_plugins; use crate::test_utils::get_context; - use crate::{if_paused, pause, Ownable, Pausable}; + use crate::{ + access_control, if_paused, pause, AccessControlRole, AccessControllable, Pausable, + }; use std::collections::HashSet; use std::convert::TryInto; use near_sdk::borsh::BorshDeserialize; use near_sdk::borsh::BorshSerialize; - use near_sdk::{near_bindgen, testing_env, VMContext}; + use near_sdk::serde::{Deserialize, Serialize}; + use near_sdk::{near_bindgen, testing_env, AccountId, VMContext}; + + #[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(Ownable, Pausable)] + #[derive(Pausable)] + #[pausable(manager_roles(Role::PauseManager))] struct Counter { counter: u64, } #[near_bindgen] impl Counter { - /// Specify the owner of the contract in the constructor + /// Initializes Acl with the contract itself as super admin and grants + /// roles. #[init] fn new() -> Self { - let mut contract = Self { counter: 0 }; - contract.owner_set(Some(near_sdk::env::predecessor_account_id())); + 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, 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 started from owner or self. + /// 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. + /// 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". + /// 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 owner or self can still call this method. Any subset of {self, owner} can be specified. - #[pause(except(owner, self))] + /// 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. + /// 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!( @@ -158,12 +207,12 @@ mod tests { } } - /// Setup basic account. Owner of the account is `dave.test` + /// Sets up the contract and initializes access control, see + /// [`Counter::new`]. fn setup_basic() -> (Counter, VMContext) { let ctx = get_context(); testing_env!(ctx.clone()); - let mut counter = Counter::new(); - counter.owner_set(Some("dave.test".to_string().try_into().unwrap())); + let counter = Counter::new(); (counter, ctx) } @@ -181,7 +230,7 @@ mod tests { fn test_pause_feature() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_1".to_string()); @@ -195,10 +244,10 @@ mod tests { #[test] #[should_panic(expected = r#"Pausable: Method is paused"#)] - fn test_pause_feature_from_owner() { + fn test_pause_feature_from_pause_manager() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_1".to_string()); @@ -208,8 +257,10 @@ mod tests { } #[test] - #[should_panic(expected = r#"Ownable: Method must be called from owner"#)] - fn test_pause_only_owner() { + #[should_panic( + expected = r#"Insufficient permissions for method pa_pause_feature restricted by access control. Requires one of these roles: [\"PauseManager\"]"# + )] + fn test_pause_only_pause_manager() { let (mut counter, mut ctx) = setup_basic(); ctx.predecessor_account_id = "mallory.test".to_string().try_into().unwrap(); @@ -219,8 +270,10 @@ mod tests { } #[test] - #[should_panic(expected = r#"Ownable: Method must be called from owner"#)] - fn test_pause_only_owner_not_self() { + #[should_panic( + expected = r#"Insufficient permissions for method pa_pause_feature restricted by access control. Requires one of these roles: [\"PauseManager\"]"# + )] + fn test_pause_only_pause_manager_not_self() { let (mut counter, mut ctx) = setup_basic(); ctx.predecessor_account_id = "alice.test".to_string().try_into().unwrap(); @@ -234,7 +287,7 @@ mod tests { fn test_pause_with_all() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("ALL".to_string()); @@ -250,7 +303,7 @@ mod tests { fn test_not_paused_with_different_key() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("other_feature".to_string()); @@ -266,7 +319,7 @@ mod tests { fn test_work_after_unpause() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_1".to_string()); @@ -283,7 +336,7 @@ mod tests { fn test_paused_list() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("feature_a".to_string()); @@ -312,7 +365,7 @@ mod tests { fn test_is_paused() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); assert_eq!(counter.pa_is_paused("feature_a".to_string()), false); @@ -326,7 +379,7 @@ mod tests { fn test_pause_custom_name_ok() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_2".to_string()); @@ -343,7 +396,7 @@ mod tests { fn test_pause_custom_name_fail() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("Increase by two".to_string()); @@ -356,18 +409,21 @@ mod tests { } #[test] - fn test_pause_except_self_and_owner() { + fn test_pause_except_ok() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_4".to_string()); + ctx.predecessor_account_id = "brenda.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + counter.increase_4(); assert_eq!(counter.counter, 4); - ctx.predecessor_account_id = "alice.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "daniel.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.increase_4(); @@ -379,7 +435,7 @@ mod tests { fn test_pause_except_fail() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("increase_4".to_string()); @@ -410,7 +466,7 @@ mod tests { fn test_big_fail() { let (mut counter, mut ctx) = setup_basic(); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); counter.pa_pause_feature("INCREASE_BIG".to_string()); @@ -428,11 +484,24 @@ mod tests { #[test] fn test_escape_hatch_ok() { let (mut counter, mut ctx) = setup_basic(); - counter.increase_1(); - assert_eq!(counter.counter, 1); - ctx.predecessor_account_id = "dave.test".to_string().try_into().unwrap(); + + counter.increase_2(); + assert_eq!(counter.counter, 2); + + ctx.predecessor_account_id = "anna.test".to_string().try_into().unwrap(); testing_env!(ctx.clone()); + counter.pa_pause_feature("increase_1".to_string()); + + ctx.predecessor_account_id = "chris.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + + counter.decrease_1(); + assert_eq!(counter.counter, 1); + + ctx.predecessor_account_id = "daniel.test".to_string().try_into().unwrap(); + testing_env!(ctx.clone()); + counter.decrease_1(); assert_eq!(counter.counter, 0); }