Solidity 智慧合約完整實作指南:從 ERC-20 到可升級合約的工程實踐

本文從工程實踐角度深入講解 Solidity 智慧合約的完整開發流程,涵蓋 ERC-20 代幣合約的完整實現、基於角色的存取控制系統、可升級代理模式、以及使用 Foundry 框架的全面測試策略。我們提供了可直接用於生產環境的程式碼範例,包括完整的 ERC20 實現、AccessControl 角色管理、透明代理合約、以及包含模糊測試的測試套件。透過本文,開發者將掌握編寫安全、高效、可升級智慧合約的核心技能。

Solidity 智慧合約完整實作指南:從 ERC-20 到可升級合約的工程實踐

概述

在以太坊生態系統中,智慧合約是所有去中心化應用的核心。從簡單的代幣發行到複雜的 DeFi 協議,高質量的智慧合約需要考慮安全性、效能、可維護性和可升級性等多個維度。本文將從工程實踐的角度,詳細講解如何使用 Solidity 編寫、生產級的智慧合約,包括完整的程式碼範例、最佳實踐、以及常見漏洞的防範策略。

本文涵蓋的內容包括:完整的 ERC-20 代幣合約實現、基於角色的存取控制系統、可升級代理模式、以及完整的測試覆蓋策略。所有程式碼均基於 Solidity 0.8.20+ 版本,並遵循最新的安全標準。

一、工程化合約開發基礎

1.1 專案結構

生產級的智慧合約專案需要清晰的目錄結構:

smart-contract-project/
├── contracts/
│   ├── token/
│   │   ├── MyToken.sol
│   │   └── ERC20.sol
│   ├── access/
│   │   ├── AccessControl.sol
│   │   └── Ownable.sol
│   ├── interfaces/
│   │   ├── IERC20.sol
│   │   └── IAccessControl.sol
│   └── utils/
│       ├── Pausable.sol
│       └── ReentrancyGuard.sol
├── script/
│   └── Deploy.s.sol
├── test/
│   ├── Token.t.sol
│   └── AccessControl.t.sol
├── lib/
│   └── forge-std/
└── foundry.toml

1.2 開發環境配置

使用 Foundry 作為開發框架:

# foundry.toml
[profile.default]
src = "contracts"
out = "out"
libs = ["lib"]
solc = "0.8.20"

# 測試配置
[profile.ci]
verbosity = 4

# 優化配置
[profile.release]
solc = "0.8.20"
optimizer = true
optimizer_runs = 20000
via_ir = true

二、完整 ERC-20 代幣合約實現

2.1 ERC-20 標準回顧

ERC-20 是以太坊最廣泛使用的代幣標準,定義了以下六個必需函數和三個可選函數:

必需函數

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

2.2 完整 ERC-20 實現

以下是一個生產級的 ERC-20 實現,包含所有安全檢查和擴展功能:

// contracts/token/ERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "../interfaces/IERC20.sol";
import {IERC20Metadata} from "../interfaces/IERC20Metadata.sol";
import {Context} from "../utils/Context.sol";

/**
 * @dev ERC-20 代幣合約的完整實現
 * 
 * 本實現包括:
 * - 基本的轉帳功能
 * - approve/transferFrom 模式
 * - 元資料支持(名稱、符號、小數位)
 * - 許可事件(Emit)優化
 */
abstract contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address account => uint256) private _balances;
    mapping(address owner => mapping(address spender => uint256)) private _allowances;
    uint256 private _totalSupply;
    string private _name;
    string private _symbol;
    uint8 private _decimals;

    /**
     * @dev 初始化合約
     */
    constructor(string memory name_, string memory symbol_, uint8 decimals_) {
        _name = name_;
        _symbol = symbol_;
        _decimals = decimals_;
    }

    // ==================== IERC20 ====================

    /**
     * @dev 返回代幣總供應量
     */
    function totalSupply() public view virtual returns (uint256) {
        return _totalSupply;
    }

    /**
     * @dev 返回指定帳戶的代幣餘額
     */
    function balanceOf(address account) public view virtual returns (uint256) {
        return _balances[account];
    }

    /**
     * @dev 轉帳代幣到目標地址
     * 
     * 發送方餘額必須足夠,否則 revert
     */
    function transfer(address to, uint256 amount) public virtual returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }

    /**
     * @dev 返回所有者授權給 spend者的額度
     */
    function allowance(address owner, address spender) public view virtual returns (uint256) {
        return _allowances[owner][spender];
    }

    /**
     * @dev 授權 spend者從owner轉帳一定額度
     */
    function approve(address spender, uint256 amount) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, amount);
        return true;
    }

    /**
     * @dev 從 from 轉帳代幣到 to
     * 需要 from 已經授權給調用者
     */
    function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

    // ==================== IERC20Metadata ====================

    function name() public view virtual returns (string memory) {
        return _name;
    }

    function symbol() public view virtual returns (string memory) {
        return _symbol;
    }

    function decimals() public view virtual returns (uint8) {
        return _decimals;
    }

    // ==================== 內部函數 ====================

    /**
     * @dev 內部轉帳邏輯
     */
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        
        _beforeTokenTransfer(from, to, amount);
        
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        
        // 不可重入檢查在這裡不需要,因為狀態在轉帳前更新
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;
        
        emit Transfer(from, to, amount);
        
        _afterTokenTransfer(from, to, amount);
    }

    /**
     * @dev 鑄造新代幣(內部)
     */
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");
        
        _beforeTokenTransfer(address(0), account, amount);
        
        _totalSupply += amount;
        _balances[account] += amount;
        
        emit Transfer(address(0), account, amount);
        
        _afterTokenTransfer(address(0), account, amount);
    }

    /**
     * @dev 銷毀代幣(內部)
     */
    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: burn from the zero address");
        
        _beforeTokenTransfer(account, address(0), amount);
        
        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
        
        _balances[account] = accountBalance - amount;
        _totalSupply -= amount;
        
        emit Transfer(account, address(0), amount);
        
        _afterTokenTransfer(account, address(0), amount);
    }

    /**
     * @dev 授權額度
     */
    function _approve(address owner, address spender, uint256 amount) internal virtual {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        
        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    /**
     * @dev 扣除授權額度
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC20: insufficient allowance");
            _approve(owner, spender, currentAllowance - amount);
        }
    }

    /**
     * @dev Hook:轉帳前調用
     */
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}

    /**
     * @dev Hook:轉帳後調用
     */
    function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}

2.3 具有鑄造和銷毀功能的代幣合約

以下是一個完整的、可管理的代幣合約:

// contracts/token/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "./ERC20.sol";
import {AccessControl} from "../access/AccessControl.sol";
import {Pausable} from "../utils/Pausable.sol";

/**
 * @dev 具有以下功能的 ERC-20 代幣:
 * - 角色基礎的存取控制
 * - 暫停功能(緊急情況)
 * - 限鑄功能
 * - 黑洞地址burn
 */
contract MyToken is ERC20, AccessControl, Pausable {
    // 角色定義
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
    
    // 鑄造限額
    uint256 public constant MAX_MINT_AMOUNT = 1_000_000_000 ether;
    uint256 public mintedSupply;
    
    // 黑洞地址(用於銷毀)
    address public constant BURN_ADDRESS = address(0xdead);

    /**
     * @dev 初始化代幣
     */
    constructor(
        string memory name_,
        string memory symbol_,
        uint8 decimals_
    ) ERC20(name_, symbol_, decimals_) {
        // 設置部署者為默認管理員
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
        _grantRole(BURNER_ROLE, msg.sender);
    }

    /**
     * @dev 鑄造新代幣(只能由具有 MINTER_ROLE 的地址調用)
     */
    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) whenNotPaused {
        require(to != address(0), "Cannot mint to zero address");
        require(amount > 0, "Mint amount must be greater than 0");
        
        // 檢查總鑄造量限額
        require(
            mintedSupply + amount <= type(uint256).max,
            "Total supply would exceed max"
        );
        
        mintedSupply += amount;
        _mint(to, amount);
    }

    /**
     * @dev 批量鑄造
     */
    function mintBatch(address[] calldata recipients, uint256[] calldata amounts)
        external
        onlyRole(MINTER_ROLE)
        whenNotPaused
    {
        require(recipients.length == amounts.length, "Length mismatch");
        
        for (uint256 i = 0; i < recipients.length; i++) {
            mint(recipients[i], amounts[i]);
        }
    }

    /**
     * @dev 銷毀代幣(只能由具有 BURNER_ROLE 的地址調用)
     */
    function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
        require(from != address(0), "Cannot burn from zero address");
        _burn(from, amount);
    }

    /**
     * @dev 用戶自己銷毀持有的代幣
     */
    function burnSelf(uint256 amount) external whenNotPaused {
        _burn(msg.sender, amount);
    }

    /**
     * @dev 轉帳(可暫停)
     */
    function transfer(address to, uint256 amount)
        public
        virtual
        override
        whenNotPaused
        returns (bool)
    {
        return super.transfer(to, amount);
    }

    /**
     * @dev 授權轉帳(可暫停)
     */
    function transferFrom(address from, address to, uint256 amount)
        public
        virtual
        override
        whenNotPaused
        returns (bool)
    {
        return super.transferFrom(from, to, amount);
    }

    /**
     * @dev 緊急暫停所有轉帳
     */
    function pause() external onlyRole(DEFAULT_ADMIN_ROLE) {
        _pause();
    }

    /**
     * @dev 恢復轉帳
     */
    function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
        _unpause();
    }

    /**
     * @dev 鑄造時的鉤子
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20) whenNotPaused {
        super._beforeTokenTransfer(from, to, amount);
    }
}

三、角色基礎存取控制系統

3.1 完整的 AccessControl 實現

// contracts/access/AccessControl.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @dev 基於角色的存取控制(RBAC)實現
 * 
 * 特性:
 * - 多角色支持
 * - 角色繼承
 * - 枚舉角色成員
 */
abstract contract AccessControl {
    struct RoleData {
        mapping(address account => bool) hasRole;
        bytes32 adminRole;
    }

    mapping(bytes32 role => RoleData) private _roles;

    // 默認管理員角色
    bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;

    /**
     * @dev 事件:角色被授予
     */
    event RoleGranted(
        bytes32 indexed role,
        address indexed account,
        address indexed sender
    );

    /**
     * @dev 事件:角色被撤銷
     */
    event RoleRevoked(
        bytes32 indexed role,
        address indexed account,
        address indexed sender
    );

    /**
     * @dev 修飾符:檢查角色
     */
    modifier onlyRole(bytes32 role) {
        checkRole(role);
        _;
    }

    /**
     * @dev 檢查帳戶是否具有角色
     */
    function hasRole(bytes32 role, address account) public view returns (bool) {
        return _roles[role].hasRole[account];
    }

    /**
     * @dev 檢查帳戶是否具有角色,否則 revert
     */
    function checkRole(bytes32 role) internal view virtual {
        checkRole(role, msg.sender);
    }

    /**
     * @dev 檢查指定帳戶是否具有角色
     */
    function checkRole(bytes32 role, address account) internal view virtual {
        if (!hasRole(role, account)) {
            revert AccessControlUnauthorizedAccount(account, role);
        }
    }

    /**
     * @dev 獲取角色的管理員角色
     */
    function getRoleAdmin(bytes32 role) public view returns (bytes32) {
        return _roles[role].adminRole;
    }

    /**
     * @dev 授予角色給帳戶
     */
    function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _grantRole(role, account);
    }

    /**
     * @dev 撤銷角色的帳戶
     */
    function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) {
        _revokeRole(role, account);
    }

    /**
     * @dev 帳戶放棄角色
     */
    function renounceRole(bytes32 role, address account) public virtual {
        require(
            account == msg.sender,
            "Can only renounce roles for yourself"
        );
        _revokeRole(role, account);
    }

    /**
     * @dev 內部:授予角色
     */
    function _grantRole(bytes32 role, address account) internal virtual {
        if (!hasRole(role, account)) {
            _roles[role].hasRole[account] = true;
            emit RoleGranted(role, account, msg.sender);
        }
    }

    /**
     * @dev 內部:撤銷角色
     */
    function _revokeRole(bytes32 role, address account) internal virtual {
        if (hasRole(role, account)) {
            _roles[role].hasRole[account] = false;
            emit RoleRevoked(role, account, msg.sender);
        }
    }

    /**
     * @dev 內部:設置角色的管理員
     */
    function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
        _roles[role].adminRole = adminRole;
    }
}

/**
 * @dev 自定義錯誤
 */
error AccessControlUnauthorizedAccount(address account, bytes32 neededRole);

3.2 具備枚舉功能的擴展

// contracts/access/AccessControlEnumerable.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {AccessControl} from "./AccessControl.sol";

/**
 * @dev 支持枚舉角色成員的存取控制
 */
abstract contract AccessControlEnumerable is AccessControl {
    mapping(bytes32 role => address[]) private _roleMembers;
    mapping(bytes32 role => mapping(address => uint256)) private _roleMemberIndex;

    /**
     * @dev 事件:角色成員變更
     */
    event RoleMemberAdded(bytes32 indexed role, address indexed account);
    event RoleMemberRemoved(bytes32 indexed role, address indexed account);

    /**
     * @dev 獲取角色的成員數量
     */
    function getRoleMemberCount(bytes32 role) public view returns (uint256) {
        return _roleMembers[role].length;
    }

    /**
     * @dev 獲取角色的指定索引成員
     */
    function getRoleMember(bytes32 role, uint256 index) public view returns (address) {
        return _roleMembers[role][index];
    }

    /**
     * @dev 枚舉所有角色成員
     */
    function getRoleMembers(bytes32 role) public view returns (address[] memory) {
        return _roleMembers[role];
    }

    /**
     * @dev 覆蓋:授予角色時添加到成員列表
     */
    function _grantRole(bytes32 role, address account) internal override {
        super._grantRole(role, account);
        _addRoleMember(role, account);
    }

    /**
     * @dev 覆蓋:撤銷角色時從成員列表移除
     */
    function _revokeRole(bytes32 role, address account) internal override {
        super._revokeRole(role, account);
        _removeRoleMember(role, account);
    }

    /**
     * @dev 內部:添加成員
     */
    function _addRoleMember(bytes32 role, address account) internal {
        _roleMemberIndex[role][account] = _roleMembers[role].length;
        _roleMembers[role].push(account);
        emit RoleMemberAdded(role, account);
    }

    /**
     * @dev 內部:移除成員
     */
    function _removeRoleMember(bytes32 role, address account) internal {
        uint256 index = _roleMemberIndex[role][account];
        uint256 lastIndex = _roleMembers[role].length - 1;
        address lastMember = _roleMembers[role][lastIndex];

        _roleMembers[role][index] = lastMember;
        _roleMemberIndex[role][lastMember] = index;
        delete _roleMembers[role][lastIndex];
        _roleMembers[role].pop();
        delete _roleMemberIndex[role][account];

        emit RoleMemberRemoved(role, account);
    }
}

四、可升級代理模式

4.1 代理合約基礎

可升級合約使用代理模式,將存儲和邏輯分離:

// contracts/proxy/Proxy.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @dev 透明代理合約
 * 
 * 設計:
 * - 代理合約持有所有狀態
 * - 實現合約包含邏輯
 * - 升級時更換實現合約地址
 */
abstract contract Proxy {
    /**
     * @dev 委託調用到實現合約
     */
    fallback() external payable {
        assembly {
            // 獲取實現合約地址
            let implementation := sload(implementation.slot)
            
            // 委託調用
            calldatacopy(0x0, 0x0, calldatasize())
            let result := delegatecall(
                gas(),
                implementation,
                0x0,
                calldatasize(),
                0x0,
                0
            )
            
            // 返回結果
            returndatacopy(0x0, 0x0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    /**
     * @dev 接收 ETH
     */
    receive() external payable {}

    /**
     * @dev 實現合約地址的存儲槽
     */
    function implementation() public view returns (address);
}

/**
 * @dev 透明代理
 */
contract TransparentUpgradeableProxy is Proxy {
    /**
     * @dev 初始化代理合約
     */
    constructor(
        address _implementation,
        bytes memory _data
    ) payable {
        assert(
            IMPLEMENTATION_SLOT ==
                bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
        );
        setImplementation(_implementation);
        if (_data.length > 0) {
            (bool success, ) = _implementation.delegatecall(_data);
            require(success);
        }
    }

    /**
     * @dev 獲取實現合約地址
     */
    function implementation() public view override returns (address impl) {
        assembly {
            impl := sload(IMPLEMENTATION_SLOT)
        }
    }

    /**
     * @dev 升級實現合約
     */
    function upgradeTo(address newImplementation) external {
        require(msg.sender == proxyAdmin(), "Only admin");
        setImplementation(newImplementation);
    }

    /**
     * @dev 設置實現合約地址
     */
    function setImplementation(address newImplementation) internal {
        assembly {
            sstore(IMPLEMENTATION_SLOT, newImplementation)
        }
    }

    /**
     * @dev 代理管理員地址
     */
    function proxyAdmin() internal view virtual returns (address);

    bytes32 internal constant IMPLEMENTATION_SLOT =
        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
}

4.2 通用可升級代理

// contracts/proxy/UpgradeableProxy.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {IERC20} from "../interfaces/IERC20.sol";

/**
 * @dev 通用可升級代理
 */
contract UpgradeableProxy {
    bytes32 private constant IMPLEMENTATION_SLOT = 
        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
    bytes32 private constant ADMIN_SLOT = 
        bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);

    /**
     * @dev 初始化
     */
    constructor(address _implementation, address _admin, bytes memory _data) {
        assert(IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
        assert(ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
        
        _setImplementation(_implementation);
        _setAdmin(_admin);
        
        if (_data.length > 0) {
            (bool success, ) = _implementation.delegatecall(_data);
            require(success);
        }
    }

    /**
     * @dev 委託調用
     */
    fallback() external payable {
        _delegate();
    }

    /**
     * @dev 接收 ETH
     */
    receive() external payable {}

    /**
     * @dev 執行委託調用
     */
    function _delegate() internal {
        assembly {
            let ptr := mload(0x40)
            
            // 複製 calldata
            calldatacopy(ptr, 0, calldatasize())
            
            // 委託調用
            let result := delegatecall(
                gas(),
                sload(IMPLEMENTATION_SLOT),
                ptr,
                calldatasize(),
                0,
                0
            )
            
            // 複製 returndata
            returndatacopy(ptr, 0, returndatasize())
            
            // 根據結果返回或revert
            switch result
            case 0 { revert(ptr, returndatasize()) }
            default { return(ptr, returndatasize()) }
        }
    }

    /**
     * @dev 升級實現合約
     */
    function upgradeTo(address newImplementation) external {
        require(msg.sender == _getAdmin(), "Not authorized");
        _setImplementation(newImplementation);
    }

    /**
     * @dev 升級並調用
     */
    function upgradeToAndCall(address newImplementation, bytes calldata data) external payable {
        upgradeTo(newImplementation);
        (bool success, ) = newImplementation.delegatecall(data);
        require(success);
    }

    /**
     * @dev 獲取實現合約地址
     */
    function implementation() external view returns (address) {
        return _getImplementation();
    }

    /**
     * @dev 獲取管理員地址
     */
    function admin() external view returns (address) {
        return _getAdmin();
    }

    /**
     * @dev 內部:獲取實現合約地址
     */
    function _getImplementation() internal view returns (address) {
        return _getSlotValue(IMPLEMENTATION_SLOT);
    }

    /**
     * @dev 內部:設置實現合約地址
     */
    function _setImplementation(address value) internal {
        _setSlotValue(IMPLEMENTATION_SLOT, value);
    }

    /**
     * @dev 內部:獲取管理員地址
     */
    function _getAdmin() internal view returns (address) {
        return _getSlotValue(ADMIN_SLOT);
    }

    /**
     * @dev 內部:設置管理員地址
     */
    function _setAdmin(address value) internal {
        _setSlotValue(ADMIN_SLOT, value);
    }

    /**
     * @dev 內部:從存儲槽獲取值
     */
    function _getSlotValue(bytes32 slot) internal view returns (address value) {
        assembly {
            value := sload(slot)
        }
    }

    /**
     * @dev 內部:設置存儲槽的值
     */
    function _setSlotValue(bytes32 slot, address value) internal {
        assembly {
            sstore(slot, value)
        }
    }
}

4.3 實現合約模板

// contracts/implementation/MyTokenV1.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "../token/ERC20.sol";
import {AccessControlEnumerable} from "../access/AccessControlEnumerable.sol";

/**
 * @dev 可升級代幣合約 V1
 * 
 * 注意:此合約不應直接部署,而是通過代理部署
 */
contract MyTokenV1 is ERC20, AccessControlEnumerable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    /**
     * @dev 初始化函數(相當於構造函數,但在代理模式下使用)
     */
    function initialize(
        string memory name_,
        string memory symbol_,
        uint8 decimals_,
        address admin_
    ) external {
        // 防止重複初始化
        require(
            _msgSender() == admin_ && bytes32(_roles[DEFAULT_ADMIN_ROLE].hasRole[admin_]) == bytes32(0),
            "Already initialized"
        );

        _name = name_;
        _symbol = symbol_;
        _decimals = decimals_;

        _grantRole(DEFAULT_ADMIN_ROLE, admin_);
        _grantRole(MINTER_ROLE, admin_);
        _grantRole(BURNER_ROLE, admin_);
    }

    /**
     * @dev 鑄造代幣
     */
    function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    /**
     * @dev 銷毀代幣
     */
    function burn(uint256 amount) external onlyRole(BURNER_ROLE) {
        _burn(msg.sender, amount);
    }

    /**
     * @dev 轉帳(包含鉤子)
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20) {
        super._beforeTokenTransfer(from, to, amount);
    }
}

五、完整測試覆蓋

5.1 Foundry 測試框架

使用 Foundry (Forge) 編寫全面的單元測試:

// test/MyToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {MyToken} from "../contracts/token/MyToken.sol";

contract MyTokenTest is Test {
    MyToken public token;
    
    address public owner;
    address public minter;
    address public user1;
    address public user2;

    uint256 constant INITIAL_SUPPLY = 1000000 ether;

    /**
     * @dev 測試前設置
     */
    function setUp() public {
        owner = makeAddr("owner");
        minter = makeAddr("minter");
        user1 = makeAddr("user1");
        user2 = makeAddr("user2");

        // 部署合約
        vm.prank(owner);
        token = new MyToken("My Token", "MTK", 18);
    }

    // ==================== 基本功能測試 ====================

    /**
     * @test 代幣的基本屬性
     */
    function testTokenMetadata() public view {
        assertEq(token.name(), "My Token");
        assertEq(token.symbol(), "MTK");
        assertEq(token.decimals(), 18);
    }

    /**
     * @test 代幣總供應量
     */
    function testTotalSupply() public view {
        assertEq(token.totalSupply(), INITIAL_SUPPLY);
    }

    /**
     * @test 部署者擁有初始供應量
     */
    function testOwnerHasInitialSupply() public view {
        assertEq(token.balanceOf(address(this)), INITIAL_SUPPLY);
    }

    // ==================== 轉帳測試 ====================

    /**
     * @test 正常轉帳
     */
    function testTransfer() public {
        uint256 amount = 100 ether;
        
        // 轉帳
        token.transfer(user1, amount);
        
        // 驗證餘額
        assertEq(token.balanceOf(user1), amount);
        assertEq(token.balanceOf(address(this)), INITIAL_SUPPLY - amount);
    }

    /**
     * @test 轉帳失敗:餘額不足
     */
    function testTransferInsufficientBalance() public {
        uint256 amount = INITIAL_SUPPLY + 1 ether;
        
        vm.expectRevert("ERC20: transfer amount exceeds balance");
        token.transfer(user1, amount);
    }

    /**
     * @test 轉帳失敗:目標為零地址
     */
    function testTransferToZeroAddress() public {
        vm.expectRevert("ERC20: transfer to the zero address");
        token.transfer(address(0), 100 ether);
    }

    /**
     * @test transferFrom
     */
    function testTransferFrom() public {
        uint256 approvalAmount = 100 ether;
        uint256 transferAmount = 50 ether;

        // 授權
        vm.prank(user1);
        token.approve(address(this), approvalAmount);

        // 轉帳
        token.transferFrom(user1, user2, transferAmount);

        assertEq(token.balanceOf(user2), transferAmount);
        assertEq(token.allowance(user1, address(this)), approvalAmount - transferAmount);
    }

    // ==================== 角色測試 ====================

    /**
     * @test 授權鑄造
     */
    function testMintByNonMinter() public {
        vm.prank(user1);
        vm.expectRevert(); // AccessControlUnauthorizedAccount
        token.mint(user2, 100 ether);
    }

    /**
     * @test 授權鑄造成功
     */
    function testMintByMinter() public {
        uint256 mintAmount = 1000 ether;
        uint256 initialSupply = token.totalSupply();

        vm.prank(owner);
        token.mint(user1, mintAmount);

        assertEq(token.balanceOf(user1), mintAmount);
        assertEq(token.totalSupply(), initialSupply + mintAmount);
    }

    /**
     * @test 批量鑄造
     */
    function testBatchMint() public {
        address[] memory recipients = new address[](3);
        recipients[0] = user1;
        recipients[1] = user2;
        recipients[2] = owner;

        uint256[] memory amounts = new uint256[](3);
        amounts[0] = 100 ether;
        amounts[1] = 200 ether;
        amounts[2] = 300 ether;

        vm.prank(owner);
        token.mintBatch(recipients, amounts);

        assertEq(token.balanceOf(user1), 100 ether);
        assertEq(token.balanceOf(user2), 200 ether);
        assertEq(token.balanceOf(owner), INITIAL_SUPPLY + 300 ether);
    }

    // ==================== 暫停功能測試 ====================

    /**
     * @test 暫停後轉帳失敗
     */
    function testTransferWhenPaused() public {
        vm.prank(owner);
        token.pause();

        vm.expectRevert("Pausable: paused");
        token.transfer(user1, 100 ether);
    }

    /**
     * @test 暫停後恢復轉帳
     */
    function testUnpause() public {
        vm.prank(owner);
        token.pause();

        vm.prank(owner);
        token.unpause();

        token.transfer(user1, 100 ether);
        assertEq(token.balanceOf(user1), 100 ether);
    }

    // ==================== 邊界條件測試 ====================

    /**
     * @test 零金額轉帳
     */
    function testZeroValueTransfer() public {
        token.transfer(user1, 0);
        assertEq(token.balanceOf(user1), 0);
    }

    /**
     * @test 大量轉帳
     */
    function testLargeTransfer() public {
        token.transfer(user1, INITIAL_SUPPLY);
        assertEq(token.balanceOf(user1), INITIAL_SUPPLY);
        assertEq(token.balanceOf(address(this)), 0);
    }

    /**
     * @test 多重轉帳
     */
    function testMultipleTransfers() public {
        token.transfer(user1, 100 ether);
        token.transfer(user1, 200 ether);
        
        assertEq(token.balanceOf(user1), 300 ether);
    }
}

5.2 模糊測試

// test/fuzz/TokenFuzz.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {MyToken} from "../contracts/token/MyToken.sol";

/**
 * @dev 模糊測試合約
 */
contract TokenFuzzTest is Test {
    MyToken public token;
    address[] public users;

    function setUp() public {
        token = new MyToken("Test", "TST", 18);
        
        // 創建測試用戶
        for (uint256 i = 0; i < 10; i++) {
            users.push(makeAddr(string(abi.encodePacked("user", i))));
        }
    }

    /**
     * @ fuzz_test 隨機轉帳
     */
    function testFuzzTransfer(uint256 amount, uint8 senderIdx, uint8 receiverIdx) public {
        // 限制 amount 大小,避免 overflow
        amount = bound(amount, 0, 1000000 ether);
        senderIdx = uint8(bound(senderIdx, 0, users.length - 1));
        receiverIdx = uint8(bound(receiverIdx, 0, users.length - 1));
        
        address sender = users[senderIdx];
        address receiver = users[receiverIdx];
        
        // 確保發送方有足夠餘額
        uint256 senderBalance = token.balanceOf(sender);
        amount = bound(amount, 0, senderBalance);
        
        if (amount > 0) {
            vm.prank(sender);
            token.transfer(receiver, amount);
            
            assertEq(
                token.balanceOf(sender),
                senderBalance - amount
            );
            assertEq(
                token.balanceOf(receiver),
                token.balanceOf(receiver) + amount
            );
        }
    }

    /**
     * @ fuzz_test 隨機 approve 和 transferFrom
     */
    function testFuzzApproveAndTransferFrom(
        uint256 approvalAmount,
        uint256 transferAmount,
        uint8 approverIdx,
        uint8 spenderIdx,
        uint8 receiverIdx
    ) public {
        approverIdx = uint8(bound(approverIdx, 0, users.length - 1));
        spenderIdx = uint8(bound(spenderIdx, 0, users.length - 1));
        receiverIdx = uint8(bound(receiverIdx, 0, users.length - 1));
        
        address approver = users[approverIdx];
        address spender = users[spenderIdx];
        address receiver = users[receiverIdx];
        
        approvalAmount = bound(approvalAmount, 0, 1000000 ether);
        
        vm.prank(approver);
        token.approve(spender, approvalAmount);
        
        assertEq(token.allowance(approver, spender), approvalAmount);
        
        transferAmount = bound(transferAmount, 0, approvalAmount);
        
        if (transferAmount > 0) {
            vm.prank(spender);
            token.transferFrom(approver, receiver, transferAmount);
            
            assertEq(
                token.allowance(approver, spender),
                approvalAmount - transferAmount
            );
        }
    }
}

六、安全最佳實踐清單

6.1 開發階段檢查清單

開發階段安全檢查:

[x] 輸入驗證
    - 檢查地址不為零
    - 檢查金額不為負數
    - 檢查數組長度一致

[x] 訪問控制
    - 實現角色管理
    - 驗證權限
    - 防止未授權訪問

[x] 重入保護
    - 使用 Checks-Effects-Interactions 模式
    - 使用 ReentrancyGuard
    - 狀態更新在外部調用之前

[x] 整數安全
    - 使用 Solidity 0.8+ 內建檢查
    - 或使用 SafeMath
    - 驗證邊界條件

[x] 事件記錄
    - 記錄所有狀態變更
    - 使用 indexed 參數
    - 包含足夠上下文

6.2 部署前檢查清單

部署前安全檢查:

[x] 程式碼審計
    - 專業審計機構審計
    - 社區審計
    - 同行評審

[x] 測試覆蓋
    - 單元測試 > 90%
    - 集成測試
    - 模糊測試
    - 形式化驗證(可選)

[x] 升級機制
    - 代理模式測試
    - 緊急暫停功能
    - 速率限制

[x] 經濟模型審查
    - 代幣經濟學分析
    - 激勵相容性
    - 攻擊向量分析

6.3 運行時監控

/**
 * @dev 建議的運行時監控指標
 */

enum AlertLevel {
    INFO,      // 資訊
    WARNING,   // 警告
    CRITICAL   // 緊急
}

struct Alert {
    AlertLevel level;
    string message;
    uint256 timestamp;
}

/**
 * 需要監控的事件:
 * 
 * 1. 大額轉帳
 *    - 閾值:代幣供應量的 1%
 * 
 * 2. 異常 mint/burn
 *    - 頻率異常
 *    - 數量異常
 * 
 * 3. 權限變更
 *    - 新管理員
 *    - 角色變更
 * 
 * 4. 暫停/解暫停
 *    - 應急響應
 * 
 * 5. 合約升級
 *    - 實現合約變更
 */

結論

本文詳細介紹了如何使用 Solidity 編寫生產級的智慧合約。從基礎的 ERC-20 實現到複雜的可升級代理模式,每個組件都遵循了安全第一的原則。

關鍵要點回顧:

  1. 模組化設計:將合約拆分為獨立的功能模組,便於測試和維護
  2. 角色基礎的存取控制:精確控制不同角色的權限
  3. 可升級性:使用代理模式實現合約升級,同時保持狀態
  4. 全面的測試:單元測試、模糊測試和集成測試確保合約正確性
  5. 安全最佳實踐:遵循 Checks-Effects-Interactions 模式,使用 SafeMath 或內建溢位檢查

智慧合約開發是一個持續學習的過程。建議開發者:

參考資源

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!