Solidity入门(10)-智能合约设计模式1

文章目录

  • [1. 设计模式概述](#1. 设计模式概述)
    • [1.1 什么是设计模式](#1.1 什么是设计模式)
    • [1.2 智能合约中的6个核心设计模式](#1.2 智能合约中的6个核心设计模式)
  • [2. 访问控制模式](#2. 访问控制模式)
    • [2.1 为什么需要访问控制](#2.1 为什么需要访问控制)
    • [2.2 Ownable模式](#2.2 Ownable模式)
    • [2.3 RBAC模式(基于角色的访问控制)](#2.3 RBAC模式(基于角色的访问控制))
    • [2.4 使用OpenZeppelin的实现](#2.4 使用OpenZeppelin的实现)
  • [3. 提现模式](#3. 提现模式)
    • [3.1 传统做法的风险](#3.1 传统做法的风险)
    • [3.2 攻击合约示例](#3.2 攻击合约示例)
    • [3.3 安全方案1:Pull Over Push模式](#3.3 安全方案1:Pull Over Push模式)
    • [3.5 结合重入锁](#3.5 结合重入锁)
  • [4. 状态机模式](#4. 状态机模式)
    • [4.1 什么是状态机](#4.1 什么是状态机)
    • [4.2 ICO众筹示例](#4.2 ICO众筹示例)
    • [4.3 适用场景](#4.3 适用场景)

1. 设计模式概述

1.1 什么是设计模式

在软件工程中,设计模式是被反复使用的、经过实践验证的解决方案。它们不是具体的代码,而是一种解决特定问题的思路和方法。

设计模式的价值:

  • 提高代码质量:

    • 经过实践验证的解决方案
    • 减少常见错误
    • 提高代码可维护性
  • 加速开发:

    • 不需要从零开始设计
    • 复用成熟的方案
    • 减少开发时间
  • 增强安全性:

    • 模式通常考虑了安全因素
    • 避免常见的安全漏洞
    • 提高合约的可靠性
  • 便于协作:

    • 团队成员都理解这些模式
    • 代码更容易理解和维护
    • 降低沟通成本

1.2 智能合约中的6个核心设计模式

在智能合约开发领域,有6个核心的设计模式,它们分别解决不同的问题:

  • 访问控制模式:

    • 解决权限管理问题
    • 确保只有授权者能执行敏感操作
    • 基础但至关重要
  • 提现模式:

    • 解决资金转账的安全问题
    • 防止重入攻击
    • 确保资金安全转移
  • 状态机模式:

    • 管理合约的生命周期
    • 规范状态转换流程
    • 适用于有明确阶段的场景
  • 代理模式:

    • 实现合约升级
    • 分离数据和逻辑
    • 解决不可变性与升级需求的矛盾
  • 工厂模式:

    • 批量部署相同类型的合约
    • 降低部署成本
    • 统一管理合约实例
  • 紧急停止模式:

    • 风险控制机制
    • 快速暂停合约功能
    • 保护用户资产安全

这些模式在实际项目中通常会组合使用,共同构建安全可靠的智能合约系统。

2. 访问控制模式

访问控制模式是几乎所有合约都需要的基础模式。通过权限管理,我们可以确保系统的安全性和完整性,控制谁可以执行敏感操作。

2.1 为什么需要访问控制

设想一下,如果一个合约没有任何访问控制,会发生什么?

没有访问控制的危险:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 没有访问控制的危险合约
contract UnsafeToken {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    
    /**
     * @notice 任何人都可以铸造代币
     * @dev 危险:没有权限检查,攻击者可以给自己铸造无限代币
     */
    function mint(address to, uint256 amount) public {
        balances[to] += amount;
        totalSupply += amount;
    }
    
    /**
     * @notice 任何人都可以销毁合约
     * @dev 危险:没有权限检查,任何人都可以销毁合约并提取资金
     */
    function destroy() public {
        selfdestruct(payable(msg.sender));
    }
}

问题分析:

  • 任何人都可以铸造代币:

    • 攻击者可以给自己铸造无限代币
    • 代币价值会瞬间归零
    • 项目完全崩溃
  • 任何人都可以销毁合约:

    • 攻击者可以销毁合约并提取所有资金
    • 用户资金全部丢失
    • 系统完全瘫痪
  • 无法审计和追踪:

    • 不知道谁执行了什么操作
    • 无法追溯问题来源
    • 无法进行权限管理
  • 有了访问控制的好处:

    • 确保只有授权者能执行敏感操作
    • 不同角色可以拥有不同的权限
    • 可以转移或撤销权限
    • 能够审计追踪所有的操作历史

2.2 Ownable模式

Ownable模式是最基础的访问控制实现。合约有一个owner地址,所有关键操作都需要通过onlyOwner修饰符来检查调用者是否是owner。

实现示例:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Ownable模式:简单的单所有者权限控制
contract OwnableToken {
    // 记录合约所有者
    address public owner;
    
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    
    // 事件:记录所有权转移
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    /**
     * @notice 构造函数:初始化owner
     * @dev 部署合约时,msg.sender成为owner
     */
    constructor() {
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }
    
    /**
     * @notice onlyOwner修饰符
     * @dev 只有owner可以调用被此修饰符修饰的函数
     */
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    /**
     * @notice 铸造代币(只有owner可以调用)
     * @param to 接收者地址
     * @param amount 铸造数量
     * @dev 使用onlyOwner修饰符确保只有owner可以铸造
     */
    function mint(address to, uint256 amount) public onlyOwner {
        balances[to] += amount;
        totalSupply += amount;
    }
    
    /**
     * @notice 转移所有权
     * @param newOwner 新的所有者地址
     * @dev 只有当前owner可以转移所有权
     */
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Invalid owner");
        address oldOwner = owner;
        owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
    
    /**
     * @notice 放弃所有权
     * @dev owner可以放弃所有权,之后合约将无法升级或修改
     */
    function renounceOwnership() public onlyOwner {
        address oldOwner = owner;
        owner = address(0);
        emit OwnershipTransferred(oldOwner, address(0));
    }
}

Ownable模式的特点:

优点:

  • 实现简单,易于理解
  • Gas成本低
  • 适合权限需求单一的场景

缺点:

  • 只有一个角色(owner)
  • 无法实现细粒度的权限控制
  • 不适合复杂的权限需求

适用场景:

  • 简单的代币合约
  • 权限需求单一的项目
  • 小型项目或原型

2.3 RBAC模式(基于角色的访问控制)

RBAC(Role-Based Access Control)是更灵活的权限管理方式。OpenZeppelin提供了AccessControl合约,允许我们定义多个角色,每个函数可以指定需要的角色。

实现示例:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// RBAC模式:基于角色的访问控制
contract RBACToken {
    // 角色映射:角色 => 地址 => 是否有权限
    mapping(bytes32 => mapping(address => bool)) private roles;
    
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    bool public paused;
    
    // 定义角色常量
    // 使用keccak256确保角色标识符的唯一性
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
    
    // 事件:记录角色授予和撤销
    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
    
    /**
     * @notice 构造函数:初始化管理员
     * @dev 部署者自动获得ADMIN_ROLE
     */
    constructor() {
        roles[ADMIN_ROLE][msg.sender] = true;
        emit RoleGranted(ADMIN_ROLE, msg.sender, msg.sender);
    }
    
    /**
     * @notice onlyRole修饰符
     * @param role 需要的角色
     * @dev 检查调用者是否拥有指定角色
     */
    modifier onlyRole(bytes32 role) {
        require(roles[role][msg.sender], "Access denied");
        _;
    }
    
    /**
     * @notice 检查地址是否拥有角色
     * @param role 角色
     * @param account 地址
     * @return 是否拥有角色
     */
    function hasRole(bytes32 role, address account) public view returns (bool) {
        return roles[role][account];
    }
    
    /**
     * @notice 授予角色
     * @param role 角色
     * @param account 地址
     * @dev 只有ADMIN可以授予角色
     */
    function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        require(!roles[role][account], "Already has role");
        roles[role][account] = true;
        emit RoleGranted(role, account, msg.sender);
    }
    
    /**
     * @notice 撤销角色
     * @param role 角色
     * @param account 地址
     * @dev 只有ADMIN可以撤销角色
     */
    function revokeRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
        require(roles[role][account], "Does not have role");
        roles[role][account] = false;
        emit RoleRevoked(role, account, msg.sender);
    }
    
    /**
     * @notice 铸造代币(只有MINTER可以调用)
     * @param to 接收者地址
     * @param amount 铸造数量
     * @dev 使用onlyRole(MINTER_ROLE)确保只有MINTER可以铸造
     */
    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        require(!paused, "Contract is paused");
        balances[to] += amount;
        totalSupply += amount;
    }
    
    /**
     * @notice 销毁代币(只有BURNER可以调用)
     * @param from 销毁者地址
     * @param amount 销毁数量
     * @dev 使用onlyRole(BURNER_ROLE)确保只有BURNER可以销毁
     */
    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        require(balances[from] >= amount, "Insufficient balance");
        balances[from] -= amount;
        totalSupply -= amount;
    }
    
    /**
     * @notice 暂停合约(只有PAUSER可以调用)
     * @dev 暂停后,mint等操作将无法执行
     */
    function pause() public onlyRole(PAUSER_ROLE) {
        paused = true;
    }
    
    /**
     * @notice 恢复合约(只有PAUSER可以调用)
     * @dev 恢复后,合约功能恢复正常
     */
    function unpause() public onlyRole(PAUSER_ROLE) {
        paused = false;
    }
}

RBAC模式的特点:

优点:

  • 支持多个角色
  • 权限管理灵活
  • 可以实现细粒度的权限控制
  • 适合大型项目

缺点:

  • 实现相对复杂
  • Gas成本稍高
  • 需要仔细设计角色体系

适用场景:

  • 大型DeFi协议
  • 需要多角色管理的项目
  • 复杂的权限需求

2.4 使用OpenZeppelin的实现

OpenZeppelin提供了经过充分审计的访问控制实现,推荐直接使用:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 导入OpenZeppelin的Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

// 使用OpenZeppelin Ownable的代币合约
contract SecureToken is Ownable {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    
    /**
     * @notice 构造函数
     * @dev Ownable会自动将msg.sender设置为owner
     */
    constructor() Ownable() {
        // owner已经在Ownable的构造函数中设置
    }
    
    /**
     * @notice 铸造代币
     * @param to 接收者地址
     * @param amount 铸造数量
     * @dev 使用onlyOwner修饰符
     */
    function mint(address to, uint256 amount) external onlyOwner {
        balances[to] += amount;
        totalSupply += amount;
    }
}

OpenZeppelin的优势:

  • 经过充分审计
  • 被广泛使用
  • 提供完整的功能
  • 持续维护和更新

推荐做法:

除非有特殊需求,否则应该使用OpenZeppelin的标准实现,而不是自己从零实现。

3. 提现模式

提现模式专门用于处理资金转账的安全问题。这个模式能够有效防止重入攻击,确保资金的安全转移。

3.1 传统做法的风险

很多开发者会写出这样的代码:先调用transfer或send转账给用户,然后再更新用户的余额。这种写法有三大核心风险。

不安全的实现:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 存在重入漏洞的银行合约
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    /**
     * @notice 存款函数
     * @dev 用户可以存入以太币
     */
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    
    /**
     * @notice 提现函数(存在重入漏洞!)
     * @dev 危险:先转账,后更新状态
     */
    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // 危险:先转账
        // 如果接收者是一个恶意合约,它可以在receive函数中再次调用withdraw
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        
        // 危险:后更新状态
        // 如果发生重入,此时余额还没有被清零,攻击者可以再次提取
        balances[msg.sender] = 0;
    }
    
    /**
     * @notice 查询合约余额
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

三大核心风险:

重入攻击风险:

  • 如果接收者是一个恶意合约,它可以在receive或fallback函数中再次调用withdraw
  • 由于余额还没有被清零,攻击者可以反复提取资金
  • 2016年著名的The DAO攻击就是因为这个漏洞,导致损失了5000万美元

Gas不足导致转账失败:

  • 如果transfer或send失败,但用户余额已经被扣除
  • 就会导致用户资金被锁定在合约中
  • 用户无法取回资金

影响其他用户:

  • 如果某一笔转账失败,可能会影响到其他用户的正常操作
  • 导致整个系统的不稳定

3.2 攻击合约示例

以下是一个利用重入漏洞的攻击合约:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 攻击合约:利用重入漏洞
contract Attacker {
    VulnerableBank public bank;
    uint256 public attackCount;
    
    /**
     * @notice 构造函数:初始化目标银行合约地址
     * @param _bankAddress 目标银行合约地址
     */
    constructor(address _bankAddress) {
        bank = VulnerableBank(_bankAddress);
    }
    
    /**
     * @notice 发起攻击
     * @dev 攻击流程:先存款,再提现,在receive中触发重入
     */
    function attack() public payable {
        require(msg.value >= 1 ether, "Need at least 1 ether");
        attackCount = 0;
        
        // 步骤1:先向银行存入1 ether
        bank.deposit{value: msg.value}();
        
        // 步骤2:发起第一次提现
        // 这会触发receive函数,在receive中会再次调用withdraw
        bank.withdraw();
    }
    
    /**
     * @notice 接收以太币时触发重入攻击
     * @dev 这是攻击的关键:在receive函数中再次调用withdraw
     */
    receive() external payable {
        // 限制攻击次数,避免Gas耗尽
        if (attackCount < 3 && address(bank).balance >= 1 ether) {
            attackCount++;
            // 重入攻击:再次调用withdraw
            // 此时bank的balances[address(this)]还没有被清零
            bank.withdraw();
        }
    }
    
    /**
     * @notice 提取攻击获得的资金
     */
    function getStolen() public {
        payable(msg.sender).transfer(address(this).balance);
    }
}

攻击流程:

bash 复制代码
1. 攻击者调用attack(),存入1 ether
   - bank.balances[attacker] = 1 ether

2. 攻击者调用withdraw()
   - 检查余额:1 ether(通过)
   - 向攻击者转账1 ether
   - 触发攻击者的receive()函数

3. receive()函数中再次调用withdraw()
   - 此时balances[attacker]还是1 ether(还没被清零!)
   - 检查余额:1 ether(通过)
   - 再次向攻击者转账1 ether
   - 再次触发receive()函数

4. 重复步骤3,直到攻击次数达到限制
   - 最终攻击者提取了4 ether(1 ether本金 + 3 ether窃取)

3.3 安全方案1:Pull Over Push模式

Pull Over Push模式的核心思想是:让用户主动来提现,而不是合约主动推送资金。

Pull模式实现:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// Pull模式:用户主动提现
contract SafeBankPull {
    // 记录每个用户的待提现金额
    mapping(address => uint256) public pendingWithdrawals;
    
    /**
     * @notice 存款函数
     * @dev 用户存入以太币
     */
    function deposit() public payable {
        // 直接记录待提现金额,不立即转账
        pendingWithdrawals[msg.sender] += msg.value;
    }
    
    /**
     * @notice 提现函数(Pull模式)
     * @dev 用户主动调用此函数来提取自己的资金
     */
    function withdraw() public {
        // 获取用户的待提现金额
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "No pending withdrawal");
        
        // 先清零待提现金额(防止重入)
        pendingWithdrawals[msg.sender] = 0;
        
        // 然后转账
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
    
    /**
     * @notice 查询合约余额
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

Pull模式的优势:

  • 防止重入攻击:

余额在转账前就被清零

即使发生重入,余额检查也会失败

  • Gas由用户承担:

用户主动调用withdraw

Gas消耗由用户支付

不会因为Gas耗尽导致功能不可用

  • 更好的用户体验:

用户可以选择何时提现

可以分批提现

更灵活

3.4 安全方案2:CEI原则

CEI原则(Checks-Effects-Interactions)是一个非常重要的安全原则。执行顺序是:首先进行所有的检查,然后更新合约的状态变量,最后才进行外部交互。

CEI模式实现:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// CEI模式:遵循检查-效果-交互原则
contract SafeBankCEI {
    mapping(address => uint256) public balances;
    
    /**
     * @notice 存款函数
     */
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    
    /**
     * @notice 提现函数(遵循CEI原则)
     * @dev 按照Checks-Effects-Interactions的顺序执行
     */
    function withdraw() public {
        // 1. Checks(检查):验证所有条件
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // 2. Effects(效果):先更新状态
        // 关键:在外部调用之前更新状态
        // 这样即使发生重入,余额检查也会失败
        balances[msg.sender] = 0;
        
        // 3. Interactions(交互):然后进行外部调用
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
    
    /**
     * @notice 查询合约余额
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

CEI原则的关键点:

  • Checks(检查):

首先验证所有前置条件

检查余额是否足够

检查参数是否有效

  • Effects(效果):

然后更新合约状态

将余额清零

更新其他状态变量

  • Interactions(交互):

最后进行外部调用

转账给用户

调用外部合约

  • 为什么CEI模式安全:

余额在外部调用前就是0

即使发生重入,余额检查也会失败(require(amount > 0)会失败)

攻击无效

3.5 结合重入锁

除了CEI原则,我们还可以使用重入锁提供额外的保护:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 结合CEI和重入锁的安全实现
contract SafeBankWithLock {
    mapping(address => uint256) public balances;
    
    // 重入锁
    bool private locked;
    
    /**
     * @notice 重入锁修饰符
     * @dev 防止函数被重入调用
     */
    modifier noReentrant() {
        require(!locked, "No reentrancy");
        locked = true;
        _;
        locked = false;
    }
    
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    
    /**
     * @notice 提现函数(CEI + 重入锁)
     * @dev 双重保护:CEI原则 + 重入锁
     */
    function withdraw() public noReentrant {
        // Checks
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        
        // Effects
        balances[msg.sender] = 0;
        
        // Interactions
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

最佳实践:

  • 优先使用CEI模式(零成本)
  • 关键函数添加重入锁(额外保护)
  • 使用OpenZeppelin的ReentrancyGuard(经过审计)

4. 状态机模式

状态机模式用于管理合约的生命周期。通过定义有限状态和状态转换规则,我们可以规范合约的行为,确保在正确的状态下执行正确的操作。

4.1 什么是状态机

状态机(State Machine)是一种计算模型,它定义了一组有限的状态,以及状态之间的转换规则。在不同的状态下,系统允许执行的操作是不同的。

状态机的核心概念:

  • 状态(State):

系统在某个时刻的特定情况

用enum定义所有可能的状态

  • 转换(Transition):

从一个状态转换到另一个状态

需要满足特定的条件

  • 规则(Rules):

定义在什么状态下可以执行什么操作

确保操作的合法性

状态机模式的优势:

  • 行为规范:

每个状态下允许的操作是明确的

避免了在错误的时间执行错误的操作

  • 可预测性:

状态转换的逻辑集中管理

便于理解和维护

  • 安全性:

防止在错误状态下执行操作

减少逻辑错误

4.2 ICO众筹示例

让我们通过一个ICO众筹的例子来理解状态机模式:

bash 复制代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

// 状态机模式:ICO众筹合约
contract SimpleCrowdfunding {
    // 定义所有可能的状态
    enum State { 
        Preparing,  // 准备阶段:项目初始化,还未开始募资
        Funding,    // 募资阶段:用户可以投资
        Success,    // 成功:达到募资目标
        Failed      // 失败:未达到募资目标
    }
    
    // 当前状态
    State public state;
    
    // 项目信息
    address public owner;
    uint256 public goal;        // 募资目标
    uint256 public raised;      // 已募集金额
    uint256 public deadline;    // 截止时间
    
    // 记录每个用户的投资金额
    mapping(address => uint256) public contributions;
    
    // 事件:记录状态变化和投资
    event StateChanged(State newState);
    event Contributed(address indexed contributor, uint256 amount);
    
    /**
     * @notice 构造函数:初始化项目
     * @param _goal 募资目标(wei)
     * @param _durationMinutes 募资持续时间(分钟)
     * @dev 项目初始状态为Preparing
     */
    constructor(uint256 _goal, uint256 _durationMinutes) {
        owner = msg.sender;
        goal = _goal;
        deadline = block.timestamp + (_durationMinutes * 1 minutes);
        state = State.Preparing;  // 初始状态:准备阶段
    }
    
    /**
     * @notice inState修饰符:检查当前状态
     * @param _state 要求的状态
     * @dev 确保函数只在特定状态下可以执行
     */
    modifier inState(State _state) {
        require(state == _state, "Wrong state");
        _;
    }
    
    /**
     * @notice onlyOwner修饰符:只有所有者可以调用
     */
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    /**
     * @notice 开始募资
     * @dev 只有owner可以调用,且只能在Preparing状态下调用
     */
    function startFunding() public onlyOwner inState(State.Preparing) {
        // 状态转换:Preparing -> Funding
        state = State.Funding;
        emit StateChanged(State.Funding);
    }
    
    /**
     * @notice 投资函数
     * @dev 只能在Funding状态下调用,且必须在截止时间之前
     */
    function contribute() public payable inState(State.Funding) {
        // 检查是否在截止时间之前
        require(block.timestamp < deadline, "Funding ended");
        require(msg.value > 0, "Must send ETH");
        
        // 更新状态
        contributions[msg.sender] += msg.value;
        raised += msg.value;
        
        emit Contributed(msg.sender, msg.value);
    }
    
    /**
     * @notice 完成募资
     * @dev 只能在Funding状态下调用,且必须在截止时间之后
     * @dev 根据募集金额是否达到目标,转换到Success或Failed状态
     */
    function finalize() public inState(State.Funding) {
        // 检查是否已经过了截止时间
        require(block.timestamp >= deadline, "Funding not ended");
        
        // 根据募集金额决定最终状态
        if (raised >= goal) {
            // 达到目标:转换到Success状态
            state = State.Success;
            emit StateChanged(State.Success);
        } else {
            // 未达到目标:转换到Failed状态
            state = State.Failed;
            emit StateChanged(State.Failed);
        }
    }
    
    /**
     * @notice 提取资金(只有成功时才能提取)
     * @dev 只能在Success状态下调用,只有owner可以调用
     */
    function withdrawFunds() public onlyOwner inState(State.Success) {
        payable(owner).transfer(address(this).balance);
    }
    
    /**
     * @notice 退款(失败时用户可以退款)
     * @dev 只能在Failed状态下调用
     */
    function refund() public inState(State.Failed) {
        uint256 amount = contributions[msg.sender];
        require(amount > 0, "No contribution");
        
        // 清零投资记录(防止重复退款)
        contributions[msg.sender] = 0;
        
        // 退款给用户
        payable(msg.sender).transfer(amount);
    }
}

状态流转图:

bash 复制代码
Preparing(准备阶段)
    ↓ startFunding()
Funding(募资阶段)
    ↓ finalize()
    ├─ raised >= goal → Success(成功)
    └─ raised < goal → Failed(失败)

状态机模式的关键点:

  • 使用enum定义状态:

清晰明确

易于扩展

  • 使用修饰符检查状态:

inState修饰符确保函数只在正确状态下执行

减少错误操作

  • 状态转换逻辑集中:

所有状态转换都在特定函数中

便于维护和审计

4.3 适用场景

状态机模式非常适合有明确生命周期的场景:

  • 众筹项目:

准备、募资、成功/失败

  • 拍卖系统:

创建、竞拍、结束、结算

  • 游戏合约:

准备、进行中、结束

  • 投票系统:

创建、投票中、计票、完成

相关推荐
小明的小名叫小明2 小时前
Solidity入门(11)-智能合约设计模式2
设计模式·区块链·智能合约
__万波__2 小时前
二十三种设计模式(十四)--命令模式
java·设计模式·命令模式
程序员zgh2 小时前
C++常用设计模式
c语言·数据结构·c++·设计模式
MicroTech20252 小时前
微算法科技(NASDAQ MLGO)使用多线程技术提升区块链共识算法的性能
科技·区块链·共识算法
爬点儿啥3 小时前
[Ai Agent] 12 Swarm 与 Agents SDK —— 去中心化的多智能体协作
去中心化·区块链·swarm·langgraph·agents sdk·handoff
山风wind3 小时前
设计模式-模板方法模式详解
python·设计模式·模板方法模式
Biteagle3 小时前
P2SH:比特币的「脚本保险箱」与比特鹰的技术解析
区块链·智能合约
郝学胜-神的一滴3 小时前
Linux线程的共享资源与非共享资源详解
linux·服务器·开发语言·c++·程序人生·设计模式
syt_10133 小时前
设计模式之-单例模式
单例模式·设计模式