Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# 0.3.0 (TBD)

## BREAKING CHANGES:

### Pausable Plugin

- Replaced the combined `manager_roles` attribute with separate `pause_roles` and `unpause_roles` attributes for the `Pausable` plugin.
- This allows for separate permissions for pausing and unpausing features, enabling more granular access control.

To migrate from the previous version:
```rust
// Old format
#[pausable(manager_roles(Role::PauseManager))]

// New format
#[pausable(
pause_roles(Role::PauseManager),
unpause_roles(Role::UnpauseManager)
)]
```

See the [migration guide](docs/migrations/pausable-separate-roles.md) for more details.


# 0.2.0 (TBD)

## BREAKING CHANGES:
Expand Down
114 changes: 114 additions & 0 deletions docs/migrations/pausable-separate-roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Migrating to Separate Pause/Unpause Roles

This guide explains how to migrate your code to use the new `pause_roles` and `unpause_roles` attributes instead of the consolidated `manager_roles` attribute in the Pausable plugin.

## Changes Required

### Before

Previously, you would define permissions for both pausing and unpausing using a single attribute:

```rust
#[pausable(manager_roles(Role::PauseManager))]
struct Contract {
// Contract fields
}
```

This meant that any account with the `PauseManager` role could both pause and unpause features.

### After

Now, you need to specify permissions for pausing and unpausing separately:

```rust
#[pausable(
pause_roles(Role::PauseManager),
unpause_roles(Role::UnpauseManager)
)]
struct Contract {
// Contract fields
}
```

With this change, you can:
- Grant an account only the ability to pause features (emergency response)
- Grant a different account only the ability to unpause features (recovery process)
- Grant some accounts both abilities

## Step-by-Step Migration

1. **Update your Role enum** to include separate roles for pausing and unpausing, if desired:

```rust
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
// Previous role that could both pause and unpause
// PauseManager,
Copy link
Copy Markdown
Collaborator

@mitinarseny mitinarseny Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should never remove a variant or add one more in the middle of enums with #[derive(AccessControlRole)], since it uses bitflags to store permissions for each AccountId.
Please, fix the migration guide.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should never remove a variant or add one more in the middle of enums with #[derive(AccessControlRole)], since it uses bitflags to store permissions for each AccountId.
Please, fix the migration guide.

@r-near can you please also add a migration test for this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @r-near

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karim-en PR here: #163


// New separate roles
PauseManager, // Can only pause features
UnpauseManager, // Can only unpause features
// Other roles...
}
```

2. **Update the pausable attribute** to use the new format:

```rust
// Old format
// #[pausable(manager_roles(Role::PauseManager))]

// New format
#[pausable(
pause_roles(Role::PauseManager),
unpause_roles(Role::UnpauseManager)
)]
```

3. **Update contract initialization** to grant the appropriate roles:

```rust
#[init]
pub fn new(pause_manager: AccountId, unpause_manager: AccountId) -> Self {
let mut contract = Self {
// contract fields
};

// Make the contract itself super admin
contract.acl_init_super_admin(env::current_account_id());

// Grant pause role
contract.acl_grant_role(Role::PauseManager.into(), pause_manager);

// Grant unpause role (might be the same or different account)
contract.acl_grant_role(Role::UnpauseManager.into(), unpause_manager);

contract
}
```

4. **Update tests** to test both pause and unpause permissions separately.

## Example

Here's a complete example of a contract using the new separated roles:

```rust
#[access_control(role_type(Role))]
#[near(contract_state)]
#[derive(Pausable, PanicOnDefault)]
#[pausable(
pause_roles(Role::PauseManager, Role::EmergencyPauser),
unpause_roles(Role::UnpauseManager, Role::ServiceRestorer)
)]
pub struct Counter {
counter: u64,
}
```

In this example:
- Accounts with either `PauseManager` or `EmergencyPauser` roles can pause features
- Accounts with either `UnpauseManager` or `ServiceRestorer` roles can unpause features
- An account might have multiple roles (e.g., both pause and unpause capabilities)
21 changes: 14 additions & 7 deletions near-plugins-derive/src/pausable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ 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<String>,
/// Access control roles whose grantees may pause and unpause features.
manager_roles: PathList,
/// Access control roles whose grantees may pause features.
pause_roles: PathList,
/// Access control roles whose grantees may unpause features.
unpause_roles: PathList,
}

/// Generates the token stream that implements `Pausable`.
Expand All @@ -27,10 +29,15 @@ 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;
let pause_roles = opts.pause_roles;
let unpause_roles = opts.unpause_roles;
assert!(
manager_roles.len() > 0,
"Specify at least one role for manager_roles"
pause_roles.len() > 0,
"Specify at least one role for pause_roles"
);
assert!(
unpause_roles.len() > 0,
"Specify at least one role for unpause_roles"
);

let output = quote! {
Expand All @@ -53,7 +60,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
})
}

#[#cratename::access_control_any(roles(#(#manager_roles),*))]
#[#cratename::access_control_any(roles(#(#pause_roles),*))]
fn pa_pause_feature(&mut self, key: String) -> bool {
let mut paused_keys = self.pa_all_paused().unwrap_or_default();
let newly_paused = paused_keys.insert(key.clone());
Expand All @@ -80,7 +87,7 @@ pub fn derive_pausable(input: TokenStream) -> TokenStream {
true
}

#[#cratename::access_control_any(roles(#(#manager_roles),*))]
#[#cratename::access_control_any(roles(#(#unpause_roles),*))]
fn pa_unpause_feature(&mut self, key: String) -> bool {
let mut paused_keys = self.pa_all_paused().unwrap_or_default();
let was_paused = paused_keys.remove(&key);
Expand Down
24 changes: 16 additions & 8 deletions near-plugins-derive/tests/contracts/pausable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ use near_sdk::{env, near, AccountId, PanicOnDefault};
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
/// May pause and unpause features.
/// May pause features.
PauseManager,
/// May unpause features.
UnpauseManager,
/// May call `increase_4` even when it is paused.
Unrestricted4Increaser,
/// May call `decrease_4` even when `increase_4` is not paused.
Expand All @@ -23,7 +25,7 @@ pub enum Role {
#[access_control(role_type(Role))]
#[near(contract_state)]
#[derive(Pausable, PanicOnDefault)]
#[pausable(manager_roles(Role::PauseManager))]
#[pausable(pause_roles(Role::PauseManager), unpause_roles(Role::UnpauseManager))]
pub struct Counter {
counter: u64,
}
Expand All @@ -34,13 +36,12 @@ impl Counter {
///
/// * Making the contract itself super admin.
/// * Granting `Role::PauseManager` to the account id `pause_manager`.
/// * Granting `Role::UnpauseManager` to the account id `unpause_manager`.
///
/// For a general overview of access control, please refer to the `AccessControllable` plugin.
#[init]
pub fn new(pause_manager: AccountId) -> Self {
let mut contract = Self {
counter: 0,
};
pub fn new(pause_manager: AccountId, unpause_manager: AccountId) -> Self {
let mut contract = Self { counter: 0 };

// Make the contract itself super admin. This allows us to grant any role in the
// constructor.
Expand All @@ -51,7 +52,11 @@ impl Counter {

// Grant `Role::PauseManager` to the provided account.
let result = contract.acl_grant_role(Role::PauseManager.into(), pause_manager);
near_sdk::require!(Some(true) == result, "Failed to grant role");
near_sdk::require!(Some(true) == result, "Failed to grant pause role");

// Grant `Role::UnpauseManager` to the provided account.
let result = contract.acl_grant_role(Role::UnpauseManager.into(), unpause_manager);
near_sdk::require!(Some(true) == result, "Failed to grant unpause role");

contract
}
Expand Down Expand Up @@ -99,7 +104,10 @@ impl Counter {

/// Similar to `#[if_paused]` but roles passed as argument may successfully call the method even
/// when the feature is _not_ paused.
#[if_paused(name = "increase_4", except(roles(Role::Unrestricted4Decreaser)))]
#[if_paused(
name = "increase_4",
except(roles(Role::Unrestricted4Decreaser, Role::Unrestricted4Modifier))
)]
pub fn decrease_4(&mut self) {
self.counter -= 4;
}
Expand Down
Loading