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 直接管理和外部策略灵活授权两种权限模式?
答案是双重授权机制:
Owned.sol 中的单一管理员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() ← 外部权限判断入口
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 = 拒绝 |
设计决策:
Auth 和 Authority 定义在同一个文件中,方便一次 import 获取两者Authority 是 interface 而非 abstract contract,不引入任何状态或实现,最大化灵活性canCall 传入 target 参数,使得一个 Authority 实例可以同时为多个 Auth 合约提供权限判断(共享权限注册表)三个参数构成完整的权限三元组:谁 → 对哪个合约 → 做什么操作
采用接口而非具体实现的好处:
setAuthority() 更换,业务合约不用动┌─────────────────────────────────────────┐
│ Authority 实现(权限注册表) │
│ │
│ 记录:谁 → 对哪个合约 → 哪个函数 → 能否调用 │
└──────────┬──────────────┬───────────────┘
canCall 查询 canCall 查询
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ 合约 A │ │ 合约 B │
│ is Auth │ │ is Auth │
└──────────┘ └──────────┘
OwnershipTransferredevent OwnershipTransferred(address indexed user, address indexed newOwner);
作用:记录所有权转移,方便链下系统追踪合约控制权变更。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
user |
address |
✅ | 触发转移的调用者地址(构造时为 msg.sender,即部署者) |
newOwner |
address |
✅ | 新所有者地址 |
触发时机:
OwnershipTransferred(msg.sender, _owner) — msg.sender 是部署者transferOwnership() 执行成功后:OwnershipTransferred(msg.sender, newOwner)设计决策:
Owned 合约的区别:Owned 构造时 user 传 address(0)(表示从无到有),而 Auth 传 msg.sender(表示部署者设置了 owner)indexed,方便链下按旧/新 owner 过滤日志AuthorityUpdatedevent AuthorityUpdated(address indexed user, Authority indexed newAuthority);
作用:记录 Authority 合约更换,追踪权限策略的变更历史。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
user |
address |
✅ | 触发更新的调用者地址 |
newAuthority |
Authority |
✅ | 新的 Authority 合约地址(address(0) 表示禁用外部授权) |
触发时机:
AuthorityUpdated(msg.sender, _authority)setAuthority() 执行成功后:AuthorityUpdated(msg.sender, newAuthority)设计决策:
newAuthority 类型为 Authority 而非 address,增强类型安全,链下解析时也能区分是权限合约地址indexed 方便链下按操作者或新 Authority 地址过滤日志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"owneraddress public owner;
作用:存储当前合约所有者地址,是 isAuthorized 判断的兜底条件。public 自动生成 owner() getter。
| 类型 | 含义 |
|---|---|
address |
当前所有者地址。address(0) 表示合约无 owner,仅依赖 authority |
设计决策:与 Owned 完全一致的 address public owner,保持 solmate 权限体系的一致性。
authorityAuthority public authority;
作用:存储外部授权策略合约地址,是权限体系的"可插拔"部分。public 自动生成 authority() getter。
| 类型 | 含义 |
|---|---|
Authority |
外部授权合约地址。address(0) 表示不使用外部授权,仅依赖 owner |
设计决策:
Authority(接口类型)而非 address,编译期类型检查更严格requiresAuthmodifier 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 auth = authority; 节省约 100 gas(避免第二次 warm SLOAD)isAuthorized 也会 revert,即使 user == owner 也无法通过。这是有意的设计取舍——setAuthority 单独处理了这个问题internal view virtual:internal 不暴露给外部,view 不修改状态,virtual 允许 overridesetAuthorityfunction 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 → 避免死锁
设计决策:
isAuthorized 判断顺序故意相反:isAuthorized 是 authority 优先(优化 gas),setAuthority 是 owner 优先(防死锁)address(0) 是合法操作,表示清除外部授权,回退到纯 owner 模式transferOwnershipfunction 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 添加额外校验任意地址调用受 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"
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)
→ 权限策略无缝升级,业务合约无需重新部署
Auth 只负责"问"(调用 authority.canCall),不负责"答"(权限判断逻辑)Authority 实现中,业务合约零感知setAuthority,无需修改或重新部署业务合约target 参数区分)// 关键优化:缓存 authority 到局部变量
Authority auth = authority; // 1 次 SLOAD
// 后续 address(auth) 和 auth.canCall 都用局部变量,不再读 storage
abstract → 明确表达"请继承我"virtual → 子合约可自由 overrideAuthority 是 interface → 任意合约实现 canCall 即可接入isAuthorized 标记 internal virtual → 子合约可 override 自定义授权逻辑setAuthority 故意不用 requiresAuth,而是手动 owner 优先 判断transferOwnership 使用 requiresAuth(authority 优先),因为可以通过 setAuthority 先修复 authority死锁场景(如果 setAuthority 也用 requiresAuth):
authority 合约 revert
→ isAuthorized 整体 revert
→ owner 也无法调用 `requiresAuth` 保护的函数 → 永久死锁 ❌
实际设计(setAuthority 用 owner 优先):
authority 合约 revert
→ setAuthority 先判断 msg.sender == owner → 短路通过
→ owner 更换 authority → 恢复正常 ✅
"UNAUTHORIZED" → 不泄露内部状态setAuthority 的 require 无错误信息 → 非 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 |
| 维度 | 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 可以是任何逻辑) | 中(框架内灵活,但受角色模型约束) |
| 适合场景 | 自定义策略、多合约共享权限、追求极简 | 快速开发、标准角色系统、团队协作 |
需要外部权限策略(RBAC、白名单等)?
├── YES → 用 Auth + Authority 实现
└── NO → 只需 owner 保护?
├── YES → 用 Owned(更轻量)
└── NO → 可能不需要权限控制
// 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;
}
}
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);
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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
