name: web3-solidity-patterns description: Solidity 设计模式与最佳实践。适用于设计合约架构、实现代理、治理、访问控制或任何结构性 Solidity 模式。涵盖工厂模式、代理模式、钻石模式、治理者模式等。
Solidity 设计模式
合约架构
接口优先开发
始终在实现之前定义接口:
interface IVault {
function deposit(uint256 amount) external;
function withdraw(uint256 amount) external;
function balanceOf(address user) external view returns (uint256);
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
error InsufficientBalance(uint256 available, uint256 requested);
}
合约结构顺序
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
contract MyContract {
// 1. 类型声明(结构体、枚举)
// 2. 状态变量
// 3. 事件
// 4. 错误
// 5. 修饰器
// 6. 构造函数
// 7. 外部函数
// 8. 公共函数
// 9. 内部函数
// 10. 私有函数
// 11. 视图/纯函数
}
设计模式
工厂模式
从工厂部署新的合约实例:
contract VaultFactory {
address[] public vaults;
event VaultCreated(address indexed vault, address indexed owner);
function createVault(address token) external returns (address) {
Vault vault = new Vault(token, msg.sender);
vaults.push(address(vault));
emit VaultCreated(address(vault), msg.sender);
return address(vault);
}
}
适用场景:部署具有不同参数的同一合约的多个实例。
最小代理模式(EIP-1167 克隆)
部署共享同一实现的廉价副本:
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
contract VaultFactory {
address public immutable implementation;
constructor() {
implementation = address(new Vault());
}
function createVault(address token) external returns (address) {
address clone = Clones.clone(implementation);
Vault(clone).initialize(token, msg.sender);
return clone;
}
}
适用场景:部署许多相同的合约(节省 90%+ 的部署 Gas)。
代理模式
| 模式 | 升级逻辑在 | Gas 成本 | 复杂度 |
|---|---|---|---|
| 透明代理 | 代理合约 | 较高(管理员检查) | 中等 |
| UUPS (EIP-1822) | 实现合约 | 较低 | 中等 |
| 信标代理 | 信标合约 | 中等 | 高 |
| 钻石模式 (EIP-2535) | 钻石合约 | 可变 | 非常高 |
访问控制
| 模式 | 适用场景 |
|---|---|
Ownable |
单一管理员 |
Ownable2Step |
带安全转移的单一管理员 |
AccessControl |
多角色 |
AccessControlDefaultAdminRules |
带管理员安全性的多角色 |
| 多签(Gnosis Safe) | 高价值操作 |
| 时间锁 + 治理者 | DAO 治理 |
拉取优于推送(支付)
// 差:推送模式 — 可能失败/DoS
function distribute(address[] calldata recipients) external {
for (uint i; i < recipients.length;) {
payable(recipients[i]).transfer(amount); // 可能失败
unchecked { ++i; }
}
}
// 好:拉取模式 — 用户自行提取
mapping(address => uint256) public pendingWithdrawals;
function withdraw() external {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, NothingToWithdraw());
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, TransferFailed());
}
事件驱动架构
为链下索引(The Graph、自定义索引器)设计事件:
// 便于索引的事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Swap(
address indexed sender,
address indexed tokenIn,
address indexed tokenOut,
uint256 amountIn,
uint256 amountOut
);
- 最多 3 个索引参数(用于过滤)
- 非索引参数用于数据
- 为每个状态变更发出事件
库的使用
// 使用库来实现可复用的逻辑,无需继承
using SafeERC20 for IERC20;
using Math for uint256;
// 在以下情况优先使用库而非继承:
// - 逻辑是无状态的
// - 多个不相关的合约需要相同的工具
// - 希望避免钻石继承问题
测试模式
| 类型 | 目的 | 工具 |
|---|---|---|
| 单元测试 | 单一函数正确性 | forge test |
| 集成测试 | 多合约交互 | forge test(带设置) |
| 分叉测试 | 针对真实主网状态 | forge test --fork-url |
| 模糊测试 | 随机输入测试属性 | forge test(模糊) |
| 不变性测试 | 协议范围属性 | forge test(不变性) |
代码示例
查看 examples.md 获取完整代码示例:
- 最小代理模式(EIP-1167)
- UUPS 代理设置
- 钻石模式骨架
- 治理者设置
- 带许可的 ERC-20