RolesAuthority.sol 通过 256 位位图实现角色×能力的细粒度权限矩阵,支持公开函数、角色授权与自治管理三级访问控制。
版本说明:[solmate]:main分支 [commit: 89365b8],[forge-std]:v1.15.0
源码:https://github.com/RevelationOfTuring/solmate/blob/main/src/auth/authorities/RolesAuthority.sol
RolesAuthority 是 solmate 权限体系中的 基于角色的访问控制(RBAC) 具体实现。
解决的核心问题:如何用极低的 gas 成本,管理"谁可以调用哪个合约的哪个函数"。
Auth → 自身的管理函数也受权限保护(requiresAuth)Authority 接口 → 对外提供 canCall() 供其他 Auth 合约查询& 运算判断权限,支持最多 256 个角色(role 0~255)在 solmate 权限体系中的位置:
用户调用受保护函数
→ requiresAuth 修饰符
→ isAuthorized(msg.sender, msg.sig)
→ authority.canCall(user, target, sig)
→ RolesAuthority.canCall() ← 就是这个合约
| 适合 | 不适合 |
|---|---|
| 中小型 DeFi 协议的权限管理 | 需要超过 256 个角色的超大型系统 |
| 角色数量有限、权限关系清晰的场景 | 需要角色层级/继承关系(如 admin > manager > user) |
| 需要极致 gas 优化的链上权限判断 | 需要角色有效期/自动过期机制 |
| 多个合约共享同一套角色体系 | 需要多签/时间锁等复杂治理 |
| 需要公开函数(任何人可调用)的灵活配置 | 需要细粒度参数级别的权限控制 |
RolesAuthority is Auth, Authority
│
├── Events(3 个事件)
│ ├── UserRoleUpdated ← 用户角色变更
│ ├── PublicCapabilityUpdated ← 公开权限变更
│ └── RoleCapabilityUpdated ← 角色权限变更
│
├── Constructor
│ └── constructor(_owner, _authority) → Auth(_owner, _authority)
│
├── Modifier(继承自 Auth)
│ └── requiresAuth ← 权限校验,保护所有管理函数
│
├── Storage(3 个映射)
│ ├── getUserRoles ← address → bytes32(用户角色位图)
│ ├── isCapabilityPublic ← address → bytes4 → bool(函数是否公开)
│ └── getRolesWithCapability ← address → bytes4 → bytes32(函数角色位图)
│
├── View Functions(3 个查询)
│ ├── doesUserHaveRole() ← 查询用户是否拥有某角色
│ ├── doesRoleHaveCapability() ← 查询角色是否能调某函数
│ └── canCall() ← Authority 接口:综合权限判断(核心)
│
└── Admin Functions(3 个管理,均需 requiresAuth)
├── setPublicCapability() ← 设置函数公开/非公开
├── setRoleCapability() ← 设置角色对函数的权限
└── setUserRole() ← 给用户分配/撤销角色
contract RolesAuthority is Auth, Authority { ... }
| 父合约/接口 | 提供的能力 |
|---|---|
Auth |
owner 状态变量、authority 状态变量、requiresAuth 修饰符、isAuthorized() 函数、setAuthority()、transferOwnership() |
Authority |
canCall() 接口定义,本合约需要 override 实现 |
设计决策:同时继承两者,使得 RolesAuthority 既是一个"被管理的合约"(自身函数受 Auth 保护),又是一个"权限提供者"(为其他合约提供 canCall 查询)。
UserRoleUpdatedevent UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled);
作用:记录用户角色变更,方便链下系统(前端/索引服务)追踪谁被授予或撤销了哪个角色。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
user |
address |
✅ | 被修改角色的用户地址 |
role |
uint8 |
✅ | 角色编号(0~255) |
enabled |
bool |
❌ | true = 授予角色,false = 撤销角色 |
触发时机:setUserRole() 执行成功后。
PublicCapabilityUpdatedevent PublicCapabilityUpdated(address indexed target, bytes4 indexed functionSig, bool enabled);
作用:记录函数公开状态变更,方便监控哪些函数被设为公开或取消公开。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
target |
address |
✅ | 目标合约地址 |
functionSig |
bytes4 |
✅ | 函数选择器(如 vault.withdraw.selector) |
enabled |
bool |
❌ | true = 设为公开,false = 取消公开 |
触发时机:setPublicCapability() 执行成功后。
RoleCapabilityUpdatedevent RoleCapabilityUpdated(uint8 indexed role, address indexed target, bytes4 indexed functionSig, bool enabled);
作用:记录角色权限变更,追踪哪个角色被授予或撤销了对某个函数的调用权限。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
role |
uint8 |
✅ | 角色编号(0~255) |
target |
address |
✅ | 目标合约地址 |
functionSig |
bytes4 |
✅ | 函数选择器 |
enabled |
bool |
❌ | true = 授予权限,false = 撤销权限 |
触发时机:setRoleCapability() 执行成功后。
设计决策:
indexed 参数已达 EVM 事件上限,方便链下按角色/合约/函数过滤日志enabled 不加 indexed,因为 bool 过滤意义不大,放在 data 字段即可constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}
作用:初始化合约,设置初始 owner 和 authority,所有逻辑透传给父合约 Auth。
| 参数 | 类型 | 含义 |
|---|---|---|
_owner |
address |
合约所有者地址,拥有最高管理权限,可调用所有 requiresAuth 函数 |
_authority |
Authority |
外部权限合约地址,可传 Authority(address(0)) 表示不使用外部授权,仅依赖 owner |
设计决策:
AuthAuth 的构造函数会设置 owner、authority,并触发 OwnershipTransferred 和 AuthorityUpdated 事件getUserRolesmapping(address => bytes32) public getUserRoles;
作用:存储每个用户拥有的角色集合,用 bytes32 位图表示。
| Key | Value | 含义 |
|---|---|---|
address(用户地址) |
bytes32(256 位位图) |
第 N 位为 1 表示用户拥有角色 N |
示例:值为 0x...05(二进制 ...0101)
bit: ... bit3 bit2 bit1 bit0 ← 从右往左读
值: ... 0 1 0 1
→ 拥有角色 0(bit0=1)和角色 2(bit2=1)
设计决策:用 bytes32 位图而非 mapping(uint8 => bool) 或数组,因为:
& 运算,O(1) 判断权限isCapabilityPublicmapping(address => mapping(bytes4 => bool)) public isCapabilityPublic;
作用:标记某个合约的某个函数是否对所有人公开(无需角色即可调用)。
| Key1 | Key2 | Value | 含义 |
|---|---|---|---|
address(目标合约) |
bytes4(函数选择器) |
bool |
true = 该函数对所有人公开 |
设计决策:独立 bool 映射,在 canCall 中作为 || 短路条件优先判断,公开函数直接跳过位图运算,节省 gas。
getRolesWithCapabilitymapping(address => mapping(bytes4 => bytes32)) public getRolesWithCapability;
作用:存储哪些角色可以调用某个合约的某个函数,用 bytes32 位图表示。
| Key1 | Key2 | Value | 含义 |
|---|---|---|---|
address(目标合约) |
bytes4(函数选择器) |
bytes32(256 位位图) |
第 N 位为 1 表示角色 N 可调用该函数 |
设计决策:与 getUserRoles 格式完全一致的位图,确保可以直接 & 运算。
doesUserHaveRolefunction doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) {
return (uint256(getUserRoles[user]) >> role) & 1 != 0;
}
作用:查询某个用户是否拥有指定角色。通过位移取位操作,从用户角色位图中提取指定 bit 的值。
| 参数 | 类型 | 含义 |
|---|---|---|
user |
address |
要查询的用户地址 |
role |
uint8 |
要查询的角色编号(0~255) |
| 返回值 | bool |
true = 用户拥有该角色 |
位运算拆解(bit 位从右往左数,bit0 在最右边):
原始位图: bit3 bit2 bit1 bit0
0 1 0 1 (用户有角色 0 和 2)
查询 role=2:
原始: 0 1 0 1
>> 2: 0 0 0 1 ← bit2 移到 bit0 位置
& 1: 0 0 0 1 → != 0 → true ✅
查询 role=1:
原始: 0 1 0 1
>> 1: 0 0 1 0 ← bit1 移到 bit0 位置
& 1: 0 0 0 0 → == 0 → false ❌
设计决策:先转 uint256 再位移,因为 Solidity 的 >> 运算符需要数值类型,bytes32 不支持直接位移。
doesRoleHaveCapabilityfunction doesRoleHaveCapability(
uint8 role,
address target,
bytes4 functionSig
) public view virtual returns (bool) {
return (uint256(getRolesWithCapability[target][functionSig]) >> role) & 1 != 0;
}
作用:查询某个角色是否有权调用指定合约的指定函数。逻辑与 doesUserHaveRole 完全一致,只是数据源从用户角色位图变成了函数角色位图。
| 参数 | 类型 | 含义 |
|---|---|---|
role |
uint8 |
要查询的角色编号(0~255) |
target |
address |
目标合约地址 |
functionSig |
bytes4 |
目标函数选择器(如 Vault.withdraw.selector) |
| 返回值 | bool |
true = 该角色有权调用该函数 |
canCall(核心函数)function canCall(
address user,
address target,
bytes4 functionSig
) public view virtual override returns (bool) {
return
// 条件 1:函数是否公开
isCapabilityPublic[target][functionSig] ||
// 条件 2:用户角色位图 AND 函数角色位图,非零则有权限
// 注:用户角色位图 & 函数角色位图,只要 & 结果不全为 0,说明至少有一个角色既属于用户、又有权调用该函数 → 放行。
bytes32(0) != getUserRoles[user] & getRolesWithCapability[target][functionSig];
}
作用:Authority 接口的核心实现。判断某个用户是否有权调用某个合约的某个函数。这是外部 Auth 合约通过 isAuthorized() 间接调用的入口,是整个 RBAC 权限判断的最终裁决者。
| 参数 | 类型 | 含义 |
|---|---|---|
user |
address |
调用者地址(通常是 msg.sender) |
target |
address |
被调用的目标合约地址(通常是 address(this)) |
functionSig |
bytes4 |
被调用函数的选择器(通常是 msg.sig) |
| 返回值 | bool |
true = 允许调用,false = 拒绝调用 |
两级判断逻辑:
条件 1(短路优先):isCapabilityPublic[target][sig] == true
→ 该函数是公开的,任何人都能调用,直接返回 true,跳过位图运算
条件 2(位图 AND):getUserRoles[user] & getRolesWithCapability[target][sig] != bytes32(0)
→ 用户角色位图 AND 函数角色位图
→ 只要结果不全为 0,说明至少有一个角色既属于用户、又有权调用该函数 → 放行
完整示例:
用户位图(Alice 有角色 0、2):
bit: bit3 bit2 bit1 bit0
0 1 0 1
函数位图(角色 1、2 可调用 withdraw):
bit: bit3 bit2 bit1 bit0
0 1 1 0
AND 运算:
0 1 0 1 ← Alice
& 0 1 1 0 ← withdraw 允许的角色
─────────────────
0 1 0 0 ← 角色 2 重叠!!= 0 → 有权限 ✅
设计决策:
|| 短路:公开函数跳过位图运算,节省 ~1 SLOAD 的 gas& 运算优先级高于 !=,所以 bytes32(0) != A & B 无需额外括号& 运算同时检查 256 个角色,O(1),无论系统有多少角色setPublicCapabilityfunction setPublicCapability(
address target,
bytes4 functionSig,
bool enabled
) public virtual requiresAuth {
// 直接设置布尔值
isCapabilityPublic[target][functionSig] = enabled;
emit PublicCapabilityUpdated(target, functionSig, enabled);
}
作用:设置某个合约的某个函数是否为公开可调用。设为公开后,canCall 会短路返回 true,任何人无需角色即可调用该函数。
| 参数 | 类型 | 含义 |
|---|---|---|
target |
address |
目标合约地址(要配置的合约) |
functionSig |
bytes4 |
函数选择器(如 Vault.deposit.selector) |
enabled |
bool |
true = 任何人可调用(公开),false = 需要角色才能调用 |
设计决策:最简单的布尔赋值。设为 true 后 canCall 第一个条件短路返回,跳过所有角色检查。
setRoleCapabilityfunction setRoleCapability(
uint8 role,
address target,
bytes4 functionSig,
bool enabled
) public virtual requiresAuth {
if (enabled) {
// 如果是授予权限:
// 将第 role 位置 1,授予该角色调用权限
getRolesWithCapability[target][functionSig] |= bytes32(1 << role);
} else {
// 如果是撤销权限:
// 将第 role 位清 0,撤销该角色调用权限
getRolesWithCapability[target][functionSig] &= ~bytes32(1 << role);
}
emit RoleCapabilityUpdated(role, target, functionSig, enabled);
}
作用:设置某个角色是否有权调用指定合约的指定函数。通过位运算在函数角色位图中置位或清位,控制"角色 → 函数"的映射关系。
| 参数 | 类型 | 含义 |
|---|---|---|
role |
uint8 |
角色编号(0~255) |
target |
address |
目标合约地址 |
functionSig |
bytes4 |
函数选择器 |
enabled |
bool |
true = 授予该角色调用权限,false = 撤销该角色调用权限 |
位运算拆解:
授予角色 2 调用权限(enabled = true):
1 << 2 = 0100 ← 生成掩码,只有 bit2 为 1
原始位图 = 1001
|= 0100 = 1101 ← OR:bit2 被置 1,其他不变
撤销角色 2 调用权限(enabled = false):
1 << 2 = 0100
~0100 = 1011 ← 取反:只有 bit2 为 0
原始位图 = 1101
&= 1011 = 1001 ← AND:bit2 被清 0,其他不变
设计决策:
|= 置位和 &= ~ 清位是经典的位运算惯用法,只影响目标位,不破坏其他角色的配置1 << role 中 role 为 uint8(0~255),与 bytes32 的 256 位完美匹配,不存在越界setUserRolefunction setUserRole(
address user,
uint8 role,
bool enabled
) public virtual requiresAuth {
if (enabled) {
// 如果是授予权限:
// 将用户角色位图的第 role 位置 1
getUserRoles[user] |= bytes32(1 << role);
} else {
// 如果是撤销权限:
// 将用户角色位图的第 role 位清 0
getUserRoles[user] &= ~bytes32(1 << role);
}
emit UserRoleUpdated(user, role, enabled);
}
作用:给用户分配或撤销角色。通过位运算在用户角色位图中置位或清位,控制"用户 → 角色"的映射关系。与 setRoleCapability 位运算逻辑完全相同,操作目标从函数角色位图变为用户角色位图。
| 参数 | 类型 | 含义 |
|---|---|---|
user |
address |
要配置的用户地址 |
role |
uint8 |
角色编号(0~255) |
enabled |
bool |
true = 分配角色,false = 撤销角色 |
设计决策:
setRoleCapability 使用相同的位运算模式,保持代码一致性address(0) 也可以被分配角色(需业务层自行防范)外部合约调用受保护函数
│
▼
requiresAuth 修饰符(Auth 合约)
作用:校验 msg.sender 是否有权调用当前函数
│
▼
isAuthorized(msg.sender, msg.sig)
作用:核心授权判断,先查 authority 再查 owner
│
▼
authority 是否为零地址?
│
┌────┴────┐
YES NO
│ │
│ ▼
│ authority.canCall(user, target, sig)
│ 作用:调用 RolesAuthority 判断角色权限
│ │
│ ▼
│ ┌─────────────────────────────┐
│ │ isCapabilityPublic[t][sig]? │
│ └──────┬──────────┬───────────┘
│ YES NO
│ │ │
│ ▼ ▼
│ return getUserRoles[user]
│ true & getRolesWithCapability[t][sig]
│ │
│ ▼
│ ┌──────────┐
│ │ != 0x0 ? │
│ └──┬────┬──┘
│ YES NO
│ │ │
│ ▼ ▼
│ return return
│ true false
│ │
└────────┬───────────────┘
▼
canCall 返回 false(或 authority 为零地址)?
检查 user == owner?
│
┌──┴──┐
YES NO
│ │
▼ ▼
通过 revert("UNAUTHORIZED")
管理员(owner 或被 authority 授权者)
│
▼
requiresAuth 校验通过
│
┌────┴─────────────┬──────────────────┐
▼ ▼ ▼
setUserRole() setRoleCapability() setPublicCapability()
作用:分配/ 作用:设置角色 作用:设置函数
撤销用户角色 对函数的调用权限 是否公开
│ │ │
▼ ▼ ▼
位运算修改 位运算修改 直接设置
用户角色位图 函数角色位图 bool 值
(getUserRoles) (getRolesWithCap) (isCapabilityPublic)
│ │ │
▼ ▼ ▼
emit emit emit
UserRoleUpdated RoleCapabilityUpdated PublicCapabilityUpdated
1. deploy RolesAuthority(deployer, Authority(address(0)))
→ 创建权限管理合约,deployer 为 owner
│
2. deploy Vault(deployer, rolesAuthority)
→ 创建业务合约,挂载权限合约
│
3. authority.setRoleCapability(ROLE_ID, vault, sig, true)
→ 配置哪些角色可以调用哪些函数
│
4. authority.setUserRole(alice, ROLE_ID, true)
→ 给用户分配角色
│
5. alice 调用 vault.protectedFunc()
│
requiresAuth → isAuthorized → authority.canCall()
│
getUserRoles[alice] & getRolesWithCapability[vault][sig]
│
结果 != 0 → 通过 ✅
| 操作 | Gas 成本 | 原因 |
|---|---|---|
| canCall 判断 | ~2 SLOAD + 1 AND | 位图一次性检查 256 个角色,O(1) |
| 公开函数判断 | ~1 SLOAD | \|\| 短路返回,跳过位图运算 |
| 角色分配/撤销 | ~1 SLOAD + 1 SSTORE | 单 slot 位运算,不涉及数组扩展 |
对比传统循环遍历方式:
// 传统做法 — O(n),最坏循环 256 次
for (uint8 i = 0; i < 256; i++) {
if (userHasRole[user][i] && roleCanCall[i][target][sig]) return true;
}
// 位图做法 — O(1),一条指令
getUserRoles[user] & getRolesWithCapability[target][functionSig] != 0
Authority 接口 → 可插入任何继承了 Auth 的合约Auth → 自身管理也受权限保护,可以实现"角色管理角色"virtual → 子合约可自由 override 扩展requiresAuth 保护,非授权者无法修改权限uint8 role 范围 0~255,与 bytes32 的 256 位完美匹配,不存在越界&= ~mask,只影响目标位,不破坏其他角色的设置| 风险 | 说明 | 建议 |
|---|---|---|
| owner 单点风险 | owner 可随意分配/撤销所有角色和权限 | 部署完成后将 owner 转移给多签钱包或 Timelock |
| 无零地址校验 | setUserRole 可给 address(0) 分配角色 |
业务层自行校验,或继承后 override 加校验 |
| 无过期机制 | 角色一旦分配永久有效,直到手动撤销 | 如需过期,需自行扩展(加 expiry 映射) |
| Authority 死锁 | 若 RolesAuthority 自身的 authority 合约异常(revert),即使 owner 也可能无法调管理函数 | 参考 Auth.setAuthority 的特殊处理;或将 authority 设为 address(0) 仅依赖 owner |
| 公开函数风险 | setPublicCapability(true) 后所有人可调用该函数 |
谨慎使用,配合事件监控,上线前审计 |
| indexed 参数上限 | RoleCapabilityUpdated 已用满 3 个 indexed |
若需更多过滤维度,需链下解析 data 字段 |
| 角色编号无语义 | 角色只是 0~255 的数字,链上无名称 | 在前端/文档/常量中维护角色编号与名称的映射表 |
| 无批量操作 | 一次只能设置一个角色/一个权限 | 如需批量操作,可封装 multicall 或写辅助合约 |
| 特性 | solmate RolesAuthority | OpenZeppelin AccessControl |
|---|---|---|
| 角色存储 | bytes32 位图(1 slot / 用户) | mapping(role => mapping(user => bool)) |
| 角色数量 | 最多 256 | 无限制(bytes32 哈希作为 role ID) |
| 角色标识 | 纯数字 0~255 | 可读哈希,如 keccak256("MINTER_ROLE") |
| 权限判断 gas | ~2 SLOAD + 1 AND(O(1),一次检查所有角色) | ~1 SLOAD(O(1)),但每个角色需单独查询 |
| 角色管理 | owner / authority 统一管理 | 每个角色有独立的 adminRole |
| 角色层级 | ❌ 无 | ✅ 支持(roleAdmin 机制) |
| 函数级权限 | ✅ 原生支持 (target, functionSig) 粒度 |
❌ 需自行在函数内用 hasRole 检查 |
| 公开函数 | ✅ isCapabilityPublic 内置支持 |
❌ 需自行实现 |
| 批量操作 | ❌ | ❌ |
| 代码量 | ~80 行 | ~150 行 |
| 适合场景 | Gas 敏感、角色少、需函数级控制 | 角色多、需层级管理、生态兼容性优先 |
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Auth, Authority} from "solmate/auth/Auth.sol";
/*
* @title Vault — 使用 RolesAuthority 做权限控制的金库合约
* @notice 存取款功能受角色保护,展示 RolesAuthority 的典型用法
*/
contract Vault is Auth {
// 角色常量(编译时内联,不占 storage,可读性好)
uint8 public constant ROLE_DEPOSITOR = 0;
uint8 public constant ROLE_WITHDRAWER = 1;
uint8 public constant ROLE_ADMIN = 2;
// 用户余额
mapping(address => uint256) public balances;
constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}
/// @notice 存款,需要 DEPOSITOR 角色或 owner
function deposit() external payable requiresAuth {
balances[msg.sender] += msg.value;
}
/// @notice 取款,需要 WITHDRAWER 角色或 owner
function withdraw(uint256 amount) external requiresAuth {
require(balances[msg.sender] >= amount, "INSUFFICIENT");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
/// @notice 紧急提取全部资金,需要 ADMIN 角色或 owner
function emergencyWithdraw() external requiresAuth {
payable(msg.sender).transfer(address(this).balance);
}
}
import {RolesAuthority} from "solmate/auth/authorities/RolesAuthority.sol";
// 1. 部署权限合约(deployer 为初始 owner)
RolesAuthority authority = new RolesAuthority(deployer, Authority(address(0)));
// 2. 部署业务合约,挂载权限合约
Vault vault = new Vault(deployer, authority);
// 3. 配置:哪些角色可以调用哪些函数
authority.setRoleCapability(0, address(vault), Vault.deposit.selector, true);
authority.setRoleCapability(1, address(vault), Vault.withdraw.selector, true);
authority.setRoleCapability(2, address(vault), Vault.emergencyWithdraw.selector, true);
// 4. 分配角色给用户
authority.setUserRole(alice, 0, true); // Alice 可存款
authority.setUserRole(alice, 1, true); // Alice 也可取款
authority.setUserRole(bob, 0, true); // Bob 只能存款
// 5.(可选)将 deposit 设为公开,任何人可存
authority.setPublicCapability(address(vault), Vault.deposit.selector, true);
// 6.(生产环境)将 owner 转给多签,消除单点风险
authority.transferOwnership(multisigAddress);
vault.transferOwnership(multisigAddress);
Alice 调用 vault.withdraw(100):
→ requiresAuth
→ authority.canCall(alice, vault, withdraw.selector)
→ getUserRoles[alice] = ...011(角色 0、1)
→ getRolesWithCapability[vault][withdraw] = ...010(角色 1)
→ AND = ...010 != 0 → 通过 ✅
Bob 调用 vault.withdraw(100):
→ requiresAuth
→ authority.canCall(bob, vault, withdraw.selector)
→ getUserRoles[bob] = ...001(角色 0)
→ getRolesWithCapability[vault][withdraw] = ...010(角色 1)
→ AND = ...000 == 0 → 拒绝
→ bob != owner → revert("UNAUTHORIZED") ❌
目标合约:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Auth} from "solmate/auth/Auth.sol";
/// @dev 被保护的目标合约,用于端到端验证
contract Target is Auth {
constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}
function protectedFunc() external view requiresAuth returns (bool) {
return true;
}
function anotherFunc() external view requiresAuth returns (bool) {
return true;
}
}
全部foundry测试合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/test/auth/authorities/RolesAuthority.t.sol
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
