Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
63 changes: 55 additions & 8 deletions docs/migrations/pausable-separate-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

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.

## Important Warning About Role Enums

When using `#[derive(AccessControlRole)]`, **never remove existing variants or add new variants in the middle of the enum**. The order of variants is critical because:

1. Each variant is mapped to specific bit positions in the permissions bitflags
2. Removing or reordering variants will cause existing permissions to be mapped incorrectly
3. This can result in accounts unintentionally gaining or losing access to features

**Always add new variants at the end of the enum to preserve existing permission mappings.**

## Changes Required

### Before
Expand Down Expand Up @@ -38,19 +48,32 @@ With this change, you can:

## Step-by-Step Migration

1. **Update your Role enum** to include separate roles for pausing and unpausing, if desired:
1. **Update your Role enum** by adding new roles at the end to preserve existing mappings:

```rust
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
// Keep existing variants in the same order
PauseManager, // Original role that could both pause and unpause
// Other existing roles...

// Add new roles at the end
UnpauseManager, // New role that can only unpause features
}
```

Alternatively, if you want to keep using your existing PauseManager role for pause permissions and create a new role for unpause:

```rust
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
// Previous role that could both pause and unpause
// PauseManager,
PauseManager, // Now used only for pausing
// Other existing roles...

// New separate roles
PauseManager, // Can only pause features
UnpauseManager, // Can only unpause features
// Other roles...
// Add new roles at the end
UnpauseManager, // New role for unpausing
}
```

Expand All @@ -67,6 +90,15 @@ With this change, you can:
)]
```

If you want to maintain backward compatibility where existing PauseManager accounts can still do both operations:

```rust
#[pausable(
pause_roles(Role::PauseManager),
unpause_roles(Role::PauseManager, Role::UnpauseManager)
)]
```

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

```rust
Expand All @@ -89,7 +121,22 @@ With this change, you can:
}
```

4. **Update tests** to test both pause and unpause permissions separately.
4. **Update or add a migration function** if you're upgrading an existing contract:

```rust
#[private]
pub fn migrate_pause_unpause_roles(&mut self) {
// Optionally grant UnpauseManager role to existing PauseManager accounts
// This gives existing managers the same capabilities they had before

let pause_managers = self.acl_get_grantees("PauseManager", 0, 100);
for account_id in pause_managers {
self.acl_grant_role(Role::UnpauseManager.into(), account_id);
}
}
```

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

## Example

Expand Down
21 changes: 21 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_new/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "pausable_new"
version = "0.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-plugins = { path = "../../../../near-plugins" }
near-sdk = "5.2"

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

[workspace]
8 changes: 8 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_new/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build:
cargo build --target wasm32-unknown-unknown --release

# Helpful for debugging. Requires `cargo-expand`.
expand:
cargo expand > expanded.rs

.PHONY: build expand
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.84.0"
components = ["clippy", "rustfmt"]
targets = [ "wasm32-unknown-unknown" ]
105 changes: 105 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_new/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use near_plugins::{
access_control, if_paused, pause, AccessControlRole, AccessControllable, Pausable,
};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, near, AccountId, PanicOnDefault};

/// Define roles for access control of `Pausable` features.
/// IMPORTANT: Keep the same order of existing variants to preserve permission mappings.
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
/// Now only used for pausing features
PauseManager,
/// Existing roles kept in the same order
Unrestricted4Increaser,
/// Existing roles kept in the same order
Unrestricted4Decreaser,
/// Existing roles kept in the same order
Unrestricted4Modifier,
/// Add new roles at the end
UnpauseManager,
}

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

#[near]
impl Counter {
/// Constructor initializes the counter to 0 and sets up ACL.
#[init]
pub fn new(pause_manager: AccountId, unpause_manager: AccountId) -> Self {
let mut contract = Self { counter: 0 };

// Make the contract itself super admin
near_sdk::require!(
contract.acl_init_super_admin(env::current_account_id()),
"Failed to initialize super admin",
);

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

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

contract
}

/// Returns the current counter value
#[pause]
pub fn get_counter(&self) -> u64 {
self.counter
}

/// Increments the counter - can be paused
#[pause]
pub fn increment(&mut self) {
self.counter += 1;
}

/// Similar to `#[pause]` but use an explicit name for the feature.
#[pause(name = "Increase by two")]
pub fn increase_2(&mut self) {
self.counter += 2;
}

/// 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;
}

/// This method can only be called when "increment" is paused.
#[if_paused(name = "increment")]
pub fn decrease_1(&mut self) {
self.counter -= 1;
}

/// For verifying that an account has a specific role
pub fn has_role(&self, role: String, account_id: AccountId) -> bool {
self.acl_has_role(role, account_id)
}

/// Migration function to maintain backward compatibility
/// Grants UnpauseManager role to existing PauseManager accounts
#[private]
pub fn migrate_pause_unpause_roles(&mut self) {
// Get all accounts with PauseManager role
let pause_managers = self.acl_get_grantees("PauseManager".to_string(), 0, 100);

// Grant UnpauseManager role to all existing PauseManager accounts
for account_id in pause_managers {
let result = self.acl_grant_role(Role::UnpauseManager.into(), account_id);
near_sdk::require!(result.is_some(), "Failed to grant UnpauseManager role");
}
}
}
21 changes: 21 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_old/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "pausable_old"
version = "0.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-plugins = { path = "../../../../near-plugins" }
near-sdk = "5.2"

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

[workspace]
8 changes: 8 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_old/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
build:
cargo build --target wasm32-unknown-unknown --release

# Helpful for debugging. Requires `cargo-expand`.
expand:
cargo expand > expanded.rs

.PHONY: build expand
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.84.0"
components = ["clippy", "rustfmt"]
targets = [ "wasm32-unknown-unknown" ]
85 changes: 85 additions & 0 deletions near-plugins-derive/tests/contracts/pausable_old/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use near_plugins::{
access_control, if_paused, pause, AccessControlRole, AccessControllable, Pausable,
};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::serde::{Deserialize, Serialize};
use near_sdk::{env, near, AccountId, PanicOnDefault};

/// Define roles for access control of `Pausable` features.
#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
/// The pause manager in the old style can both pause and unpause
PauseManager,
/// For testing except functionality
Unrestricted4Increaser,
/// For testing except functionality
Unrestricted4Decreaser,
/// For testing except functionality
Unrestricted4Modifier,
}

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

#[near]
impl Counter {
/// Constructor initializes the counter to 0 and sets up ACL.
#[init]
pub fn new(pause_manager: AccountId) -> Self {
let mut contract = Self { counter: 0 };

// Make the contract itself super admin
near_sdk::require!(
contract.acl_init_super_admin(env::current_account_id()),
"Failed to initialize super admin",
);

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

contract
}

/// Returns the current counter value
#[pause]
pub fn get_counter(&self) -> u64 {
self.counter
}

/// Increments the counter - can be paused
#[pause]
pub fn increment(&mut self) {
self.counter += 1;
}

/// Similar to `#[pause]` but use an explicit name for the feature.
#[pause(name = "Increase by two")]
pub fn increase_2(&mut self) {
self.counter += 2;
}

/// 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;
}

/// This method can only be called when "increment" is paused.
#[if_paused(name = "increment")]
pub fn decrease_1(&mut self) {
self.counter -= 1;
}

/// For verifying that an account has a specific role
pub fn has_role(&self, role: String, account_id: AccountId) -> bool {
self.acl_has_role(role, account_id)
}
}
Loading
Loading