Owned.sol 是 Solmate 最简单的权限合约,实现单所有者访问控制与所有权转移,极简设计,够用就好,把扩展权留给开发者。
版本说明:[solmate]:main分支 [commit: 89365b8],[forge-std]:v1.15.0
源码:https://github.com/RevelationOfTuring/solmate/blob/main/src/auth/Owned.sol
Owned.sol 是 Solmate 中最简单的权限控制抽象合约,整个合约只做一件事:维护一个 owner 地址,只有 owner 才能调用被保护的函数。
virtual,子合约可自由重写它代表了 Solmate 的核心设计哲学:够用就好,不做多余的事,把选择权留给开发者。
| 适合 | 不适合 |
|---|---|
| 只需单个管理员的简单合约 | 需要多角色/多级权限管理 |
| 代理合约、工厂合约等只需 owner 保护的场景 | 需要外部权限合约(authority)做动态授权 |
| 对 gas 极度敏感,权限逻辑越少越好 | 需要两步确认(pending owner)的安全转移 |
| 原型/MVP 阶段快速开发 | 需要多签/时间锁等复杂治理 |
| 作为更复杂权限系统的基类被继承 | 需要零地址校验等防误操作机制 |
Owned (abstract contract)
│
├── 状态变量
│ └── owner : address ← 唯一的权限判断依据
│
├── 事件
│ └── OwnershipTransferred ← 所有权变更时触发
│
├── 修饰符
│ └── onlyOwner() ← 限制仅 owner 可调用
│
├── 构造函数
│ └── constructor(address) ← 设置初始 owner
│
└── 函数
└── transferOwnership(address) ← 一步转移所有权
abstract contract Owned { ... }
| 关键词 | 含义 |
|---|---|
abstract |
不可独立部署,必须被子合约继承后才能部署 |
设计决策:
abstract 表明这是一个 mixin(混入合约),设计目的就是被继承abstract 来明确表达"不应独立使用"的语义OwnershipTransferredevent OwnershipTransferred(address indexed user, address indexed newOwner);
作用:记录所有权转移,方便链下系统(前端/索引服务)追踪合约控制权变更。
| 参数 | 类型 | indexed | 含义 |
|---|---|---|---|
user |
address |
✅ | 原所有者地址(构造时为 address(0),表示从"无主"状态创建) |
newOwner |
address |
✅ | 新所有者地址 |
触发时机:
OwnershipTransferred(address(0), _owner) — 表示合约从"无主"状态转为初始 ownertransferOwnership() 执行成功后:OwnershipTransferred(msg.sender, newOwner) — 表示所有权从当前 owner 转移设计决策:
indexed,方便链下按旧 owner 或新 owner 过滤日志user 传 address(0) 而非 msg.sender,语义更清晰:表示"从无到有",而非"部署者转给 owner"OwnershipTransferred(previousOwner, newOwner) 命名不同,solmate 用 user 指代原 ownerconstructor(address _owner) {
// 将传入的 _owner 写入 storage,设置初始所有者
// 注意:不做零地址校验,如果传入 address(0) 则合约部署后即"无主"
owner = _owner;
// 触发事件,address(0) 作为 user 表示"初始设置"而非"从某人转移"
// 链下服务可通过监听此事件追踪合约的所有权历史
emit OwnershipTransferred(address(0), _owner);
}
作用:初始化合约,设置初始所有者并触发事件。
| 参数 | 类型 | 含义 |
|---|---|---|
_owner |
address |
初始所有者地址,拥有合约最高管理权限,可调用所有 onlyOwner 函数 |
设计决策:
_owner 可以是 address(0),此时合约诞生即无主,所有 onlyOwner 函数永远不可调用(除非子合约 override onlyOwner)msg.sender 校验:部署者和 owner 可以不是同一个人(如工厂合约代部署)owneraddress public owner;
作用:存储当前合约所有者地址,是所有权限判断的唯一依据。public 自动生成 owner() getter 函数。
| 类型 | 含义 |
|---|---|
address |
当前所有者地址。address(0) 表示合约无主 |
设计决策:
address,没有 pendingOwner、没有角色映射,是权限管理的最小化实现public 确保任何人都能查询当前 owner,提高透明度onlyOwnermodifier onlyOwner() virtual {
// 将调用者 msg.sender 与存储的 owner 比较
// 不相等则 revert,错误信息为字符串 "UNAUTHORIZED"
require(msg.sender == owner, "UNAUTHORIZED");
// 占位符:校验通过后,执行被修饰的函数体
_;
}
作用:访问控制修饰符,仅允许当前 owner 调用被修饰的函数。若 msg.sender != owner,则 revert 并返回 "UNAUTHORIZED"。
设计决策:
virtual:子合约可 override 以自定义权限逻辑(如多签校验、角色分级、时间锁等)require + 字符串而非 if + revert CustomError():代码更简洁,虽然 gas 稍高(~200 gas),但作为权限检查只在 revert 路径多消耗,正常路径无影响transferOwnershipfunction transferOwnership(address newOwner) public virtual onlyOwner {
// 直接覆盖 owner 为新地址
// 无零地址校验:传入 address(0) 等于放弃所有权且不可逆
// 无旧值校验:传入当前 owner 地址也不会报错,只是触发一次无意义的事件
owner = newOwner;
// 触发事件,记录是谁(msg.sender,即当前 owner)把所有权转给了谁
emit OwnershipTransferred(msg.sender, newOwner);
}
作用:一步转移所有权,将 owner 直接更新为 newOwner。调用后立即生效,原 owner 失去所有权限,新 owner 获得所有权限。
| 参数 | 类型 | 含义 |
|---|---|---|
newOwner |
address |
新所有者地址。可以是任意地址,包括 address(0)(放弃所有权) |
设计决策:
virtual:子合约可 override 添加额外校验(如禁止转给零地址、增加两步确认 pending owner 等)newOwner = address(0) 相当于放弃所有权(renounce),这是有意为之的功能,但不可逆newOwner 填错地址(如合约地址、EOA 私钥丢失),所有权将永久丢失msg.sender 而非 owner 作为事件的 user 参数:两者在执行时值相同(已通过 onlyOwner),但语义上 msg.sender 更直观外部调用受保护函数(如 transferOwnership)
│
▼
onlyOwner 修饰符
作用:校验 msg.sender 是否为 owner
│
▼
msg.sender == owner ?
│
┌────┴────┐
YES NO
│ │
▼ ▼
执行函数 revert("UNAUTHORIZED")
原始逻辑
│
▼
返回结果
当前 owner 调用 transferOwnership(newOwner)
│
▼
onlyOwner 校验通过
作用:确保只有当前 owner 才能转移所有权
│
▼
owner = newOwner
作用:直接覆盖 storage,立即生效
│
▼
emit OwnershipTransferred(msg.sender, newOwner)
作用:链下记录所有权变更历史
│
▼
结果:
- 原 owner(msg.sender)立即失去所有 onlyOwner 权限
- 新 owner(newOwner)立即获得所有 onlyOwner 权限
1. 子合约继承 Owned(因为 Owned 是 abstract,不可独立部署)
contract MyContract is Owned {
constructor(address _owner) Owned(_owner) {}
function adminFunc() external onlyOwner { ... }
}
│
2. deploy MyContract(deployer)
→ 构造函数:owner = deployer
→ 触发 OwnershipTransferred(address(0), deployer)
│
3. deployer 调用 adminFunc()
→ onlyOwner:msg.sender == owner ✅ → 执行
│
4. 非 owner 调用 adminFunc()
→ onlyOwner:msg.sender != owner → revert("UNAUTHORIZED") ❌
│
5.(可选)deployer 调用 transferOwnership(multisig)
→ owner 变为 multisig
→ deployer 不再是 owner,无法再调 adminFunc
pendingOwner、没有 renounceOwnership()、没有零地址校验| 操作 | Gas 成本 | 原因 |
|---|---|---|
| onlyOwner 校验 | ~1 SLOAD(~2100 gas cold / ~100 gas warm) | 只读取一个 storage slot,一次 == 比较 |
| transferOwnership | ~1 SLOAD + 1 SSTORE + 1 LOG | 读旧值校验 + 写新值 + 事件 |
对比 OpenZeppelin Ownable:
// OpenZeppelin — 额外有 _checkOwner() internal function + custom error
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
// Solmate — 直接在 modifier 内完成
modifier onlyOwner() virtual {
require(msg.sender == owner, "UNAUTHORIZED");
_;
}
solmate 用 msg.sender 而非 _msgSender()(省去一次函数调用),用 require 而非 if + revert,代码更紧凑。
abstract → 明确表达"请继承我"的语义onlyOwner 标记 virtual → 子合约可自定义权限逻辑transferOwnership 标记 virtual → 子合约可添加零地址校验、两步确认等onlyOwner 在 _; 之前执行校验 → 确保先检查权限再执行逻辑(checks-effects-interactions 的 checks 部分)transferOwnership 事件使用 msg.sender 而非旧 owner → 语义清晰,且避免在 override 场景下 owner 已被提前修改的风险"UNAUTHORIZED" 统一 → 不泄露内部状态(如不说"你不是 owner",防信息泄露)| 风险 | 说明 | 建议 |
|---|---|---|
| owner 单点风险 | owner 拥有所有 onlyOwner 权限,一旦私钥泄露,合约被完全控制 |
生产环境将 owner 转给多签钱包(如 Gnosis Safe) |
| 无零地址校验 | transferOwnership(address(0)) 会让合约永久无主 |
继承后 override 添加 require(newOwner != address(0)) |
| 无两步确认 | 一步转移,填错地址则所有权永久丢失 | 继承后实现 pending owner + acceptOwnership 两步确认机制 |
| 构造函数可传零地址 | constructor(address(0)) 会让合约诞生即无主 |
调用方自行确保传入有效地址 |
| 无 renounceOwnership | 没有专门的"放弃所有权"函数 | transferOwnership(address(0)) 可达到同样效果,但语义不够显式 |
| 字符串 revert | "UNAUTHORIZED" 比 custom error 多消耗 gas |
在 revert 路径上多消耗 ~200 gas,但 solmate 追求代码简洁而非极致 revert 优化 |
| abstract 但无未实现函数 | 开发者可能困惑为什么要标 abstract | 这是语义选择,强调"不应独立部署" |
| 维度 | Solmate Owned | OpenZeppelin Ownable |
|---|---|---|
| 代码量 | ~30 行 | ~80 行 |
| 零地址校验 | ❌ 无 | ✅ 有 |
| 两步转移 | ❌ 无 | ✅ Ownable2Step 提供 |
| renounceOwnership | ❌ 无(可传零地址实现) | ✅ 有专门函数 |
| 构造函数 | 显式传入 owner | 默认 msg.sender,也可传入 |
| virtual 修饰 | ✅ 所有函数和修饰符 | ✅ 部分函数 |
| 自定义 error | ❌ 用字符串 | ✅ OwnableUnauthorizedAccount |
| 设计哲学 | 最小原语,开发者自行扩展 | 开箱即用,内置常见防护 |
| Gas 成本 | 更低 | 略高(多了校验逻辑) |
| 适合场景 | 追求极致优化、会自行扩展的团队 | 快速开发、需要开箱即用安全性 |
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Owned} from "solmate/auth/Owned.sol";
/*
* @title SimpleVault — 使用 Owned 做权限控制的简易金库合约
* @notice 只有 owner 可以提取资金,任何人可以存入
*/
contract SimpleVault is Owned {
// 用户余额
mapping(address => uint256) public balances;
constructor(address _owner) Owned(_owner) {}
/// @notice 存款,任何人可调用
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice 取款,仅 owner 可调用
function withdraw(uint256 amount) external onlyOwner {
require(address(this).balance >= amount, "INSUFFICIENT");
payable(owner).transfer(amount);
}
/// @notice 紧急暂停,仅 owner 可调用
function emergencyWithdrawAll() external onlyOwner {
payable(owner).transfer(address(this).balance);
}
}
// 1. 部署合约(deployer 作为初始 owner)
SimpleVault vault = new SimpleVault(deployer);
// → OwnershipTransferred(address(0), deployer)
// 2. 任何人可存款
vault.deposit{value: 1 ether}();
// 3. 只有 owner 能取款
vault.withdraw(0.5 ether); // deployer 调用 → 成功 ✅
// 4.(生产环境)转移所有权给多签
vault.transferOwnership(multisigAddress);
// → OwnershipTransferred(deployer, multisigAddress)
// 5. 旧 owner 不再能取款
vault.withdraw(0.1 ether); // deployer 调用 → revert("UNAUTHORIZED") ❌
deployer 调用 vault.withdraw(0.5 ether):
→ onlyOwner 修饰符
→ msg.sender == owner ? → deployer == deployer → true ✅
→ 执行 withdraw 逻辑
alice 调用 vault.withdraw(0.5 ether):
→ onlyOwner 修饰符
→ msg.sender == owner ? → alice == deployer → false ❌
→ revert("UNAUTHORIZED")
deployer 调用 vault.transferOwnership(address(0)):
→ onlyOwner 校验通过
→ owner = address(0)
→ 从此所有 onlyOwner 函数永远不可调用(合约永久无主)
目标合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/src/auth/MockOwned.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import {Owned} from "solmate/auth/Owned.sol";
contract MockOwned is Owned {
constructor(address owner) Owned(owner) {}
function protectedFunction() external view onlyOwner returns (bool) {
return true;
}
function unprotectedFunction() external pure returns (bool) {
return true;
}
}
全部foundry测试合约:https://github.com/RevelationOfTuring/foundry-solmate/blob/main/test/auth/Owned.t.sol
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!
