深入剖析Solmate库

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

深入剖析Solmate库 #01:Owned.sol

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 才能调用被保护的函数。

  • 代码量极少(不到 30 行有效代码)
  • 没有任何多余的校验和防护
  • 所有函数和修饰符均为 virtual,子合约可自由重写

它代表了 Solmate 的核心设计哲学:够用就好,不做多余的事,把选择权留给开发者。

二、适用场景

适合 不适合
只需单个管理员的简单合约 需要多角色/多级权限管理
代理合约、工厂合约等只需 owner 保护的场景 需要外部权限合约(authority)做动态授权
对 gas 极度敏感,权限逻辑越少越好 需要两步确认(pending owner)的安全转移
原型/MVP 阶段快速开发 需要多签/时间锁等复杂治理
作为更复杂权限系统的基类被继承 需要零地址校验等防误操作机制

三、合约结构总览

Owned (abstract contract)
│
├── 状态变量
│   └── owner : address           ← 唯一的权限判断依据
│
├── 事件
│   └── OwnershipTransferred      ← 所有权变更时触发
│
├── 修饰符
│   └── onlyOwner()               ← 限制仅 owner 可调用
│
├── 构造函数
│   └── constructor(address)      ← 设置初始 owner
│
└── 函数
    └── transferOwnership(address) ← 一步转移所有权

四、源码逐行解析

4.1 合约声明

abstract contract Owned { ... }
关键词 含义
abstract 不可独立部署,必须被子合约继承后才能部署

设计决策

  • 标记为 abstract 表明这是一个 mixin(混入合约),设计目的就是被继承
  • 虽然合约内部没有未实现的函数(所有函数都有实现体),仍然标记 abstract 来明确表达"不应独立使用"的语义
  • 不继承任何合约/接口,零依赖,最小化

4.2 Events

OwnershipTransferred

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

作用:记录所有权转移,方便链下系统(前端/索引服务)追踪合约控制权变更。

参数 类型 indexed 含义
user address 原所有者地址(构造时为 address(0),表示从"无主"状态创建)
newOwner address 新所有者地址

触发时机

  1. 构造函数执行时:OwnershipTransferred(address(0), _owner) — 表示合约从"无主"状态转为初始 owner
  2. transferOwnership() 执行成功后:OwnershipTransferred(msg.sender, newOwner) — 表示所有权从当前 owner 转移

设计决策

  • 两个参数都加 indexed,方便链下按旧 owner 或新 owner 过滤日志
  • 构造时 useraddress(0) 而非 msg.sender,语义更清晰:表示"从无到有",而非"部署者转给 owner"
  • 与 OpenZeppelin 的 OwnershipTransferred(previousOwner, newOwner) 命名不同,solmate 用 user 指代原 owner

4.3 Constructor

constructor(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 可以不是同一个人(如工厂合约代部署)
  • 构造时就触发事件,确保链下有完整的所有权历史记录

4.4 Storage

owner

address public owner;

作用:存储当前合约所有者地址,是所有权限判断的唯一依据。public 自动生成 owner() getter 函数。

类型 含义
address 当前所有者地址。address(0) 表示合约无主

设计决策

  • 只用一个 address,没有 pendingOwner、没有角色映射,是权限管理的最小化实现
  • public 确保任何人都能查询当前 owner,提高透明度

4.5 Modifier

onlyOwner

modifier 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 路径多消耗,正常路径无影响

4.6 管理函数

transferOwnership

function 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 更直观

五、完整调用流程图

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

外部调用受保护函数(如 transferOwnership)
         │
         ▼
  onlyOwner 修饰符
  作用:校验 msg.sender 是否为 owner
         │
         ▼
  msg.sender == owner ?
         │
    ┌────┴────┐
   YES       NO
    │         │
    ▼         ▼
  执行函数   revert("UNAUTHORIZED")
  原始逻辑
    │
    ▼
  返回结果

5.2 所有权转移流程

当前 owner 调用 transferOwnership(newOwner)
         │
         ▼
  onlyOwner 校验通过
  作用:确保只有当前 owner 才能转移所有权
         │
         ▼
  owner = newOwner
  作用:直接覆盖 storage,立即生效
         │
         ▼
  emit OwnershipTransferred(msg.sender, newOwner)
  作用:链下记录所有权变更历史
         │
         ▼
  结果:
  - 原 owner(msg.sender)立即失去所有 onlyOwner 权限
  - 新 owner(newOwner)立即获得所有 onlyOwner 权限

5.3 典型部署与使用流程

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

六、设计思想

6.1 极简主义

  • 整个合约不到 30 行有效代码(去除注释和空行)
  • 1 个事件、1 个状态变量、1 个修饰符、1 个函数 — 权限管理的绝对最小集
  • 没有 pendingOwner、没有 renounceOwnership()、没有零地址校验
  • 功能不够?继承后 override 扩展,而不是把所有场景都塞进基类

6.2 Gas 极致优化

操作 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,代码更紧凑。

6.3 可组合性

  • 标记 abstract → 明确表达"请继承我"的语义
  • onlyOwner 标记 virtual → 子合约可自定义权限逻辑
  • transferOwnership 标记 virtual → 子合约可添加零地址校验、两步确认等
  • 零依赖 → 不引入任何外部合约,可被任何项目直接使用

6.4 防御性设计

  • 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 这是语义选择,强调"不应独立部署"

八、与OpenZeppelin Ownable对比

维度 Solmate Owned OpenZeppelin Ownable
代码量 ~30 行 ~80 行
零地址校验 ❌ 无 ✅ 有
两步转移 ❌ 无 ✅ Ownable2Step 提供
renounceOwnership ❌ 无(可传零地址实现) ✅ 有专门函数
构造函数 显式传入 owner 默认 msg.sender,也可传入
virtual 修饰 ✅ 所有函数和修饰符 ✅ 部分函数
自定义 error ❌ 用字符串 OwnableUnauthorizedAccount
设计哲学 最小原语,开发者自行扩展 开箱即用,内置常见防护
Gas 成本 更低 略高(多了校验逻辑)
适合场景 追求极致优化、会自行扩展的团队 快速开发、需要开箱即用安全性

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

9.1 业务合约

// 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);
    }
}

9.2 部署与配置

// 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") ❌

9.3 调用验证

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

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

0 条评论

请先 登录 后评论