访问控制
访问控制——也就是“谁被允许做这件事”——在智能合约的世界中极其重要。你的合约的访问控制可能决定了谁可以铸造代币、对提案进行投票、冻结转账等等。因此,*至关重要*的是理解你如何实现它,否则其他人可能会 窃取你的整个系统。
所有权和 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_role
和 revoke_role
函数的权限。 如果调用帐户具有相应的管理角色,则可以使用这些函数来授予或撤销角色。 多个角色可能具有相同的管理角色,以简化管理。 角色的管理员甚至可以是角色本身,这将导致具有该角色的帐户也能够授予和撤销它。
此机制可用于创建类似于组织结构图的复杂权限结构,但它也提供了一种管理更简单应用程序的简便方法。 AccessControl
包括一个特殊角色,称为 DEFAULT_ADMIN_ROLE
,它是所有角色的默认管理角色。 除非使用 _set_role_admin
选择新的管理角色,否则具有此角色的帐户将能够管理任何其他角色。
请注意,默认情况下,不会向任何帐户授予“minter”或“burner”角色。 我们假设你使用构造函数将默认管理角色设置为部署者的角色,或者你有一种不同的机制,可以确保你能够授予角色。 但是,由于这些角色的管理角色是默认管理角色,并且_该_角色被授予 msg::sender()
,因此同一帐户可以调用 grant_role
来授予铸造或销毁权限,并调用 revoke_role
来删除它。
动态角色分配通常是一种理想的属性,例如在对参与者的信任可能随时间变化的系统中。 它也可用于支持诸如 KYC 之类的用例,其中角色承担者的列表可能不是预先已知的,或者包含在单个交易中可能过于昂贵。