Some common functionality is required for many smart contracts. Examples are temporarily pausing certain features, staging and deploying a new version of the contract, and restricting access to contract methods. While such functionality is out of scope for an SDK like near-sdk-rs, ideally it is not implemented anew for every smart contract.

The most obvious benefits of an open-source library are reusability and the value it adds to the ecosystem. The smart contract features mentioned above can be tricky to implement and cumbersome to test. Without a library, developers might gloss over functionality that does not add business value but still is critical for security. As near-plugins is open-source and used by many developers, there are more engineering hours and eyeballs dedicated to it compared to a solution specific to a single smart contract.

Case study: A counter managing permissions with ACL

Let’s look at a case study to see how near-plugins can be useful to smart contract developers. We are building a Counter that stores its current value and has methods to increment, decrement, and reset the value. It is intentionally kept simple to allow us to focus on how near-plugins adds functionality. This is what the contract looks like prior to using any plugins:

#[near_bindgen]
impl Counter {
    #[init]
    pub fn new() -> Self {
        Self { value: 0 }
    }

    /// Anyone can retrieve the current value.
    pub fn value(&self) -> i64 {
        self.value
    }

    /// Increases the value of the counter by one.
    pub fn increment(&mut self) {
        self.value += 1;
    }

    /// Decreases the value of the counter by one.
    pub fn decrement(&mut self) {
        self.value -= 1;
    }

    /// Resets the value of the counter to zero.
    pub fn reset(&mut self) {
        self.value = 0;
    }
}

The final version of the code is available in this repository on github. The Counter example is inspired by near-examples/counter-rust.

Permissions

The contract methods defined above can be called by anyone since they are public and inside an implementation block marked with #[near_bindgen]. Using near-sdk-rs it is possible to restrict some methods such that they can be called only by the contract itself. Either by using #[private] or by not exposing the method publicly, as described in the documentation.

However, what if we wanted to implement more flexible permissions (e.g. allowing only some set of accounts to call a given function)? This is where the AccessControllable plugin comes in handy.

Managing permissions with ACL

ACL stands for access control lists and they are used in the following way within the AccessControllable plugin. The user defines the roles required for their use case as Rust enum variants. Then it is possible to restrict access to a method to accounts that have been granted roles. Restricting access is possible with one line of code, for example:

#[access_control_any(roles(Role::Decrementer))]
pub fn decrement(&mut self) {
    // ...
}

Let’s walk through it step by step to see how you can make your Near smart contract AccessControllable.

Step 1: Add near-plugins as a dependency

For now, near-plugins has not yet been published on crates.io. Still, the crate is ready for usage and it can be added as git dependency:

# Add `near-plugins` under `dependencies` in your Cargo.toml.

[dependencies]
near-plugins = { git = "https://github.com/aurora-is-near/near-plugins.git", tag = "v0.2.0" }

Step 2: Define roles

Every use case may require a different set of roles, so users may define their roles as variants of an enum. For the Counter example, we define the following roles:

#[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)]
#[serde(crate = "near_sdk::serde")]
pub enum Role {
    /// Grantees of this role may decrease the counter.
    Decrementer,
    /// Grantees of this role may reset the counter.
    Resetter,
}

Deriving the AccessControlRole trait prepares the Role enum for usage in the AccessControllable plugin.

Step 3: Make the contract AccessControllable

The contract is made AccessControllable by attaching the access_control attribute macro on the definition of the struct which represents the contract’s state. We pass our Role as argument to make the AccessControllable implementation aware of it:

#[access_control(role_type(Role))]
#[near_bindgen]
#[derive(PanicOnDefault, BorshDeserialize, BorshSerialize)]
pub struct Counter {
    value: i64,
}

Step 4: Restrict contract methods

Access to a contract method is restricted by attaching #[access_control_any] and providing the roles to be whitelisted as arguments:

#[near_bindgen]
impl Counter {
    // We must be inside an implementation block with `#[near-bindgen]`.

    /// Resets the value of the counter to zero.
    ///
    /// Only accounts that have been granted `Role::Resetter` may successfully call this method.
    /// If called by an account without this role, the method panics and state remains unchanged.
    #[access_control_any(roles(Role::Resetter))] // enables ACL for this method
    pub fn reset(&mut self) {
        self.value = 0;
    }

    /// By the way, it is also possible to restrict access to accounts that have been granted any of
    /// multiple roles. This is how the syntax looks.
    #[access_control_any(roles(Role::Decrementer, Role::Resetter))]
    pub fn no_op(&self) { }
}

Now the contract is set up for access control. The only step that is missing is granting roles to accounts, enabling them to call restricted methods.

Step 5: Grant permissions

In our contract’s constructor method new() we make the contract itself super admin:

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

The AccessControllable super admin is an admin for every role defined in the Role enum. For this example, it is sufficient to know that a super admin may grant and revoke every role. Making the contract itself super admin facilitates the setup procedure as well as testing. More detailed information on admin roles can be found in the documentation of the AccessControllable trait.

To grant the Resetter role to the account alice.near, the contract can call the following function on itself:

/// See `AccessControllable::acl_grant_role` for details.
acl_grant_role("Resetter", "alice.near");

The AccessControllable trait provides many more methods to administer ACL permissions. After following the steps above, all of them are automatically implemented for a contract using the AccessControllable plugin.

Done!

The steps above are sufficient to add complex and configurable ACL permissions to a contract using near-plugins. At this point, alice.near is the only account which has been granted the Resetter role. This means that only alice.near may successfully call the contract’s reset() method.

The repo contains an integration test which verifies that AccessControllable was set up correctly for our Counter contract. Take a look at it to learn more about interacting with an AccessControllable contract. To run the test on-chain in a local sandbox, it suffices to clone the repo and execute the following command. This is made possible by near-workspaces-rs.

cargo test

Teaser: How it works internally

Using AccessControllable extends the contract state to store the permissions that have been granted. Moreover, the AccessControllable trait is implemented for the contract to enable administering permissions. When #[access_control_any(roles(...))] is attached to a method, near-plugin injects code that checks if the caller was granted any of the required roles. If not, a panic is generated which aborts the function call.

To learn about all the details, you can dive into the implementation of the AccessControllable macro.

A note on testing

The functionality provided by near-plugins is critical for security and we strive to test it exhaustively. In tests, we compile demo contracts for all plugins and deploy them on-chain in a local sandbox. Then we verify that using a particular plugin adds exactly the expected functionality to the contract. These tests and demo contracts can be found here.

Ready for production, though?

As mentioned earlier, near-plugins comes with the caveat of not yet being published to crates.io. Nevertheless, it is already used in some contracts on mainnet, e.g. contracts related to the Rainbow Bridge. Moreover, both Hacken and AuditOne audited near-plugins, awarding it high scores.

Conclusion

Using near-plugins, developers can add complex functionality to their smart contracts with just a few lines of code. Developers can focus on creating value for their users by relying on near-plugins for some cumbersome administrative tasks that are nevertheless critical for security. We are testing all plugins extensively and the near-plugins crate has been audited twice. We hope to contribute to the Near ecosystem by providing secure smart contract plugins which developers can build upon.

This article provides a step-by-step guide to using the AccessControllable plugin. In principle, using other plugins is similar. Head over to the repository and have a look at the documentation and tests to get started with other plugins.