メインコンテンツへスキップする

Modifiers

Modifiers ensure certain conditions are fulfilled prior to entering a function. By defining modifiers, you will reduce code redundancy (keep it DRY), and increase its readability as you will not have to add guards for each of your functions.
The Pair contract defines and uses a lock modifier that prevents reentrancy attacks. During initialization, it also ensures that the caller is the Factory, so it can be used as modifier.

1. Reentrancy Guard

To protect callable functions from reentrancy attacks, we will use the reentrancy guard modifier from Openbrush, which saves the lock status in storage (either ENTERED or NOT_ENTERED) to prevent reentrancy. In the ./contracts/pair/Cargo.toml file, add the "reentrancy_guard" feature to the Openbrush dependencies:

openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.0.0", default-features = false, features = ["psp22", "reentrancy_guard"] }

In the ./contracts/pair/lib.rs file, add an import statement, and reentrancy_guard as a Storage field:

...
use openbrush::{
contracts::{
ownable::*,
psp22::*,
reentrancy_guard,
},
traits::Storage,
};
...
#[ink(storage)]
#[derive(Default, Storage)]
pub struct PairContract {
#[storage_field]
psp22: psp22::Data,
#[storage_field]
guard: reentrancy_guard::Data,
#[storage_field]
pair: data::Data,
}
...

In the ./logics/Cargo.toml file, add the "reentrancy_guard" feature as an Openbrush dependency:

openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.0.0", default-features = false, features = ["psp22", "reentrancy_guard"] }

Modifiers should be added in the impl block on top of the function, as an attribute macro. In the ./logics/impls/pair/pair.rs file, add "reentrancy_guard", import statements, and modifier on top of mint, burn and swap as well as the Storage<reentrancy_guard::Data> trait bound:

...
use openbrush::{
contracts::{
psp22::*,
reentrancy_guard::*,
traits::psp22::PSP22Ref,
},
modifiers,
traits::{
AccountId,
Balance,
Storage,
Timestamp,
ZERO_ADDRESS,
},
};
...
impl<T: Storage<data::Data> + Storage<psp22::Data> + Storage<reentrancy_guard::Data>> Pair for T {
...
#[modifiers(non_reentrant)]
fn mint(&mut self, to: AccountId) -> Result<Balance, PairError> {
...
#[modifiers(non_reentrant)]
fn burn(&mut self, to: AccountId) -> Result<(Balance, Balance), PairError> {
...
#[modifiers(non_reentrant)]
fn swap(
...

Finally, the non_reentrant modifier returns ReentrancyGuardError. So let's impl From ReentrancyGuardError for PairError:

use openbrush::{
contracts::{
reentrancy_guard::*,
traits::{
psp22::PSP22Error,
},
},
traits::{
AccountId,
Balance,
Timestamp,
},
};
...
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum PairError {
PSP22Error(PSP22Error),
ReentrancyGuardError(ReentrancyGuardError),
...
impl From<ReentrancyGuardError> for PairError {
fn from(error: ReentrancyGuardError) -> Self {
PairError::ReentrancyGuardError(error)
}
}

2. Only Owner

In initialize there is a guard that ensures the caller is the Factory. We can use the ownable modifier to store the deployer address in storage, and restrict function access to this address only. In the ./contracts/pair/Cargo.toml file, add the "ownable" feature to the Openbrush dependency:

openbrush = { tag = "v2.3.0", git = "https://github.com/Supercolony-net/openbrush-contracts", default-features = false, features = ["psp22", "ownable", "reentrancy_guard"] }
...
use openbrush::{
contracts::{
ownable::*,
psp22::*,
reentrancy_guard,
},
traits::Storage,
};
...
#[ink(storage)]
#[derive(Default, SpreadAllocate, Storage)]
pub struct PairContract {
#[storage_field]
psp22: psp22::Data,
#[storage_field]
ownable: ownable::Data,
#[storage_field]
guard: reentrancy_guard::Data,
#[storage_field]
pair: data::Data,
}
...
impl Pair for PairContract {}

impl Ownable for PairContract {}
...
impl PairContract {
#[ink(constructor)]
pub fn new() -> Self {
let mut instance = Self::default();
let caller = instance.env().caller();
instance._init_with_owner(caller);
instance.pair.factory = caller;
instance
}
}

Update Internal Trait to psp22::Internal:

...
impl psp22::Internal for PairContract {
...
}

In the ./logics/Cargo.toml file, add the "ownable" feature to openbrush dependency:

openbrush = { git = "https://github.com/727-Ventures/openbrush-contracts", version = "3.0.0", default-features = false, features = ["psp22", "ownable", "reentrancy_guard"] }

Modifiers should be added in the impl block on top of the function, as an attribute macro. In the ./logics/impls/pair/pair.rs file, add "ownable", the import statements, and modifier on top of initialize, as well as the Storage<ownable::Data> trait bound:

...
use openbrush::{
contracts::{
ownable::*,
psp22::*,
reentrancy_guard::*,
traits::psp22::PSP22Ref,
},
modifiers,
traits::{
AccountId,
Balance,
Storage,
Timestamp,
ZERO_ADDRESS,
},
};
...
impl<
T: Storage<data::Data>
+ Storage<psp22::Data>
+ Storage<reentrancy_guard::Data>
+ Storage<ownable::Data>,
> Pair for T
{
...
#[modifiers(only_owner)]
fn initialize(&mut self, token_0: AccountId, token_1: AccountId) -> Result<(), PairError> {
...

Finally, the ownable modifier returns OwnableError. So let's impl From OwnableError for PairError:

use openbrush::{
contracts::{
reentrancy_guard::*,
traits::{
ownable::*,
psp22::PSP22Error,
},
},
traits::{
AccountId,
Balance,
Timestamp,
},
};
...
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum PairError {
PSP22Error(PSP22Error),
OwnableError(OwnableError),
ReentrancyGuardError(ReentrancyGuardError),
...
impl From<OwnableError> for PairError {
fn from(error: OwnableError) -> Self {
PairError::OwnableError(error)
}
}

And that's it!

By following along with these examples you will have implemented modifiers from Openbrush, and should also be able to implement your own by using information contained in this tutorial.
Check your Pair contract with (run in contract folder):

cargo contract build

It should now look like this branch