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 實現到複雜的可升級代理模式,每個組件都遵循了安全第一的原則。
關鍵要點回顧:
- 模組化設計:將合約拆分為獨立的功能模組,便於測試和維護
- 角色基礎的存取控制:精確控制不同角色的權限
- 可升級性:使用代理模式實現合約升級,同時保持狀態
- 全面的測試:單元測試、模糊測試和集成測試確保合約正確性
- 安全最佳實踐:遵循 Checks-Effects-Interactions 模式,使用 SafeMath 或內建溢位檢查
智慧合約開發是一個持續學習的過程。建議開發者:
- 持續關注以太坊安全最佳實踐
- 使用經過審計的開源庫(如 OpenZeppelin)
- 定期進行安全審計
- 建立完善的監控和應急響應機制
參考資源
- OpenZeppelin Contracts:https://docs.openzeppelin.com/contracts
- Solidity 文件:https://docs.soliditylang.org
- Foundry 文件:https://book.getfoundry.sh
- Mythril 安全工具:https://github.com/ConsenSys/mythril
- Slither 靜態分析:https://github.com/crytic/slither
相關文章
- 可升級智慧合約完整指南:代理模式、UUPS、透明代理與安全性分析 — 本指南深入探討以太坊可升級智慧合約的技術原理與實踐。內容涵蓋基礎代理合約、透明代理、UUPS 模式、Beacon 代理的完整實現,以及存儲管理、版本兼容性、安全考量等關鍵主題。提供可直接部署的生產級代碼範例和安全檢查清單。
- Solidity 智慧合約實戰範例完整指南:2026 年最新語法與最佳實踐 — Solidity 是以太坊智慧合約開發的主要程式語言,近年來持續演進。2025-2026 年,Solidity 語言在類型安全、Gas 優化、合約可升級性等方面都有重要更新。本文提供全面的 Solidity 實戰範例,涵蓋從基礎合約到進階模式的完整程式碼,幫助開發者快速掌握 2026 年最新的 Solidity 開發技術。
- Solidity Gas 最佳化實踐完整指南:2026 年最新技術 — Gas 最佳化是以太坊智能合約開發中至關重要的課題,直接影響合約的部署成本和用戶的交易費用。隨著以太坊網路的發展和各類 Layer 2 解決方案的成熟,Gas 最佳化的策略也在持續演進。2025-2026 年期間,EIP-7702 的實施、Proto-Danksharding 帶來的 Blob 資料成本降低、以及各類新型最佳化技術的出現,都為 Gas 最佳化帶來了新的維度。本指南將從工程師視角深入
- Solidity 隱私合約開發進階指南:承諾、Merkle 樹與零知識證明整合 — 在以太坊區塊鏈上構建隱私保護應用是一項具有挑戰性的任務,因為所有交易數據預設都是公開的。然而,通過結合密碼學技術與智能合約設計,開發者可以實現多種隱私保護功能。本文將深入探討使用 Solidity 構建隱私合約的核心技術:承諾方案(Commitment Schemes)、Merkle 證明驗證、以及與鏈下零知識證明的整合。我們將通過實際的代碼示例來展示這些技術的實現細節,幫助開發者構建真正的隱私保
- EIP-7702 實際應用場景完整指南:從理論到生產環境部署 — EIP-7702 是以太坊帳戶抽象演進歷程中的重要里程碑,允許外部擁有帳戶(EOA)在交易執行期間臨時獲得智慧合約的功能。本文深入探討 EIP-7702 的實際應用場景,包括社交恢復錢包、批量交易、自動化執行和多重簽名等,提供完整的開發指南與程式碼範例,並探討從概念驗證到生產環境部署的最佳實踐。
延伸閱讀與來源
- Ethereum.org Developers 官方開發者入口與技術文件
- EIPs 以太坊改進提案
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!