深入剖析Solmate库 #04: MultiRolesAuthority.sol

MultiRolesAuthority.sol 在 256 位位图的角色权限基础上,去掉 target 维度实现与目标合约无关的一维权限映射,并新增自定义 Authority 委托机制,支持统一角色管理与个别合约特殊策略的灵活组合。

一、概述

版本说明:[solmate]:main分支 [commit: 89365b8],[forge-std]:v1.15.0

源码链接https://github.com/RevelationOfTuring/solmate/blob/main/src/auth/authorities/MultiRolesAuthority.sol

MultiRolesAuthority 是 solmate 权限体系中的 与目标合约无关的多角色访问控制(RBAC) 实现,是 RolesAuthority 的简化增强版。

解决的核心问题:如何用极低的 gas 成本,管理"谁可以调用任意合约的哪个函数",同时支持为特定目标合约设置完全独立的自定义权限策略。

与 RolesAuthority 的关键区别

  • RolesAuthority 的权限映射是 (target, functionSig) 二维键——权限绑定到特定目标合约
  • MultiRolesAuthority 的权限映射是 (functionSig) 一维键——权限与目标合约无关,同一个函数签名对所有合约通用
  • MultiRolesAuthority 额外提供 getTargetCustomAuthority——为特定目标合约设置独立的 Authority,优先级最高

在 solmate 权限体系中的位置:

用户调用受保护函数
  → requiresAuth 修饰符
    → isAuthorized(msg.sender, msg.sig)
      → authority.canCall(user, target, sig)
        → MultiRolesAuthority.canCall()  ← 就是这个合约
          ├── [优先] getTargetCustomAuthority[target].canCall()  ← 自定义 Authority 委托
          ├── isCapabilityPublic[functionSig]                    ← 公开能力
          └── getUserRoles[user] & getRolesWithCapability[sig]   ← 角色位图 AND

二、适用场景

适合 不适合
多个合约共享同一套函数级权限(如所有合约的 mint() 统一管理) 需要为不同合约的同名函数设置不同角色权限
需要为个别目标合约设置完全独立的权限策略(自定义 Authority) 需要超过 256 个角色的超大型系统
角色数量有限、权限关系清晰的场景 需要角色层级/继承关系(如 admin > manager > user)
需要极致 gas 优化的链上权限判断 需要角色有效期/自动过期机制
需要公开函数(任何人可调用)的灵活配置 需要 (target, functionSig) 二维精细权限(应使用 RolesAuthority)
权限策略需要灵活委托/组合(通过自定义 Authority) 需要多签/时间锁等复杂治理

三、合约结构总览

MultiRolesAuthority is Auth, Authority
│
├── Events(4 个事件)
│   ├── UserRoleUpdated              ← 用户角色变更
│   ├── PublicCapabilityUpdated      ← 公开能力变更
│   ├── RoleCapabilityUpdated        ← 角色能力变更
│   └── TargetCustomAuthorityUpdated ← 目标合约自定义 Authority 变更
│
├── Constructor
│   └── constructor(_owner, _authority) → Auth(_owner, _authority)
│
├── Modifier(继承自 Auth)
│   └── requiresAuth                 ← 权限校验,保护所有管理函数
│
├── Storage(4 个映射)
│   ├── getTargetCustomAuthority     ← address → Authority(目标合约自定义权限)
│   ├── getUserRoles                 ← address → bytes32(用户角色位图)
│   ├── isCapabilityPublic           ← bytes4 → bool(函数是否公开)
│   └── getRolesWithCapability       ← bytes4 → bytes32(函数角色位图)
│
├── View Functions(3 个查询)
│   ├── doesUserHaveRole()           ← 查询用户是否拥有某角色
│   ├── doesRoleHaveCapability()     ← 查询角色是否拥有某函数能力
│   └── canCall()                    ← Authority 接口:综合权限判断(核心)
│
└── Admin Functions(4 个管理,均需 requiresAuth)
    ├── setTargetCustomAuthority()   ← 设置目标合约的自定义 Authority
    ├── setPublicCapability()        ← 设置函数是否公开
    ├── setUserRole()                ← 给用户分配/撤销角色
    └── setRoleCapability()          ← 设置角色对函数的能力

四、源码逐行解析

4.1 继承关系

contract MultiRolesAuthority is Auth, Authority { ... }
父合约/接口 提供的能力
Auth owner 状态变量、authority 状态变量、requiresAuth 修饰符、isAuthorized() 函数、setAuthority()transferOwnership()
Authority canCall() 接口定义,本合约需要 override 实现

设计决策:与 RolesAuthority 完全相同的双重角色设计——既是"被管理的合约"(自身函数受 Auth 保护),又是"权限提供者"(为其他合约提供 canCall 查询)。

4.2 Events

UserRoleUpdated

event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled);

作用:记录用户角色变更,方便链下系统追踪谁被授予或撤销了哪个角色。

参数 类型 indexed 含义
user address 被修改角色的用户地址
role uint8 角色编号(0~255)
enabled bool true = 授予角色,false = 撤销角色

触发时机setUserRole() 执行成功后。

PublicCapabilityUpdated

event PublicCapabilityUpdated(bytes4 indexed functionSig, bool enabled);

作用:记录函数公开状态变更。

参数 类型 indexed 含义
functionSig bytes4 函数选择器
enabled bool true = 设为公开,false = 取消公开

触发时机setPublicCapability() 执行成功后。

设计决策:与 RolesAuthority 的关键区别——没有 target 参数。因为 MultiRolesAuthority 的公开能力是与目标合约无关的,只按函数签名生效。

RoleCapabilityUpdated

event RoleCapabilityUpdated(uint8 indexed role, bytes4 indexed functionSig, bool enabled);

作用:记录角色能力变更。

参数 类型 indexed 含义
role uint8 角色编号(0~255)
functionSig bytes4 函数选择器
enabled bool true = 授予能力,false = 撤销能力

触发时机setRoleCapability() 执行成功后。

设计决策:同样没有 target 参数(对比 RolesAuthority 的 3 个 indexed 参数)。只用了 2 个 indexed,还有 1 个富余,但因为不需要 target 维度,保持极简。

TargetCustomAuthorityUpdated

event TargetCustomAuthorityUpdated(address indexed target, Authority indexed authority);

作用:记录目标合约的自定义 Authority 变更,这是 MultiRolesAuthority 独有的事件。

参数 类型 indexed 含义
target address 目标合约地址
authority Authority 新的自定义 Authority 合约地址(address(0) 表示清除)

触发时机setTargetCustomAuthority() 执行成功后。

设计决策:两个参数都是 indexed,方便链下按目标合约或按 Authority 合约过滤日志。

4.3 Constructor

constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}

作用:初始化合约,设置初始 owner 和 authority,所有逻辑透传给父合约 Auth。

参数 类型 含义
_owner address 合约所有者地址,拥有最高管理权限,可调用所有 requiresAuth 函数
_authority Authority 外部权限合约地址,可传 Authority(address(0)) 表示不使用外部授权,仅依赖 owner

设计决策

  • 构造函数体为空,所有初始化逻辑透传给 Auth
  • Auth 构造函数会设置 ownerauthority,并触发 OwnershipTransferredAuthorityUpdated 事件

4.4 Storage

getTargetCustomAuthority

mapping(address => Authority) public getTargetCustomAuthority;

作用:为特定目标合约设置独立的权限判断合约。在 canCall 中优先级最高——如果目标合约有自定义 Authority,直接委托给它判断,完全绕过角色位图和公开能力逻辑。

Key Value 含义
address(目标合约地址) Authority(自定义权限合约) 非零地址表示已设置自定义 Authority
示例:
  getTargetCustomAuthority[vaultAddress] = specialAuthority
  → 对 vault 的所有调用,权限判断完全委托给 specialAuthority.canCall()
  → 角色位图、公开能力对 vault 均不生效

  getTargetCustomAuthority[tokenAddress] = Authority(address(0))
  → token 没有自定义 Authority,走正常的角色/公开能力判断流程

设计决策

  • 这是 MultiRolesAuthority 相比 RolesAuthority 的核心新增能力
  • 实现了权限委托的可组合性:可以将某些合约的权限判断委托给完全不同的 Authority 实现
  • 典型用途:某个 Vault 合约有特殊的权限需求(如时间锁、多签),可以单独挂载一个自定义 Authority

getUserRoles

mapping(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)

设计决策:与 RolesAuthority 完全相同。1 个 storage slot 存 256 个角色。

isCapabilityPublic

mapping(bytes4 => bool) public isCapabilityPublic;

作用:标记某个函数签名是否对所有人公开(无需角色即可调用)。

Key Value 含义
bytes4(函数选择器) bool true = 该函数对所有人公开
示例:
  isCapabilityPublic[bytes4(keccak256("balanceOf(address)"))] = true
  → 所有合约的 balanceOf 函数对所有人开放(target 无关)

设计决策

  • 与 RolesAuthority 的关键区别:RolesAuthority 是 mapping(address => mapping(bytes4 => bool)),即 (target, sig) 二维键;MultiRolesAuthority 是 mapping(bytes4 => bool),仅按函数签名——一旦公开,对所有目标合约生效
  • 这正是"target agnostic"设计理念的体现

getRolesWithCapability

mapping(bytes4 => bytes32) public getRolesWithCapability;

作用:存储哪些角色可以调用某个函数,用 bytes32 位图表示。

Key Value 含义
bytes4(函数选择器) bytes32(256 位位图) 第 N 位为 1 表示角色 N 可调用该函数
示例:
  getRolesWithCapability[bytes4(keccak256("mint(address,uint256)"))] = 0x...0000_0110
  → role 1(bit1=1)和 role 2(bit2=1)拥有 mint 能力
  → 适用于所有目标合约的 mint 函数(target 无关)

设计决策

  • 与 RolesAuthority 的关键区别:RolesAuthority 是 mapping(address => mapping(bytes4 => bytes32)),即 (target, sig) 二维键;MultiRolesAuthority 只按函数签名
  • 这意味着:如果 role 1 被授予 mint 能力,那么 role 1 可以对所有挂载了此 Authority 的合约调用 mint

4.5 查询函数

doesUserHaveRole

function 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 不支持直接位移。

doesRoleHaveCapability

function doesRoleHaveCapability(uint8 role, bytes4 functionSig) public view virtual returns (bool) {
    return (uint256(getRolesWithCapability[functionSig]) >> role) & 1 != 0;
}

作用:查询某个角色是否有权调用指定函数。逻辑与 doesUserHaveRole 完全一致,只是数据源从用户角色位图变成了函数角色位图。

参数 类型 含义
role uint8 要查询的角色编号(0~255)
functionSig bytes4 目标函数选择器
返回值 bool true = 该角色有权调用该函数

设计决策

  • 与 RolesAuthority 的区别:没有 target 参数,因为能力映射是 target agnostic 的
  • 签名简化为 (role, functionSig) 而非 (role, target, functionSig)

canCall(核心函数)

function canCall(
    address user,
    address target,
    bytes4 functionSig
) public view virtual override returns (bool) {
    // 路径 1:检查目标合约是否有自定义 Authority
    Authority customAuthority = getTargetCustomAuthority[target];

    // 若自定义 Authority 存在(非零地址),完全委托给它判断
    if (address(customAuthority) != address(0)) return customAuthority.canCall(user, target, functionSig);

    // 路径 2 || 路径 3:
    // isCapabilityPublic[functionSig]  — 该函数是否公开
    // getUserRoles[user] & getRolesWithCapability[functionSig] — 用户角色与函数所需角色的位与
    // bytes32(0) != ... — 位与结果非零说明至少有一个角色匹配
    return
        isCapabilityPublic[functionSig] || bytes32(0) != getUserRoles[user] & getRolesWithCapability[functionSig];
}

作用Authority 接口的核心实现。判断某个用户是否有权调用某个合约的某个函数。三级判断逻辑,优先级递减。

参数 类型 含义
user address 调用者地址(通常是 msg.sender
target address 被调用的目标合约地址(通常是 address(this)
functionSig bytes4 被调用函数的选择器(通常是 msg.sig
返回值 bool true = 允许调用,false = 拒绝调用

三级判断逻辑

路径 1(自定义 Authority,优先级最高):
  getTargetCustomAuthority[target] != address(0)
  → 完全委托给自定义 Authority 判断,直接 return
  → 后续的角色/公开能力逻辑对该 target 完全不生效

路径 2(公开能力,短路优先):
  isCapabilityPublic[functionSig] == true
  → 该函数是公开的,任何人可调用,直接返回 true

路径 3(角色位图 AND):
  getUserRoles[user] & getRolesWithCapability[functionSig] != 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 → 有权限 ✅

设计决策

  • 自定义 Authority 优先:先查 getTargetCustomAuthority,命中后直接 return,不走位图逻辑。用 if 分支而非 || 短路,因为需要完全委托(不是"或"关系,而是"取代"关系)
  • 缓存到局部变量Authority customAuthority = getTargetCustomAuthority[target] 缓存 SLOAD 结果,避免在 address(customAuthority)customAuthority.canCall() 中重复读取(节省 ~100 gas warm SLOAD)
  • || 短路:公开能力命中后不再做位图运算,节省 gas
  • & 运算优先级高于 !=bytes32(0) != A & B 无需额外括号
  • O(1) 复杂度:一次 & 运算同时检查 256 个角色
  • 与 RolesAuthority 的核心区别:RolesAuthority 只有路径 2 和 3(无自定义 Authority),且位图查询是 (target, sig) 二维;MultiRolesAuthority 多了路径 1,且位图查询是 (sig) 一维

4.6 管理函数(均需 requiresAuth)

setTargetCustomAuthority

function setTargetCustomAuthority(address target, Authority customAuthority) public virtual requiresAuth {
    // 存储目标合约的自定义 Authority
    getTargetCustomAuthority[target] = customAuthority;

    emit TargetCustomAuthorityUpdated(target, customAuthority);
}

作用:为指定目标合约设置独立的自定义 Authority。设置后,canCall 中对该 target 的查询将完全绕过角色/公开能力逻辑,由自定义 Authority 全权决定。

参数 类型 含义
target address 目标合约地址
customAuthority Authority 自定义 Authority 合约地址,传 Authority(address(0)) 可清除,回退到角色/公开能力判断

设计决策

  • 这是 MultiRolesAuthority 独有的管理函数,RolesAuthority 没有
  • 允许灵活的权限委托组合:部分合约走统一角色管理,个别合约走特殊权限逻辑
  • 传入 address(0) 清除自定义 Authority 而非删除 mapping entry,gas 更优

setPublicCapability

function setPublicCapability(bytes4 functionSig, bool enabled) public virtual requiresAuth {
    // 更新公开能力状态
    isCapabilityPublic[functionSig] = enabled;

    emit PublicCapabilityUpdated(functionSig, enabled);
}

作用:设置某个函数签名是否为公开可调用。设为公开后,canCall 会短路返回 true,任何人无需角色即可调用。

参数 类型 含义
functionSig bytes4 函数选择器(如 Vault.deposit.selector
enabled bool true = 任何人可调用(公开),false = 需要角色才能调用

设计决策

  • 与 RolesAuthority 的区别:没有 target 参数,公开设置对所有挂载此 Authority 的合约生效
  • 需要谨慎使用——一旦 setPublicCapability(mint.selector, true),所有合约的 mint 都公开了

setUserRole

function setUserRole(
    address user,
    uint8 role,
    bool enabled
) public virtual requiresAuth {
    if (enabled) {
        // 授予角色:用按位或将对应 bit 置 1
        getUserRoles[user] |= bytes32(1 << role);
    } else {
        // 撤销角色:用按位与 + 取反将对应 bit 置 0
        getUserRoles[user] &= ~bytes32(1 << role);
    }

    emit UserRoleUpdated(user, role, enabled);
}

作用:给用户分配或撤销角色。通过位运算在用户角色位图中置位或清位。

参数 类型 含义
user address 要配置的用户地址
role uint8 角色编号(0~255)
enabled bool true = 分配角色,false = 撤销角色

位运算拆解

授予角色 3(enabled = true):
  1 << 3       = 1000          ← 生成掩码,只有 bit3 为 1
  原始位图       = 0101
  |= 1000      = 1101          ← OR:bit3 被置 1,其他不变

撤销角色 3(enabled = false):
  1 << 3       = 1000
  ~1000        = 0111          ← 取反:只有 bit3 为 0
  原始位图       = 1101
  &= 0111      = 0101          ← AND:bit3 被清 0,其他不变

setRoleCapability

function setRoleCapability(
    uint8 role,
    bytes4 functionSig,
    bool enabled
) public virtual requiresAuth {
    if (enabled) {
        // 授予能力:用按位或将对应 bit 置 1
        getRolesWithCapability[functionSig] |= bytes32(1 << role);
    } else {
        // 撤销能力:用按位与 + 取反将对应 bit 置 0
        getRolesWithCapability[functionSig] &= ~bytes32(1 << role);
    }

    emit RoleCapabilityUpdated(role, functionSig, enabled);
}

作用:为指定角色授予或撤销指定函数的调用能力。通过位运算在函数角色位图中置位或清位。

参数 类型 含义
role uint8 角色编号(0~255)
functionSig bytes4 函数选择器
enabled bool true = 授予能力,false = 撤销能力

位运算拆解

授予 role 2 对 mint 的能力(enabled = true):
  1 << 2       = 0100          ← 生成掩码,只有 bit2 为 1
  原始位图       = 1001
  |= 0100      = 1101          ← OR:bit2 被置 1,其他不变

撤销 role 2 对 mint 的能力(enabled = false):
  1 << 2       = 0100
  ~0100        = 1011          ← 取反:只有 bit2 为 0
  原始位图       = 1101
  &= 1011      = 1001          ← AND:bit2 被清 0,其他不变

设计决策

  • 与 RolesAuthority 的区别:没有 target 参数,能力设置对所有合约的同名函数生效
  • 位运算模式与 setUserRole 完全一致,保持代码一致性

五、完整调用流程图

5.1 权限判断流程(canCall 被调用时)

外部合约调用受保护函数
         │
         ▼
  requiresAuth 修饰符(Auth 合约)
  作用:校验 msg.sender 是否有权调用当前函数
         │
         ▼
  isAuthorized(msg.sender, msg.sig)
  作用:核心授权判断,先查 authority 再查 owner
         │
         ▼
  authority 是否为零地址?
         │
    ┌────┴────┐
   YES       NO
    │         │
    │         ▼
    │   authority.canCall(user, target, sig)
    │   作用:调用 MultiRolesAuthority 进行权限判断
    │         │
    │         ▼
    │   ┌──────────────────────────────────┐
    │   │ 路径1: getTargetCustomAuthority  │
    │   │        [target] != address(0) ?  │
    │   └──────┬──────────────┬────────────┘
    │         YES            NO
    │          │              │
    │          ▼              ▼
    │   customAuthority   ┌──────────────────────┐
    │   .canCall(...)     │ 路径2: isCapability   │
    │   完全委托判断        │ Public[functionSig]? │
    │          │          └─────┬──────────┬─────┘
    │          │               YES        NO
    │          │                │          │
    │          │                ▼          ▼
    │          │             return    路径3: getUserRoles[user]
    │          │              true     & getRolesWithCapability[sig]
    │          │                            │
    │          │                            ▼
    │          │                      ┌──────────┐
    │          │                      │ != 0x0 ? │
    │          │                      └──┬────┬──┘
    │          │                        YES   NO
    │          │                         │     │
    │          ▼                         ▼     ▼
    │   return result               return  return
    │                                true   false
    │                                        │
    └────────────────┬───────────────────────┘
                     ▼
          canCall 返回 false(或 authority 为零地址)?
          检查 user == owner?
                     │
                  ┌──┴──┐
                 YES   NO
                  │     │
                  ▼     ▼
               通过   revert("UNAUTHORIZED")

5.2 管理配置流程

管理员(owner 或被 authority 授权者)
         │
         ▼
    requiresAuth 校验通过
         │
    ┌────┴───────────────┬───────────────────┬────────────────────┐
    ▼                    ▼                   ▼                    ▼
setTarget            setPublic          setUserRole()        setRole
CustomAuthority()    Capability()       作用:分配/           Capability()
作用:设置目标       作用:设置           撤销用户角色         作用:设置角色
合约自定义Authority  函数是否公开                              对函数的能力
    │                    │                   │                    │
    ▼                    ▼                   ▼                    ▼
直接赋值             直接设置              位运算修改            位运算修改
Authority地址        bool值               用户角色位图          函数角色位图
(getTarget           (isCapability        (getUserRoles)       (getRolesWith
CustomAuthority)     Public)                                   Capability)
    │                    │                   │                    │
    ▼                    ▼                   ▼                    ▼
emit                 emit                 emit                  emit
TargetCustom         PublicCapability     UserRole              RoleCapability
AuthorityUpdated     Updated             Updated               Updated

5.3 典型部署与配置流程

1. deploy MultiRolesAuthority(deployer, Authority(address(0)))
   → 创建权限管理合约,deployer 为 owner,不使用外部 Authority
         │
2. deploy Vault(deployer, multiRolesAuthority)
   deploy Token(deployer, multiRolesAuthority)
   → 创建业务合约,挂载统一的权限合约
         │
3. authority.setRoleCapability(MINTER_ROLE, mint.selector, true)
   authority.setRoleCapability(ADMIN_ROLE, withdraw.selector, true)
   → 配置:哪些角色可以调用哪些函数(适用于所有挂载的合约)
         │
4. authority.setUserRole(alice, MINTER_ROLE, true)
   authority.setUserRole(bob, ADMIN_ROLE, true)
   → 给用户分配角色
         │
5.(可选)authority.setPublicCapability(deposit.selector, true)
   → 将 deposit 设为公开,所有合约的 deposit 均免权限
         │
6.(可选)authority.setTargetCustomAuthority(specialVault, timelockAuthority)
   → 对特殊 Vault 使用时间锁 Authority,绕过角色体系
         │
7. alice 调用 token.mint(bob, 100)
         │
   requiresAuth → isAuthorized → authority.canCall()
         │
   getTargetCustomAuthority[token] == address(0) → 不走自定义路径
   isCapabilityPublic[mint.selector] == false → 不是公开函数
   getUserRoles[alice] & getRolesWithCapability[mint.selector]
         │
   结果 != 0 → 通过 ✅

六、设计思想

6.1 极简主义

  • 整个合约约 120 行(含注释),有效逻辑不到 80 行
  • 没有角色名称映射、没有枚举,角色就是 0~255 的数字
  • 没有批量操作函数,保持接口最小化
  • 构造函数为空,逻辑完全透传给父合约

6.2 Gas 极致优化

操作 Gas 成本 原因
canCall 判断(无自定义 Authority) ~2 SLOAD + 1 AND 位图一次性检查 256 个角色,O(1)
canCall 判断(有自定义 Authority) ~1 SLOAD + 外部调用 自定义 Authority 优先,命中后直接返回
公开函数判断 ~1 SLOAD || 短路返回,跳过位图运算
角色分配/撤销 ~1 SLOAD + 1 SSTORE 单 slot 位运算

与 RolesAuthority 的 Gas 对比:

// RolesAuthority — 二维映射查询(2 个嵌套 mapping)
isCapabilityPublic[target][functionSig]               // 2 次 keccak256 计算 slot
getRolesWithCapability[target][functionSig]           // 2 次 keccak256 计算 slot

// MultiRolesAuthority — 一维映射查询(1 个 mapping)
isCapabilityPublic[functionSig]                       // 1 次 keccak256 计算 slot
getRolesWithCapability[functionSig]                   // 1 次 keccak256 计算 slot

// MultiRolesAuthority 在非自定义 Authority 路径上比 RolesAuthority 更省 gas
// 因为少了 target 维度的嵌套 mapping,减少了 keccak256 计算

6.3 可组合性与可扩展性

  • 实现 Authority 接口 → 可插入任何继承了 Auth 的合约
  • 继承 Auth → 自身管理也受权限保护
  • 自定义 Authority 委托 → 可将特定合约的权限判断委托给任意 Authority 实现,实现权限策略的组合
  • 所有函数标记 virtual → 子合约可自由 override 扩展
  • Target agnostic → 天然适合"一套权限管理多个合约"的场景

6.4 防御性设计

  • 管理函数全部用 requiresAuth 保护
  • 位运算天然安全:uint8 role 范围 0~255,与 bytes32 的 256 位完美匹配,不存在越界
  • 撤销权限用 &= ~mask,只影响目标位,不破坏其他角色设置
  • 自定义 Authority 通过地址非零检查避免对 address(0) 的外部调用

七、安全注意事项

风险 说明 建议
Target agnostic 的双刃剑 同一函数签名的权限对所有合约生效,如果两个合约有同名函数但需要不同权限,无法直接区分 对需要独立权限的合约,使用 setTargetCustomAuthority 设置自定义 Authority
自定义 Authority 完全接管 一旦设置了自定义 Authority,角色/公开能力对该 target 完全不生效 确保自定义 Authority 实现正确;清除时传入 address(0)
自定义 Authority 外部调用风险 canCall 中会外部调用自定义 Authority,若其恶意或有 bug 可能导致 revert 或消耗大量 gas 仅信任经过审计的 Authority 合约;注意重入风险(虽然 canCall 是 view 函数)
owner 单点风险 owner 可随意分配/撤销所有角色和权限 部署完成后将 owner 转移给多签钱包或 Timelock
无零地址校验 setUserRole 可给 address(0) 分配角色 业务层自行校验,或继承后 override 加校验
无过期机制 角色一旦分配永久有效 如需过期,需自行扩展
公开函数风险 setPublicCapability 对所有合约生效,影响范围更大 比 RolesAuthority 更需谨慎,因为没有 target 限定
Authority 死锁 若自身的 authority 合约异常,即使 owner 也可能无法调管理函数 参考 Auth.setAuthority 的特殊处理;或将 authority 设为 address(0)
函数签名冲突 不同合约的不同函数可能有相同的 4 字节选择器(概率极低但存在) 了解此风险,关键场景可用 setTargetCustomAuthority 隔离

八、与同类方案对比

特性 solmate MultiRolesAuthority solmate RolesAuthority OpenZeppelin AccessControl
权限维度 (functionSig) 一维 + 可选自定义 Authority (target, functionSig) 二维 角色级别,无函数级别
角色存储 bytes32 位图(1 slot / 用户) bytes32 位图(1 slot / 用户) mapping(role => mapping(user => bool))
角色数量 最多 256 最多 256 无限制(bytes32 哈希作 role ID)
角色标识 纯数字 0~255 纯数字 0~255 可读哈希 keccak256("ROLE")
权限判断 gas ~1-3 SLOAD + 1 AND ~2 SLOAD + 1 AND ~1 SLOAD
函数级权限 ✅ 原生支持(target agnostic) ✅ 原生支持(target specific) ❌ 需自行 hasRole 检查
自定义 Authority 委托 getTargetCustomAuthority
公开函数 ✅ 全局生效 ✅ 按 target 生效
角色层级 ✅(adminRole 机制)
适合场景 多合约共享权限 + 个别合约特殊处理 每个合约需独立权限配置 角色多、需层级、生态兼容
代码量 ~80 行 ~80 行 ~150 行

如何选择

需要为不同合约的同名函数设置不同权限?
  → 选 RolesAuthority(二维映射,精确到 target)

多个合约共享一套角色权限,个别合约需要特殊处理?
  → 选 MultiRolesAuthority(一维映射 + 自定义 Authority 委托)

需要角色层级、可读角色名、生态兼容性?
  → 选 OpenZeppelin AccessControl

九、实战:继承该合约编写业务合约

9.1 业务合约

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Auth, Authority} from "solmate/auth/Auth.sol";

/*
 * @title TokenVault — 使用 MultiRolesAuthority 做权限控制的代币金库合约
 * @notice 展示 MultiRolesAuthority 的典型用法:多合约共享角色体系
 */
contract TokenVault is Auth {
    // 角色常量
    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);
    }
}

9.2 部署与配置

import {MultiRolesAuthority} from "solmate/auth/authorities/MultiRolesAuthority.sol";

// 1. 部署权限合约
MultiRolesAuthority authority = new MultiRolesAuthority(deployer, Authority(address(0)));

// 2. 部署多个业务合约,共享同一个权限合约
TokenVault vault1 = new TokenVault(deployer, authority);
TokenVault vault2 = new TokenVault(deployer, authority);

// 3. 配置角色能力(对所有合约的同名函数生效)
authority.setRoleCapability(0, TokenVault.deposit.selector, true);
authority.setRoleCapability(1, TokenVault.withdraw.selector, true);
authority.setRoleCapability(2, TokenVault.emergencyWithdraw.selector, true);

// 4. 分配角色
authority.setUserRole(alice, 0, true);   // Alice 可对所有 vault 存款
authority.setUserRole(alice, 1, true);   // Alice 也可对所有 vault 取款
authority.setUserRole(bob, 0, true);     // Bob 只能对所有 vault 存款

// 5.(可选)将 deposit 设为公开
authority.setPublicCapability(TokenVault.deposit.selector, true);
// → vault1 和 vault2 的 deposit 都公开了

// 6.(可选)vault2 有特殊需求,设置自定义 Authority
authority.setTargetCustomAuthority(address(vault2), timelockAuthority);
// → vault2 的权限完全由 timelockAuthority 管理,角色体系对 vault2 不生效

// 7. 生产环境:将 owner 转给多签
authority.transferOwnership(multisigAddress);

9.3 调用验证

Alice 调用 vault1.withdraw(100):
  → requiresAuth
  → authority.canCall(alice, vault1, withdraw.selector)
  → getTargetCustomAuthority[vault1] == address(0) → 不走自定义路径
  → isCapabilityPublic[withdraw.selector] == false → 不是公开函数
  → getUserRoles[alice] = ...011(角色 0、1)
  → getRolesWithCapability[withdraw.selector] = ...010(角色 1)
  → AND = ...010 != 0 → 通过 ✅

Alice 调用 vault2.withdraw(100)(vault2 有自定义 Authority):
  → requiresAuth
  → authority.canCall(alice, vault2, withdraw.selector)
  → getTargetCustomAuthority[vault2] == timelockAuthority → 走自定义路径
  → timelockAuthority.canCall(alice, vault2, withdraw.selector)
  → 结果由 timelockAuthority 决定(角色体系不参与)

Bob 调用 vault1.withdraw(100):
  → requiresAuth
  → authority.canCall(bob, vault1, withdraw.selector)
  → getTargetCustomAuthority[vault1] == address(0) → 不走自定义路径
  → isCapabilityPublic[withdraw.selector] == false
  → getUserRoles[bob] = ...001(角色 0)
  → getRolesWithCapability[withdraw.selector] = ...010(角色 1)
  → AND = ...000 == 0 → 拒绝
  → bob != owner → revert("UNAUTHORIZED") ❌

十、测试实战

目标合约

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import {Auth, Authority} 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/MultiRolesAuthority.t.sol

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论