访问控制

访问控制——也就是“谁被允许做这件事”——在智能合约的世界中极其重要。你的合约的访问控制可能决定了谁可以铸造代币、对提案进行投票、冻结转账等等。因此,*至关重要*的是理解你如何实现它,否则其他人可能会 窃取你的整个系统

所有权和 Ownable

最常见和最基本的访问控制形式是_所有权_的概念:存在一个账户是合约的 owner,并且可以在其上执行管理任务。对于具有单个管理用户的合约来说,这种方法是完全合理的。

OpenZeppelin Contracts for Stylus 提供了 Ownable 用于在你的合约中实现所有权。

use openzeppelin_stylus::access::ownable::{self, Ownable};

#[derive(SolidityError, Debug)]
enum Error {
    UnauthorizedAccount(ownable::OwnableUnauthorizedAccount),
    InvalidOwner(ownable::OwnableInvalidOwner),
    // other custom errors...
}

impl From<ownable::Error> for Error {
    fn from(value: ownable::Error) -> Self {
        match value {
            ownable::Error::UnauthorizedAccount(e) => {
                Error::UnauthorizedAccount(e)
            }
            ownable::Error::InvalidOwner(e) => Error::InvalidOwner(e),
        }
    }
}

#[entrypoint]
#[storage]
struct OwnableExample {
    ownable: Ownable,
}

#[public]
#[implements(IOwnable<Error = Error>)]
impl MyContract {
    fn normal_thing(&self) {
        // anyone can call this normal_thing()
        // 任何人都可以调用 normal_thing()
    }

    fn special_thing(&mut self) -> Result<(), Error> {
        self.ownable.only_owner()?;

        // only the owner can call special_thing()!
        // 只有所有者可以调用 special_thing()!

        Ok(())
    }
}

#[public]
impl IOwnable for MyContract {
    type Error = Error;

    fn owner(&self) -> Address {
        self.ownable.owner()
    }

    fn transfer_ownership(
        &mut self,
        new_owner: Address,
    ) -> Result<(), Self::Error> {
        Ok(self.ownable.transfer_ownership(new_owner)?)
    }

    fn renounce_ownership(&mut self) -> Result<(), Self::Error> {
        Ok(self.ownable.renounce_ownership()?)
    }
}

在部署时,Ownable 合约的 owner 被设置为提供的 initial_owner 参数。

Ownable 还允许你:

  • 将所有权从所有者帐户 transfer_ownership 转移到一个新的帐户,并且

  • renounce_ownership 让所有者放弃此管理权限,这是在集中管理结束后的初始阶段之后的一种常见模式。

完全移除所有者将意味着受 only_owner 保护的管理任务将不再可调用!

请注意,合约也可以是另一个合约的所有者! 这为使用例如 Gnosis Safe、https://aragon.org[Aragon DAO] 或你创建的完全自定义的合约打开了大门。

通过这种方式,你可以使用_可组合性_来为你的合约添加额外的访问控制复杂性层。例如,你可以使用由你的项目负责人运行的 2-of-3 多重签名,而不是将单个常规 Ethereum 帐户(外部拥有的帐户,或 EOA)作为所有者。该领域的著名项目,例如 MakerDAO,使用与此类似的系统。

基于角色的访问控制

虽然_所有权_的简单性对于简单的系统或快速原型设计很有用,但通常需要不同级别的授权。你可能希望某个帐户有权禁止用户访问系统,但不能创建新代币。 基于角色的访问控制 (RBAC) 在这方面提供了灵活性。

本质上,我们将定义多个_角色_,每个角色都可以执行不同的操作集。一个帐户可能具有例如“moderator”、“minter”或“admin”角色,然后你将检查这些角色,而不是简单地使用 only_owner。可以通过 only_role 修饰符来强制执行此检查。另外,你将能够定义关于如何授予帐户角色、撤销角色等的规则。

大多数软件都使用基于角色的访问控制系统:一些用户是普通用户,一些用户可能是主管或经理,而少数用户通常具有管理权限。

使用 AccessControl

OpenZeppelin Contracts 提供了 AccessControl 用于实现基于角色的访问控制。它的用法很简单:对于你要定义的每个角色, 你将创建一个新的_角色标识符_,该标识符用于授予、撤销和检查帐户是否具有该角色。

这是一个在 ERC-20 代币中使用 AccessControl 定义“minter”角色的简单示例,该角色允许拥有该角色的帐户创建新代币。请注意,该示例不假设你构建合约的方式。

use openzeppelin_stylus::{
    access::control::{self, AccessControl, IAccessControl},
    token::erc20::{self, Erc20, IErc20},
};

#[derive(SolidityError, Debug)]
enum Error {
    UnauthorizedAccount(control::AccessControlUnauthorizedAccount),
    BadConfirmation(control::AccessControlBadConfirmation),
    InsufficientBalance(erc20::ERC20InsufficientBalance),
    InvalidSender(erc20::ERC20InvalidSender),
    InvalidReceiver(erc20::ERC20InvalidReceiver),
    InsufficientAllowance(erc20::ERC20InsufficientAllowance),
    InvalidSpender(erc20::ERC20InvalidSpender),
    InvalidApprover(erc20::ERC20InvalidApprover),
}

impl From<control::Error> for Error {
    fn from(value: control::Error) -> Self {
        match value {
            control::Error::UnauthorizedAccount(e) => {
                Error::UnauthorizedAccount(e)
            }
            control::Error::BadConfirmation(e) => Error::BadConfirmation(e),
        }
    }
}

impl From<erc20::Error> for Error {
    fn from(value: erc20::Error) -> Self {
        match value {
            erc20::Error::InsufficientBalance(e) => {
                Error::InsufficientBalance(e)
            }
            erc20::Error::InvalidSender(e) => Error::InvalidSender(e),
            erc20::Error::InvalidReceiver(e) => Error::InvalidReceiver(e),
            erc20::Error::InsufficientAllowance(e) => {
                Error::InsufficientAllowance(e)
            }
            erc20::Error::InvalidSpender(e) => Error::InvalidSpender(e),
            erc20::Error::InvalidApprover(e) => Error::InvalidApprover(e),
        }
    }
}

#[entrypoint]
#[storage]
struct Example {
    erc20: Erc20,
    access: AccessControl,
}

const MINTER_ROLE: [u8; 32] =
    keccak_const::Keccak256::new().update(b"MINTER_ROLE").finalize();

#[public]
#[implements(IErc20<Error = Error>, IAccessControl<Error = Error>)]
impl Example {
    fn mint(&mut self, to: Address, amount: U256) -> Result<(), Error> {
        self.access.only_role(MINTER_ROLE.into())?;
        self.erc20._mint(to, amount)?;
        Ok(())
    }
}

#[public]
impl IErc20 for Example {
    type Error = Error;

    fn total_supply(&self) -> U256 {
        self.erc20.total_supply()
    }

    fn balance_of(&self, account: Address) -> U256 {
        self.erc20.balance_of(account)
    }

    fn transfer(
        &mut self,
        to: Address,
        value: U256,
    ) -> Result<bool, Self::Error> {
        Ok(self.erc20.transfer(to, value)?)
    }

    fn allowance(&self, owner: Address, spender: Address) -> U256 {
        self.erc20.allowance(owner, spender)
    }

    fn approve(
        &mut self,
        spender: Address,
        value: U256,
    ) -> Result<bool, Self::Error> {
        Ok(self.erc20.approve(spender, value)?)
    }

    fn transfer_from(
        &mut self,
        from: Address,
        to: Address,
        value: U256,
    ) -> Result<bool, Self::Error> {
        Ok(self.erc20.transfer_from(from, to, value)?)
    }
}

#[public]
impl IAccessControl for Example {
    type Error = Error;

    fn has_role(&self, role: B256, account: Address) -> bool {
        self.access.has_role(role, account)
    }

    fn only_role(&self, role: B256) -> Result<(), Self::Error> {
        Ok(self.access.only_role(role)?)
    }

    fn get_role_admin(&self, role: B256) -> B256 {
        self.access.get_role_admin(role)
    }

    fn grant_role(
        &mut self,
        role: B256,
        account: Address,
    ) -> Result<(), Self::Error> {
        Ok(self.access.grant_role(role, account)?)
    }

    fn revoke_role(
        &mut self,
        role: B256,
        account: Address,
    ) -> Result<(), Self::Error> {
        Ok(self.access.revoke_role(role, account)?)
    }

    fn renounce_role(
        &mut self,
        role: B256,
        confirmation: Address,
    ) -> Result<(), Self::Error> {
        Ok(self.access.renounce_role(role, confirmation)?)
    }
}
在你的系统上使用 AccessControl 或复制粘贴本指南中的示例之前,请确保你完全理解它的工作原理。

虽然清晰而明确,但这并不是我们无法用 Ownable 实现的。 实际上,AccessControl 的优势在于需要细粒度权限的场景,可以通过定义_多个_角色来实现。

让我们通过定义一个“burner”角色来增强我们的 ERC-20 代币示例,该角色允许帐户销毁代币,并使用 only_role 修饰符:

use openzeppelin_stylus::{
    access::control::{self, AccessControl, IAccessControl},
    token::erc20::{self, Erc20, IErc20},
};

#[derive(SolidityError, Debug)]
enum Error {
    AccessControl(control::Error),
    Erc20(erc20::Error),
}

#[entrypoint]
#[storage]
struct Example {
    erc20: Erc20,
    access: AccessControl,
}

const MINTER_ROLE: [u8; 32] =
    keccak_const::Keccak256::new().update(b"MINTER_ROLE").finalize();

const BURNER_ROLE: [u8; 32] =
    keccak_const::Keccak256::new().update(b"BURNER_ROLE").finalize();

#[public]
#[implements(IErc20<Error = Error>, IAccessControl<Error = Error>)]
impl Example {
    fn mint(&mut self, to: Address, amount: U256) -> Result<(), Error> {
        self.access.only_role(MINTER_ROLE.into())?;
        self.erc20._mint(to, amount)?;
        Ok(())
    }

    fn burn(&mut self, from: Address, amount: U256) -> Result<(), Error> {
        self.access.only_role(BURNER_ROLE.into())?;
        self.erc20._burn(from, amount)?;
        Ok(())
    }
}

#[public]
impl IErc20 for Example {
    // ...
}

#[public]
impl IAccessControl for Example {
    // ...
}

太简洁了! 通过这种方式分离关注点,可以实现比使用更简单的_所有权_访问控制方法更细粒度的权限级别。限制系统每个组件能够执行的操作被称为 最小权限原则,并且是一种良好的安全实践。请注意,如果需要,每个帐户仍然可以具有多个角色。

授予和撤销角色

上面的 ERC-20 代币示例使用了 _grant_role,这是一个 internal 函数,在以编程方式分配角色(例如在构造期间)时很有用。但是,如果稍后我们想将“minter”角色授予其他帐户怎么办?

默认情况下,拥有角色的帐户无法授予或撤销其他帐户的角色:拥有角色所做的只是使 has_role 检查通过。 要动态地授予和撤销角色,你需要来自_角色的管理员_的帮助。

每个角色都有一个关联的管理角色,该角色授予调用 grant_rolerevoke_role 函数的权限。 如果调用帐户具有相应的管理角色,则可以使用这些函数来授予或撤销角色。 多个角色可能具有相同的管理角色,以简化管理。 角色的管理员甚至可以是角色本身,这将导致具有该角色的帐户也能够授予和撤销它。

此机制可用于创建类似于组织结构图的复杂权限结构,但它也提供了一种管理更简单应用程序的简便方法。 AccessControl 包括一个特殊角色,称为 DEFAULT_ADMIN_ROLE,它是所有角色的默认管理角色。 除非使用 _set_role_admin 选择新的管理角色,否则具有此角色的帐户将能够管理任何其他角色。

请注意,默认情况下,不会向任何帐户授予“minter”或“burner”角色。 我们假设你使用构造函数将默认管理角色设置为部署者的角色,或者你有一种不同的机制,可以确保你能够授予角色。 但是,由于这些角色的管理角色是默认管理角色,并且_该_角色被授予 msg::sender(),因此同一帐户可以调用 grant_role 来授予铸造或销毁权限,并调用 revoke_role 来删除它。

动态角色分配通常是一种理想的属性,例如在对参与者的信任可能随时间变化的系统中。 它也可用于支持诸如 KYC 之类的用例,其中角色承担者的列表可能不是预先已知的,或者包含在单个交易中可能过于昂贵。