文章目录
- [5. 代理模式](#5. 代理模式)
-
- [5.1 为什么需要代理模式](#5.1 为什么需要代理模式)
- [5.2 代理模式架构](#5.2 代理模式架构)
- [5.3 存储布局兼容性](#5.3 存储布局兼容性)
- [6. 工厂模式](#6. 工厂模式)
-
- [6.1 为什么需要工厂模式](#6.1 为什么需要工厂模式)
- [6.2 基础工厂实现](#6.2 基础工厂实现)
- [6.3 Clone工厂模式(EIP-1167)](#6.3 Clone工厂模式(EIP-1167))
- [7. 紧急停止模式](#7. 紧急停止模式)
-
- [7.1 为什么需要紧急停止](#7.1 为什么需要紧急停止)
- [7.2 OpenZeppelin的Pausable实现](#7.2 OpenZeppelin的Pausable实现)
- [7.3 最佳实践](#7.3 最佳实践)
- [8. 模式对比与选择指南](#8. 模式对比与选择指南)
-
- [8.1 模式对比表](#8.1 模式对比表)
- [8.2 选择指南](#8.2 选择指南)
- [8.3 模式组合建议](#8.3 模式组合建议)
- [9. 模式组合应用案例](#9. 模式组合应用案例)
-
- [9.1 DeFi借贷协议](#9.1 DeFi借贷协议)
- [9.2 NFT交易市场](#9.2 NFT交易市场)
-
- [9.3 DAO治理系统](#9.3 DAO治理系统)
5. 代理模式
代理模式是实现合约升级的核心方案。通过分离数据存储和业务逻辑,我们可以在不改变合约地址的情况下升级业务逻辑。
5.1 为什么需要代理模式
我们都知道,智能合约部署后代码是不可修改的。但在实际项目中,我们经常需要修复Bug或者添加新功能。这就产生了一个矛盾:合约的不可变性与升级需求之间的矛盾。
传统方式的局限性:
bash
// 传统方式:合约不可升级
contract Token {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
// 如果这里发现Bug,无法修复!
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
问题:
- 发现Bug无法修复
- 无法添加新功能
- 只能部署新合约,但地址会改变
- 用户需要迁移到新合约
5.2 代理模式架构
代理模式通过分离数据存储和业务逻辑来解决这个问题。
架构组成:
- 代理合约(Proxy):
地址保持不变,用户始终与这个地址交互
只负责存储数据和管理升级
不包含业务逻辑
- 逻辑合约(Implementation):
包含所有的业务逻辑
可以部署多个版本(V1、V2、V3...)
通过升级切换版本
工作原理:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 简单的代理合约
contract SimpleProxy {
// 逻辑合约地址
address public implementation;
// 管理员地址
address public admin;
// 数据存储(与逻辑合约的存储布局必须一致)
uint256 public value;
/**
* @notice 构造函数:初始化逻辑合约地址
* @param _implementation 逻辑合约地址
*/
constructor(address _implementation) {
admin = msg.sender;
implementation = _implementation;
}
/**
* @notice onlyAdmin修饰符
*/
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
/**
* @notice 升级函数:更换逻辑合约
* @param newImplementation 新的逻辑合约地址
* @dev 只有admin可以调用
*/
function upgrade(address newImplementation) external onlyAdmin {
implementation = newImplementation;
}
/**
* @notice fallback函数:将所有调用转发到逻辑合约
* @dev 使用delegatecall调用逻辑合约
*/
fallback() external payable {
address impl = implementation;
require(impl != address(0), "Implementation not set");
// 使用delegatecall调用逻辑合约
// delegatecall的特性:
// 1. 代码在Implementation中执行
// 2. 但使用的storage是Proxy的
// 3. msg.sender保持不变(是原始调用者)
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// 接收以太币
receive() external payable {}
}
V1逻辑合约:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// V1逻辑合约:初始版本
contract ImplementationV1 {
// 注意:存储布局必须与Proxy完全一致!
address public implementation; // 对应Proxy的implementation
address public admin; // 对应Proxy的admin
uint256 public value; // 对应Proxy的value
/**
* @notice 设置值
* @param _value 要设置的值
* @dev 这个函数会修改Proxy的storage,不是本合约的
*/
function setValue(uint256 _value) public {
value = _value;
}
/**
* @notice 获取值
*/
function getValue() public view returns (uint256) {
return value;
}
}
V2逻辑合约(升级版本):
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// V2逻辑合约:升级版本(新增功能)
contract ImplementationV2 {
// 存储布局必须与V1和Proxy完全一致!
address public implementation;
address public admin;
uint256 public value;
// 新增变量只能在末尾添加
uint256 public multiplier;
/**
* @notice 设置值(新逻辑:值翻倍)
* @param _value 要设置的值
* @dev 新逻辑:值会自动翻倍
*/
function setValue(uint256 _value) public {
value = _value * (multiplier == 0 ? 1 : multiplier);
}
/**
* @notice 获取值
*/
function getValue() public view returns (uint256) {
return value;
}
/**
* @notice 设置倍数(V2新增功能)
* @param _multiplier 倍数
* @dev V1没有这个函数,升级后可以使用
*/
function setMultiplier(uint256 _multiplier) public {
multiplier = _multiplier;
}
}
执行流程:
bash
用户调用 Proxy.setValue(50)
↓
Proxy的fallback函数被触发(因为Proxy没有setValue函数)
↓
fallback函数使用delegatecall调用 Implementation.setValue(50)
↓
Implementation的代码在Proxy的上下文中执行
↓
修改的是Proxy的value(不是Implementation的)
↓
msg.sender仍然是原始用户(不是Proxy)
升级流程:
bash
V1时期:
- Proxy.value = 0
- 调用setValue(50) → Proxy.value = 50(V1逻辑:直接赋值)
升级到V2:
- upgrade(V2地址) → 逻辑切换,但Proxy.value保持50
V2时期:
- 调用setValue(50) → Proxy.value = 100(V2逻辑:50*2=100)
- 调用setMultiplier(3) → multiplier = 3(V2新功能)
5.3 存储布局兼容性
使用代理模式有一个关键的要求:存储布局必须保持兼容。
存储布局规则:
- 不能改变已有变量的类型和顺序:
bash
// V1
uint256 public value;
address public owner;
// V2(错误!)
address public value; // 类型改变,会导致数据错乱
uint256 public owner;
- 只能在末尾添加新变量:
bash
// V1
uint256 public value;
// V2(正确)
uint256 public value;
uint256 public multiplier; // 在末尾添加
- 不能删除变量:
bash
// V1
uint256 public value;
uint256 public oldValue;
// V2(错误!)
uint256 public value;
// oldValue被删除,会导致存储槽错乱
存储布局冲突的后果:
如果存储布局不兼容,会导致:
- 数据错乱
- 变量值被覆盖
- 合约功能异常
- 用户资金损失
最佳实践:
- 使用存储槽编号注释
- 充分测试升级过程
- 使用OpenZeppelin的升级代理(UUPS或Transparent)
6. 工厂模式
工厂模式用于批量部署相同类型的合约。这个模式在需要创建多个合约实例的场景中非常有用,还能大幅降低部署成本。
6.1 为什么需要工厂模式
我们来看几个实际场景:
Uniswap:
- 需要为每个交易对创建一个Pair合约
- ETH/USDT一个合约,ETH/DAI又是一个合约
- 需要统一管理和批量创建
NFT市场:
- 需要为每个创作者的集合创建独立的合约
- 每个NFT项目都有自己的合约
- 需要统一管理
多签钱包:
- 每个团队需要自己的多签钱包
- 需要批量创建和管理
传统方式的问题:
bash
// 传统方式:每次都要单独部署
contract Token {
// 部署一个Token合约需要20-50万Gas
}
// 如果需要创建100个Token,需要2000-5000万Gas!
6.2 基础工厂实现
基础的工厂实现很简单:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 简单的代币合约
contract SimpleToken {
string public name;
string public symbol;
address public creator;
uint256 public totalSupply;
mapping(address => uint256) public balances;
/**
* @notice 构造函数:初始化代币
* @param _name 代币名称
* @param _symbol 代币符号
* @param _supply 初始供应量
*/
constructor(string memory _name, string memory _symbol, uint256 _supply) {
name = _name;
symbol = _symbol;
creator = msg.sender;
totalSupply = _supply;
balances[msg.sender] = _supply;
}
/**
* @notice 转账函数
*/
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// 代币工厂合约
contract TokenFactory {
// 记录所有创建的代币地址
SimpleToken[] public tokens;
// 记录每个用户创建的代币
mapping(address => address[]) public userTokens;
// 事件:记录代币创建
event TokenCreated(
address indexed tokenAddress,
string name,
string symbol,
address indexed creator
);
/**
* @notice 创建新代币
* @param name 代币名称
* @param symbol 代币符号
* @param initialSupply 初始供应量
* @return 新代币的地址
* @dev 使用new关键字创建新合约实例
*/
function createToken(
string memory name,
string memory symbol,
uint256 initialSupply
) public returns (address) {
// 使用new关键字创建新的代币合约
SimpleToken newToken = new SimpleToken(name, symbol, initialSupply);
// 记录新代币地址
tokens.push(newToken);
userTokens[msg.sender].push(address(newToken));
// 发出事件
emit TokenCreated(address(newToken), name, symbol, msg.sender);
return address(newToken);
}
/**
* @notice 查询创建的代币数量
*/
function getTokenCount() public view returns (uint256) {
return tokens.length;
}
/**
* @notice 查询用户创建的所有代币
* @param user 用户地址
* @return 代币地址数组
*/
function getUserTokens(address user) public view returns (address[] memory) {
return userTokens[user];
}
}
基础工厂的特点:
- 实现简单
- 每个合约完整部署
- Gas成本:20-50万Gas/合约
6.3 Clone工厂模式(EIP-1167)
传统的部署方式Gas成本很高。Clone工厂模式(EIP-1167最小代理标准)可以大幅降低Gas成本。
Clone工厂的核心思想:
- 先部署一个模板合约(Implementation)
- 后续的合约不是完整部署,而是创建一个极简的代理
- 代理通过delegatecall调用模板合约
- 每个克隆只需要4.5万Gas左右,节省80%到90%!
Clone工厂实现:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 模板合约(只部署一次)
contract TokenImplementation {
string public name;
string public symbol;
address public creator;
uint256 public totalSupply;
mapping(address => uint256) public balances;
/**
* @notice 初始化函数(替代构造函数)
* @dev Clone不能使用构造函数,所以用初始化函数
*/
function initialize(
string memory _name,
string memory _symbol,
uint256 _supply
) public {
require(creator == address(0), "Already initialized");
name = _name;
symbol = _symbol;
creator = msg.sender;
totalSupply = _supply;
balances[msg.sender] = _supply;
}
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// Clone工厂合约
contract CloneFactory {
// 模板合约地址
address public implementation;
// 记录所有克隆的地址
address[] public clones;
// 记录每个用户创建的克隆
mapping(address => address[]) public userClones;
event CloneCreated(address indexed cloneAddress, address indexed creator);
/**
* @notice 构造函数:部署模板合约
* @dev 模板合约只部署一次
*/
constructor() {
implementation = address(new TokenImplementation());
}
/**
* @notice 创建克隆
* @param name 代币名称
* @param symbol 代币符号
* @param initialSupply 初始供应量
* @return 克隆合约地址
* @dev 使用create2创建确定性地址的克隆
*/
function createClone(
string memory name,
string memory symbol,
uint256 initialSupply
) public returns (address) {
// 使用create2创建克隆(需要实现最小代理合约)
// 这里简化示例,实际需要使用EIP-1167标准
bytes memory bytecode = getCloneBytecode();
bytes32 salt = keccak256(abi.encodePacked(msg.sender, clones.length));
address clone;
assembly {
clone := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
// 初始化克隆
TokenImplementation(clone).initialize(name, symbol, initialSupply);
// 记录克隆地址
clones.push(clone);
userClones[msg.sender].push(clone);
emit CloneCreated(clone, msg.sender);
return clone;
}
/**
* @notice 获取克隆字节码(EIP-1167最小代理)
* @dev 这是EIP-1167标准的最小代理合约字节码
*/
function getCloneBytecode() internal view returns (bytes memory) {
// EIP-1167最小代理合约字节码
// 实际实现需要使用OpenZeppelin的Clones库
return abi.encodePacked(
hex"3d602d80600a3d3981f3363d3d373d3d3d363d73",
implementation,
hex"5af43d82803e903d91602b57fd5bf3"
);
}
}

Clone工厂的优势:
- 大幅降低Gas成本
- 适合批量部署
- 统一管理
推荐使用OpenZeppelin的Clones库:
bash
import "@openzeppelin/contracts/proxy/Clones.sol";
contract MyFactory {
using Clones for address;
address public implementation;
function createClone() external returns (address) {
address clone = implementation.clone();
// 初始化克隆...
return clone;
}
}
7. 紧急停止模式
紧急停止模式,也叫断路器模式(Circuit Breaker),用于风险控制。在紧急情况下,能够快速暂停合约的功能,防止损失扩大。
7.1 为什么需要紧急停止
什么时候需要紧急停止呢?
- 发现安全漏洞:
合约存在严重的安全漏洞
正在遭受攻击
需要暂停功能防止损失扩大
- 预言机数据异常:
价格预言机返回异常数据
可能导致错误的交易
需要暂停等待修复
- 系统维护:
需要进行系统升级
需要修复Bug
需要暂停服务
- 市场异常:
市场出现极端波动
需要暂停交易保护用户
没有紧急停止的风险:
如果合约没有紧急停止机制,一旦发现问题,只能眼睁睁看着资金被攻击或损失,无法及时止损。
7.2 OpenZeppelin的Pausable实现
OpenZeppelin提供了Pausable合约,实现起来非常简单:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 使用OpenZeppelin Pausable的保险库合约
contract VaultWithPause {
mapping(address => uint256) public balances;
// 暂停状态
bool public paused;
// 管理员地址
address public admin;
// 事件:记录暂停和恢复操作
event Paused(address admin);
event Unpaused(address admin);
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
event EmergencyWithdrawal(address indexed user, uint256 amount);
/**
* @notice 构造函数:初始化管理员
*/
constructor() {
admin = msg.sender;
paused = false;
}
/**
* @notice whenNotPaused修饰符:要求合约未暂停
* @dev 大部分业务函数应该使用此修饰符
*/
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
/**
* @notice whenPaused修饰符:要求合约已暂停
* @dev 紧急函数应该使用此修饰符
*/
modifier whenPaused() {
require(paused, "Contract is not paused");
_;
}
/**
* @notice onlyAdmin修饰符:只有管理员可以调用
*/
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
/**
* @notice 存款函数(暂停时无法调用)
* @dev 使用whenNotPaused确保暂停时无法存款
*/
function deposit() public payable whenNotPaused {
require(msg.value > 0, "Must deposit something");
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
/**
* @notice 提现函数(暂停时无法调用)
* @param amount 提现金额
* @dev 使用whenNotPaused确保暂停时无法提现
*/
function withdraw(uint256 amount) public whenNotPaused {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawn(msg.sender, amount);
}
/**
* @notice 紧急提现(只能在暂停时调用)
* @dev 当合约暂停时,用户可以通过此函数提取资金
* @dev 使用whenPaused确保只能在暂停时调用
*/
function emergencyWithdraw() public whenPaused {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 清零余额(防止重复提取)
balances[msg.sender] = 0;
// 转账给用户
payable(msg.sender).transfer(amount);
emit EmergencyWithdrawal(msg.sender, amount);
}
/**
* @notice 暂停合约(只有管理员可以调用)
* @dev 暂停后,deposit和withdraw等函数将无法调用
*/
function pause() public onlyAdmin whenNotPaused {
paused = true;
emit Paused(admin);
}
/**
* @notice 恢复合约(只有管理员可以调用)
* @dev 恢复后,合约功能恢复正常
*/
function unpause() public onlyAdmin whenPaused {
paused = false;
emit Unpaused(admin);
}
}
紧急停止模式的关键点:
- 两个修饰符:
whenNotPaused:大部分业务函数使用
whenPaused:紧急函数使用
紧急函数:
emergencyWithdraw:允许用户在暂停时提取资金
确保用户资金安全
- 权限控制:
只有管理员可以暂停/恢复
建议使用多签钱包
7.3 最佳实践
推荐做法:
- 结合多签钱包:
紧急停止的权限最好结合多签钱包
避免单点故障
提高安全性
- 设置时间锁:
防止权限滥用
给社区反应时间
记录暂停原因:
记录每次暂停的原因和时间
便于审计和追溯
- 定期演练:
定期进行应急演练
确保在真正的紧急情况下能够快速响应
要避免的做法:
- 不要过度中心化:
Circuit Breaker是保护机制,但不应过度使用
避免滥用暂停功能
- 不要忽视用户体验:
暂停时要及时通知用户
提供紧急提现功能
- 不要忽视恢复流程:
确保有清晰的恢复流程
测试恢复功能
使用OpenZeppelin的Pausable:
bash
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyContract is Pausable, Ownable {
function deposit() public whenNotPaused {
// 存款逻辑...
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
}
8. 模式对比与选择指南
现在我们已经学习了6种设计模式,让我们做一个对比,帮助大家在实际项目中选择合适的模式。
8.1 模式对比表

8.2 选择指南
对于基础合约:
访问控制和紧急停止几乎是必备的:
- 任何合约都需要权限管理
- 金融类合约更需要紧急停止机制
如果合约涉及资金操作:
提现模式和CEI原则是必须遵守的:
- 这关系到资金安全
- 不能有任何侥幸心理
- 必须遵循CEI原则
如果合约有明确的生命周期:
状态机模式是首选:
- 众筹、拍卖等场景
- 规范流程,减少错误
需要升级能力的合约:
可以考虑代理模式,但要注意:
- 这是一个高复杂度的方案
- 需要非常谨慎
- 确保存储布局的兼容性
- 充分测试升级过程
如果需要批量部署相同类型的合约:
工厂模式是首选:
- 特别是Clone工厂能大幅降低成本
- 统一管理合约实例
8.3 模式组合建议
在实际项目中,通常会组合使用多个模式:
基础组合:
- 访问控制 + 紧急停止
资金相关:
- 访问控制 + 提现模式 + 紧急停止
复杂系统:
-访问控制 + 状态机 + 提现模式 + 紧急停止 + 代理模式
9. 模式组合应用案例
在实际项目中,我们通常会组合使用多个设计模式来构建复杂的系统。让我们看几个真实项目的例子。
9.1 DeFi借贷协议
像Compound和AAVE这样的DeFi借贷协议,通常会组合使用多个设计模式:
使用的模式:
访问控制:
- 管理管理员权限
- 使用多签钱包增加安全性
紧急停止:
- 快速应对市场风险或安全事件
- 保护用户资金
提现模式:
- 确保用户资金的安全取款
- 遵循CEI原则
代理模式:
- 让协议能够不断迭代升级
- 修复问题和添加新功能
示例代码结构:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
// DeFi借贷协议示例(组合多个模式)
contract LendingProtocol is Ownable, Pausable, ReentrancyGuard {
mapping(address => uint256) public deposits;
mapping(address => uint256) public borrows;
// 访问控制:只有owner可以设置参数
function setInterestRate(uint256 rate) external onlyOwner whenNotPaused {
// 设置利率...
}
// 提现模式:遵循CEI原则
function withdraw(uint256 amount) external nonReentrant whenNotPaused {
// Checks
require(deposits[msg.sender] >= amount, "Insufficient balance");
// Effects
deposits[msg.sender] -= amount;
// Interactions
payable(msg.sender).transfer(amount);
}
// 紧急停止:只有owner可以暂停
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
9.2 NFT交易市场
像OpenSea和Blur这样的NFT交易市场,也组合使用了多个设计模式:
使用的模式:
- 访问控制:
管理平台权限
控制平台费用
- 工厂模式:
创建不同的NFT集合
每个艺术家或项目可以有自己的合约
- 状态机:
管理拍卖流程
从开始竞拍到结束到资金结算
- 提现模式:
安全的资金结算
防止重入攻击
9.3 DAO治理系统
像Compound Governance这样的DAO治理系统,也组合使用了多个设计模式:
使用的模式:
- 访问控制:
管理投票权
只有代币持有者才能投票
- 状态机:
管理提案的完整流程
从创建、投票、排队到执行
- 代理模式:
DAO本身可以升级
通过治理投票来决定升级方案
- 紧急停止:
在出现问题时暂停治理流程
保护系统安全
- 关键要点:
从这些例子可以看出,真正的工程实践中,设计模式不是孤立使用的,而是根据业务需求组合使用,每个模式解决特定的问题。