深入剖析Solmate库

2026年04月08日更新 1 人订阅

深入剖析Solmate库 #02:Auth.sol

Auth.sol 是 Solmate 权限体系的核心合约,通过 owner 兜底 + Authority 外部策略的双重授权机制,将权限逻辑与业务逻辑彻底解耦。

一、概述

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

源码https://github.com/RevelationOfTuring/solmate/blob/main/src/auth/Auth.sol

Auth 是 solmate 权限体系中的 双重授权机制 抽象合约,同时也定义了 Authority 接口。

解决的核心问题:如何在保持极简的前提下,让合约同时支持owner 直接管理外部策略灵活授权两种权限模式?

答案是双重授权机制:

  • owner:最高权限,类似 Owned.sol 中的单一管理员
  • Authority:外部授权策略合约,通过 canCall() 接口判断任意地址是否有权调用某个函数

两者协同工作:Authority 负责日常的细粒度权限判断,owner 作为兜底和最终控制者。业务合约只需继承 Auth 并在函数上加 requiresAuth 修饰符,具体的权限规则完全由外部 Authority 合约决定——更换 Authority 就等于更换整套权限体系,业务合约本身无需任何修改。

二、适用场景

适合 不适合
DeFi 协议、DAO 等多角色权限系统 只需要一个管理员的简单场景(用 Owned.sol)
需要动态调整权限策略 权限逻辑固定不变的合约
多个合约共享统一权限管理 追求极致 gas 优化(Authority 外部调用有额外开销)
需要将权限逻辑与业务逻辑解耦 需要开箱即用的角色系统(用 OZ AccessControl)
作为基类被子合约或更复杂的权限系统继承 需要内置的角色枚举和管理功能

三、合约结构总览

Auth (abstract)
│
├── Events(2 个事件)
│   ├── OwnershipTransferred     ← 所有权转移
│   └── AuthorityUpdated         ← Authority 合约更换
│
├── Constructor
│   └── constructor(_owner, _authority) ← 设置初始 owner 和 authority + 触发事件
│
├── Modifier(1 个修饰符)
│   └── requiresAuth             ← 调用 isAuthorized 做双重权限校验
│
├── Storage(2 个状态变量)
│   ├── owner                    ← address,当前所有者(兜底权限)
│   └── authority                ← Authority,外部授权策略合约
│
├── Internal Functions(1 个)
│   └── isAuthorized()           ← 核心授权判断(authority.canCall || owner)
│
└── Admin Functions(2 个管理函数)
    ├── setAuthority()           ← 更换 Authority 合约(特殊权限校验)
    └── transferOwnership()      ← 一步转移所有权(需 requiresAuth)

Authority (interface)
│
└── canCall()                    ← 外部权限判断入口

四、源码逐行解析

4.1 合约声明与 Authority 接口

abstract contract Auth { ... }
关键词 含义
abstract 不可独立部署,必须被子合约继承后才能部署
interface Authority {
    function canCall(address user, address target, bytes4 functionSig) external view returns (bool);
}

作用:定义外部授权策略的通用接口。任何实现此接口的合约都可以作为 Auth 的授权后端。

参数 类型 含义
user address 待检查的调用者地址(通常是 msg.sender
target address 被调用的目标合约地址(即 Auth 合约自身,address(this)
functionSig bytes4 被调用函数的选择器(通常是 msg.sig
返回值 bool true = 授权通过,false = 拒绝

设计决策

  • AuthAuthority 定义在同一个文件中,方便一次 import 获取两者
  • Authorityinterface 而非 abstract contract,不引入任何状态或实现,最大化灵活性
  • canCall 传入 target 参数,使得一个 Authority 实例可以同时为多个 Auth 合约提供权限判断(共享权限注册表)

三个参数构成完整的权限三元组对哪个合约做什么操作

采用接口而非具体实现的好处:

  • 策略可插拔:白名单、RBAC、时间锁、多签,任何逻辑都可以
  • 热切换:通过 setAuthority() 更换,业务合约不用动
  • 多合约共享:多个 Auth 合约可以指向同一个 Authority,实现集中权限管理
┌─────────────────────────────────────────┐
│       Authority 实现(权限注册表)         │
│                                         │
│  记录:谁 → 对哪个合约 → 哪个函数 → 能否调用  │
└──────────┬──────────────┬───────────────┘
     canCall 查询     canCall 查询
           │              │
           ▼              ▼
     ┌──────────┐   ┌──────────┐
     │ 合约 A    │   │ 合约 B   │
     │ is Auth  │   │ is Auth  │
     └──────────┘   └──────────┘

4.2 Events

OwnershipTransferred

event OwnershipTransferred(address indexed user, address indexed newOwner);

作用:记录所有权转移,方便链下系统追踪合约控制权变更。

参数 类型 indexed 含义
user address 触发转移的调用者地址(构造时为 msg.sender,即部署者)
newOwner address 新所有者地址

触发时机

  1. 构造函数执行时:OwnershipTransferred(msg.sender, _owner)msg.sender 是部署者
  2. transferOwnership() 执行成功后:OwnershipTransferred(msg.sender, newOwner)

设计决策

  • 注意与 Owned 合约的区别:Owned 构造时 useraddress(0)(表示从无到有),而 Authmsg.sender(表示部署者设置了 owner)
  • 两个参数都加 indexed,方便链下按旧/新 owner 过滤日志

AuthorityUpdated

event AuthorityUpdated(address indexed user, Authority indexed newAuthority);

作用:记录 Authority 合约更换,追踪权限策略的变更历史。

参数 类型 indexed 含义
user address 触发更新的调用者地址
newAuthority Authority 新的 Authority 合约地址(address(0) 表示禁用外部授权)

触发时机

  1. 构造函数执行时:AuthorityUpdated(msg.sender, _authority)
  2. setAuthority() 执行成功后:AuthorityUpdated(msg.sender, newAuthority)

设计决策

  • newAuthority 类型为 Authority 而非 address,增强类型安全,链下解析时也能区分是权限合约地址
  • indexed 方便链下按操作者或新 Authority 地址过滤日志

4.3 Constructor

constructor(address _owner, Authority _authority) {
    // 设置初始 owner
    owner = _owner;
    // 设置初始 Authority(可传 Authority(address(0)) 表示不启用)
    authority = _authority;

    // 触发事件,记录初始状态
    // OwnershipTransferred 的 user 参数为 msg.sender(部署者),不是 address(0)
    // 这与 Owned.sol 不同——Owned 用 address(0) 表示"从无到有"
    emit OwnershipTransferred(msg.sender, _owner);
    emit AuthorityUpdated(msg.sender, _authority);
}

作用:初始化合约,设置初始 owner 和 authority,并分别触发事件。

参数 类型 含义
_owner address 初始所有者地址,拥有最高管理权限,是 isAuthorized 的兜底条件
_authority Authority 初始授权策略合约地址,可传 Authority(address(0)) 表示不启用外部授权,仅依赖 owner

设计决策

  • 不做零地址校验:_owner_authority 都可以是零地址,调用方需自行确保
  • 事件使用 msg.sender 而非 address(0):语义为"部署者设置了 owner/authority"
  • 分别触发两个事件而非合并为一个:保持事件粒度细,链下可以独立过滤

4.4 Storage

owner

address public owner;

作用:存储当前合约所有者地址,是 isAuthorized 判断的兜底条件。public 自动生成 owner() getter。

类型 含义
address 当前所有者地址。address(0) 表示合约无 owner,仅依赖 authority

设计决策:与 Owned 完全一致的 address public owner,保持 solmate 权限体系的一致性。

authority

Authority public authority;

作用:存储外部授权策略合约地址,是权限体系的"可插拔"部分。public 自动生成 authority() getter。

类型 含义
Authority 外部授权合约地址。address(0) 表示不使用外部授权,仅依赖 owner

设计决策

  • 类型为 Authority(接口类型)而非 address,编译期类型检查更严格

4.5 Modifier 与核心授权函数

requiresAuth

modifier requiresAuth() virtual {
    // 将 msg.sender(调用者)和 msg.sig(当前函数选择器)传入 isAuthorized
    // 未授权则 revert "UNAUTHORIZED"
    require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED");

    // 校验通过后执行被修饰的函数体
    _;
}

作用:权限校验修饰符,结合 msg.sender(调用者)和 msg.sig(函数选择器)调用 isAuthorized 做双重验证。

设计决策

  • 标记 virtual:子合约可 override 自定义校验逻辑
  • 传入 msg.sig 而非仅 msg.sender:支持函数级粒度的权限控制(Authority 可以根据函数选择器做不同判断)
  • Owned.onlyOwner 的核心区别:onlyOwner 只检查 owner,requiresAuth 检查 authority + owner 双通道

isAuthorized(核心函数)

function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) {
    // 将 storage 中的 authority 缓存到内存局部变量
    // 避免后续多次读取 storage,节省 ~100 gas(warm SLOAD)
    Authority auth = authority;

    // 短路求值(从左到右):
    //
    //   条件 A: address(auth) != address(0)
    //           → Authority 合约是否存在
    //
    //   条件 B: auth.canCall(user, address(this), functionSig)
    //           → Authority 是否允许该调用
    //
    //   条件 C: user == owner
    //           → 调用者是否为 owner
    //
    //   返回 (A && B) || C
    //
    //   执行路径:
    //   - Authority 存在且授权通过 → true(不检查 owner,省 gas)
    //   - Authority 不存在(A 为 false)→ 短路跳过 B → 检查 C
    //   - Authority 存在但拒绝(B 为 false)→ 检查 C
    //   - Authority 存在但 revert → 整个调用 revert,C 不会被执行!
    return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner;
}

作用:核心授权判断函数,决定某个用户是否有权调用本合约的指定函数。这是整个 Auth 权限体系的"裁决者"。

参数 类型 含义
user address 待检查的调用者地址(通常是 msg.sender
functionSig bytes4 待检查的函数选择器(通常是 msg.sig
返回值 bool true = 授权通过,false = 拒绝

设计决策

  • authority 优先、owner 兜底:大多数场景下用户通过 authority 授权,先检查 authority 可以在 authority 返回 true 时短路跳过 owner 比较,节省 gas
  • 缓存 authority 到局部变量Authority auth = authority; 节省约 100 gas(避免第二次 warm SLOAD)
  • 致命陷阱:如果 authority 合约 revert(而非返回 false),整个 isAuthorized 也会 revert,即使 user == owner 也无法通过。这是有意的设计取舍——setAuthority 单独处理了这个问题
  • 标记 internal view virtualinternal 不暴露给外部,view 不修改状态,virtual 允许 override

4.6 管理函数

setAuthority

function setAuthority(Authority newAuthority) public virtual {
    // 权限检查:owner 优先判断(廉价的地址比较)
    // 若不是 owner,再调用当前 authority.canCall 检查
    //
    // ⚠️ 故意不使用 requiresAuth 修饰符!
    // 原因:requiresAuth → isAuthorized → authority.canCall
    //       如果 authority 合约 revert,isAuthorized 整体 revert
    //       owner 也无法通过,造成死锁
    //
    // 而这里先判断 msg.sender == owner(短路求值):
    // owner 调用时直接通过,不触发 authority.canCall,避免死锁
    require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig));

    // 直接覆盖 authority(无旧值校验)
    authority = newAuthority;

    // 记录谁更换了 Authority 以及新地址
    emit AuthorityUpdated(msg.sender, newAuthority);
}

作用:更换 Authority 授权策略合约。这是整个 Auth 合约中最特殊的函数——没有使用 requiresAuth 修饰符,而是自己实现了权限校验逻辑。

参数 类型 含义
newAuthority Authority 新的 Authority 合约地址。可传 Authority(address(0)) 以禁用外部授权

为什么不用 requiresAuth

假设用了 requiresAuth(即 isAuthorized):
  → isAuthorized 中 authority 优先、owner 兜底
  → 如果 authority 合约 revert,isAuthorized 整体 revert
  → owner 也无法更换 authority → 死锁!

实际实现(owner 优先):
  → 先判断 msg.sender == owner
  → owner 直接短路通过,不调用可能 revert 的 authority
  → owner 始终能更换 authority → 避免死锁

设计决策

  • owner 优先判断:确保 owner 始终能更换 authority,即使当前 authority 合约异常(revert / 消耗大量 gas)
  • isAuthorized 判断顺序故意相反:isAuthorized 是 authority 优先(优化 gas),setAuthority 是 owner 优先(防死锁)
  • 不做零地址校验:传入 address(0) 是合法操作,表示清除外部授权,回退到纯 owner 模式

transferOwnership

function transferOwnership(address newOwner) public virtual requiresAuth {
    owner = newOwner;

    emit OwnershipTransferred(msg.sender, newOwner);
}

作用:一步转移所有权,将 owner 直接更新为 newOwner。使用 requiresAuth 修饰符,owner 和被 authority 授权的地址均可调用。

参数 类型 含义
newOwner address 新所有者地址。可以是任意地址,包括 address(0)(放弃所有权)

设计决策

  • 使用 requiresAuth 而非自定义校验:与 setAuthority 不同,这里不存在死锁风险(authority revert 时 owner 无法 transferOwnership,但可以先通过 setAuthority 修复 authority)
  • 不做零地址校验:与 Owned.transferOwnership 一致
  • 标记 virtual:子合约可 override 添加额外校验

五、完整调用流程图

5.1 权限判断流程(requiresAuth 被触发时)

任意地址调用受 requiresAuth 保护的函数
        │
        ▼
  isAuthorized(msg.sender, msg.sig)
        │
        ▼
  Authority 存在?(address(auth) != address(0))
        │
        ├── Yes ──→ auth.canCall(user, this, sig)
        │               │
        │               ├── true  → ✅ 放行(不检查 owner)
        │               ├── false → 继续检查 owner ↓
        │               └── revert → ❌ 整个调用 revert(owner 也被阻断!)
        │
        └── No(authority == address(0))
                │
                ▼
          user == owner ?
                │
                ├── Yes → ✅ 放行
                └── No  → ❌ revert "UNAUTHORIZED"

5.2 典型部署与配置流程

1. 实现一个 Authority(如 RolesAuthority)
   → 定义角色、权限映射规则
         │
2. deploy Authority(deployer, ...)
   → 创建权限策略合约
         │
3. 子合约继承 Auth(因为 Auth 是 abstract)
   contract MyContract is Auth {
       constructor(address _owner, Authority _authority) Auth(_owner, _authority) {}
       function adminFunc() external requiresAuth { ... }
   }
         │
4. deploy MyContract(deployer, authority)
   → owner = deployer, authority = authority 实例
   → 触发 OwnershipTransferred + AuthorityUpdated
         │
5. 在 Authority 中配置权限
   → authority.setRoleCapability(ROLE, myContract, sig, true)
   → authority.setUserRole(alice, ROLE, true)
         │
6. alice 调用 myContract.adminFunc()
   → requiresAuth → isAuthorized
   → authority.canCall(alice, myContract, sig) → true ✅
         │
7.(可选)更换 Authority 合约
   → owner 调用 myContract.setAuthority(newAuthority)
   → 权限策略无缝升级,业务合约无需重新部署

六、设计思想

6.1 关注点分离

  • Auth 只负责"问"(调用 authority.canCall),不负责"答"(权限判断逻辑)
  • 权限策略完全外置到 Authority 实现中,业务合约零感知
  • 更换权限策略只需 setAuthority,无需修改或重新部署业务合约
  • 一个 Authority 实例可同时服务多个 Auth 合约(通过 target 参数区分)

6.2 Gas 优化

// 关键优化:缓存 authority 到局部变量
Authority auth = authority; // 1 次 SLOAD
// 后续 address(auth) 和 auth.canCall 都用局部变量,不再读 storage

6.3 可组合性

  • 标记 abstract → 明确表达"请继承我"
  • 所有函数和修饰符标记 virtual → 子合约可自由 override
  • Authorityinterface → 任意合约实现 canCall 即可接入
  • isAuthorized 标记 internal virtual → 子合约可 override 自定义授权逻辑

6.4 防死锁设计

  • setAuthority 故意不用 requiresAuth,而是手动 owner 优先 判断
  • 确保 owner 在 authority 异常时始终能更换 authority
  • transferOwnership 使用 requiresAuth(authority 优先),因为可以通过 setAuthority 先修复 authority
死锁场景(如果 setAuthority 也用 requiresAuth):
  authority 合约 revert
    → isAuthorized 整体 revert
    → owner 也无法调用 `requiresAuth` 保护的函数 → 永久死锁 ❌

实际设计(setAuthority 用 owner 优先):
  authority 合约 revert
    → setAuthority 先判断 msg.sender == owner → 短路通过
    → owner 更换 authority → 恢复正常 ✅

6.5 防御性设计

  • 错误信息统一 "UNAUTHORIZED" → 不泄露内部状态
  • setAuthorityrequire 无错误信息 → 非 owner 且 authority 拒绝时 revert 无提示(gas 更省)

七、安全注意事项

风险 说明 建议
Authority revert 阻断 owner isAuthorized 中 Authority revert 会导致整个调用 revert,owner 也无法调用受保护函数 owner 通过 setAuthority 换掉异常 Authority(此函数不受影响)
Authority 信任风险 Authority 合约有漏洞可能导致未授权访问 审计 Authority 实现合约;使用成熟的 Authority 实现
零地址转移所有权 transferOwnership(address(0)) 永久丢失所有权 子合约 override 添加零地址校验
一步转移误操作 填错 newOwner 地址无法恢复 实现两步确认机制
setAuthority 无零地址确认 setAuthority(address(0)) 会清除 Authority,退化为纯 owner 模式 如果非预期行为,子合约 override 添加校验
Authority + owner 双重失效 owner 转移到错误地址 + Authority 也设为零地址 → 合约完全失控 生产环境 owner 使用多签钱包
msg.sig 信任 requiresAuth 中 msg.sig 可被 delegatecall 篡改上下文 避免在 delegatecall 场景中使用 Auth

八、与 OpenZeppelin AccessControl 对比

维度 Solmate Auth OpenZeppelin AccessControl
权限模型 owner + 外部 Authority 策略 内置角色系统(bytes32 role)
角色管理 全部委托给 Authority 合约 内置 grantRole / revokeRole / renounceRole
角色枚举 无(Authority 自行实现) 可通过 AccessControlEnumerable 扩展
权限粒度 函数级(bytes4 functionSig 角色级(bytes32 role
策略切换 setAuthority() 一键切换整套策略 需要逐个 grant/revoke
代码量 ~50 行 ~150+ 行
Gas 成本 更低(极简逻辑) 略高(内置角色管理)
复杂度 低(理解 Authority 接口即可) 中(需理解角色层级、admin role 等概念)
开箱即用 ❌ 需要自己实现 Authority ✅ 内置完整角色管理
灵活度 极高(Authority 可以是任何逻辑) 中(框架内灵活,但受角色模型约束)
适合场景 自定义策略、多合约共享权限、追求极简 快速开发、标准角色系统、团队协作

Auth vs Owned 的选择

需要外部权限策略(RBAC、白名单等)?
  ├── YES → 用 Auth + Authority 实现
  └── NO  → 只需 owner 保护?
              ├── YES → 用 Owned(更轻量)
              └── NO  → 可能不需要权限控制

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

9.1 业务合约

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

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

/*
 * @title TokenMinter — 使用 Auth 做权限控制的代币铸造合约
 * @notice 铸造和暂停功能受 requiresAuth 保护,展示 Auth 的典型用法
 */
contract TokenMinter is Auth {
    bool public paused;
    mapping(address => uint256) public balances;

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

    /// @notice 铸造代币,需要通过 authority 授权或是 owner
    function mint(address to, uint256 amount) external requiresAuth {
        require(!paused, "PAUSED");
        balances[to] += amount;
    }

    /// @notice 暂停合约,需要通过 authority 授权或是 owner
    function pause() external requiresAuth {
        paused = true;
    }

    /// @notice 恢复合约,需要通过 authority 授权或是 owner
    function unpause() external requiresAuth {
        paused = false;
    }
}

9.2 部署与配置

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

// 角色定义
uint8 constant ROLE_MINTER = 0;
uint8 constant ROLE_PAUSER = 1;

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

// 2. 部署业务合约,挂载权限合约
TokenMinter minter = new TokenMinter(deployer, authority);

// 3. 配置角色权限
authority.setRoleCapability(ROLE_MINTER, address(minter), TokenMinter.mint.selector, true);
authority.setRoleCapability(ROLE_PAUSER, address(minter), TokenMinter.pause.selector, true);
authority.setRoleCapability(ROLE_PAUSER, address(minter), TokenMinter.unpause.selector, true);

// 4. 分配角色
authority.setUserRole(alice, ROLE_MINTER, true);   // Alice 可铸造
authority.setUserRole(bob, ROLE_PAUSER, true);      // Bob 可暂停/恢复

// 5.(生产环境)转移所有权
minter.transferOwnership(multisigAddress);
authority.transferOwnership(multisigAddress);

9.3 调用验证

Alice 调用 minter.mint(charlie, 100):
  → requiresAuth
  → isAuthorized(alice, mint.selector)
  → Authority auth = authority(缓存)
  → address(auth) != address(0) → true
  → auth.canCall(alice, minter, mint.selector)
  → getUserRoles[alice] & getRolesWithCapability[minter][mint] != 0 → true ✅
  → 短路返回 true,不检查 owner

Bob 调用 minter.mint(charlie, 100):
  → requiresAuth
  → isAuthorized(bob, mint.selector)
  → auth.canCall(bob, minter, mint.selector)
  → getUserRoles[bob] & getRolesWithCapability[minter][mint] == 0 → false
  → bob == owner? → false
  → revert("UNAUTHORIZED") ❌

deployer(owner)调用 minter.mint(charlie, 100):
  → requiresAuth
  → isAuthorized(deployer, mint.selector)
  → auth.canCall(deployer, minter, mint.selector) → false(deployer 没有 MINTER 角色)
  → deployer == owner? → true ✅(owner 兜底)

十、测试实战

目标合约https://github.com/RevelationOfTuring/foundry-solmate/blob/main/src/auth/MockAuth.sol

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

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

contract MockAuth is Auth {
    constructor(address owner, Authority authority) Auth(owner, authority) {}

    function protectedFunction() external view requiresAuth returns (bool) {
        return true;
    }

    function unprotectedFunction() external pure returns (bool) {
        return true;
    }
}

contract MockAuthority is Authority {
    mapping(address => mapping(address => mapping(bytes4 => bool))) public permissions;

    function setCanCall(address user, address target, bytes4 functionSig, bool allowed) external {
        permissions[user][target][functionSig] = allowed;
    }

    function canCall(address user, address target, bytes4 functionSig) external view override returns (bool) {
        return permissions[user][target][functionSig];
    }
}

contract RevertingAuthority is Authority {
    function canCall(address, address, bytes4) external pure override returns (bool) {
        revert("AUTHORITY_REVERTED");
    }
}

全部 Foundry 测试合约https://github.com/RevelationOfTuring/foundry-solmate/blob/main/test/auth/Auth.t.sol

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

0 条评论

请先 登录 后评论