Solidity 程式碼編譯練習完整指南:從線上工具到本地開發環境

本文提供一個完整的 Solidity 程式碼編譯練習指南,涵蓋從線上 IDE 到本地開發環境的多種練習方式。從最基礎的 Hello World 合約開始,逐步過渡到 ERC-20 代幣合約、去中心化投票系統等實際應用。包含完整的程式碼範例、詳細的語法解釋、以及使用 Remix IDE 和 Hardhat 的實際操作步驟。

Solidity 程式碼編譯練習完整指南:從線上工具到本地開發環境

概述

學習以太坊智慧合約開發的最佳方式是實際動手編寫和部署程式碼。本文提供一個完整的 Solidity 程式碼編譯練習指南,涵蓋從線上 IDE 到本地開發環境的多種練習方式。我們將帶領讀者通過一系列由淺入深的練習專案,逐步掌握 Solidity 語言的核心概念和開發技巧。

Solidity 是以太坊智慧合約開發的主要程式語言,目前版本已經發展到 0.8.x 系列。作為一種靜態類型語言,Solidity 借鑒了 JavaScript、Python 和 C++ 的語法特性,同時針對區塊鏈環境進行了大量優化,例如原生的地址類型、事件日誌機制、以及ether 和 gas 相關的特殊語法。

本文適合完全沒有程式設計經驗的初學者,以及希望系統性提升智慧合約開發技能的進階讀者。我們的練習專案將從最基礎的「Hello World」合約開始,逐步過渡到具有實際應用價值的 ERC-20 代幣合約、去中心化投票系統、以及簡單的借貸合約。每個練習都包含完整的程式碼範例、詳細的語法解釋、以及實際操作的步驟說明。

通過完成本文的所有練習,讀者將能夠:理解 Solidity 語言的基礎語法和類型系統、掌握智慧合約的開發調試流程、能夠獨立編寫和部署基本的智慧合約、了解智慧合約的安全性考量,並具備進一步學習進階主題的基礎。

第一部分:線上 IDE 快速上手

Remix IDE 介紹

Remix IDE 是以太坊官方推薦的智慧合約線上開發環境,無需安裝任何軟體,通過瀏覽器即可使用。它提供了完整的智慧合約開發工具鏈,包括:程式碼編輯器、編譯器、部署工具、調試器、以及測試框架。

訪問 remix.ethereum.org 即可開始使用 Remix IDE。頁面載入後,你會看到一個預設的 Solidity 檔案「1_Storage.sol」,這是一個簡單的存儲合約範例。我們將從這個範例開始,逐步學習 Remix IDE 的使用方法。

Remix IDE 的介面主要分為四個區域:左側是檔案瀏覽器,用於管理專案中的多個檔案;中間是程式碼編輯器,用於編寫和查看程式碼;左下角是編譯控制面板,用於編譯合約;右下角是部署和運行面板,用於將合約部署到區塊鏈或本地模擬器。

第一個練習:Hello World 合約

讓我們從最基本的練習開始,創建一個簡單的「Hello World」智慧合約。

在 Remix IDE 中,點擊左上角的「New File」按鈕,創建一個名為「HelloWorld.sol」的新檔案。然後,輸入以下程式碼:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract HelloWorld {
    // 存儲訊息的變量
    string private message;
    
    // 事件:用於記錄訊息變更
    event MessageChanged(string oldMessage, string newMessage);
    
    // 建構函數:合約部署時執行
    constructor() {
        message = "Hello, Ethereum!";
    }
    
    // 獲取訊息的函數
    function getMessage() public view returns (string memory) {
        return message;
    }
    
    // 設定訊息的函數
    function setMessage(string calldata _message) public {
        string memory oldMessage = message;
        message = _message;
        emit MessageChanged(oldMessage, _message);
    }
}

讓我們解讀這段程式碼的各個部分:

版權聲明和編譯器版本

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

第一行是 SPDX 版權標識,說明合約使用 MIT 許可證。第二行指定編譯器版本,「^0.8.19」表示接受 0.8.19 或更高版本的 Solidity 編譯器(但在 0.9.0 之前)。

合約定義

contract HelloWorld { ... }

Solidity 中使用 contract 關鍵字定義智慧合約,類似於其他語言中的 class(類)。

狀態變量

string private message;

狀態變量是永久存儲在區塊鏈上的資料。private 關鍵字表示該變量只能從合約內部訪問(儘管區塊鏈上的任何人都可以看到這些資料,只是不能直接讀取)。

事件

event MessageChanged(string oldMessage, string newMessage);

事件是用於記錄區塊鏈上發生的事情的機制。客戶端可以「監聽」事件來獲取合約狀態的變化通知。

建構函數

constructor() { ... }

建構函數在合約部署時執行一次,用於初始化合約的初始狀態。

函數

Solidity 中的函數可以分為兩類:view 函數(只讀,不會修改區塊鏈狀態)和非 view 函數(會修改狀態,需要支付 Gas)。

編譯合約

現在讓我們編譯剛才編寫的合約:

  1. 點擊左側菜單中的「Solidity Compiler」圖示(看起來像個向下箭頭)
  2. 確認編譯器版本選擇為 0.8.19(或你使用的版本)
  3. 點擊「Compile HelloWorld.sol」按鈕
  4. 觀察編譯結果:如果成功,會顯示綠色的勾選標記和「Compilation successful」訊息;如果有錯誤,會顯示具體的錯誤訊息和行號

常見的編譯錯誤包括:語法錯誤(拼寫錯誤、缺少分號)、類型錯誤(類型不匹配的賦值)、以及版本兼容性問題。

部署合約

編譯成功後,讓我們將合約部署到區塊鏈上進行測試:

  1. 點擊左側菜單中的「Deploy & Run Transactions」圖示
  2. 在「Environment」下拉選單中,選擇「Injected Provider - MetaMask」
  3. 如果尚未連接 MetaMask,會彈出連接請求,點擊「連接」
  4. 確認 MetaMask 中選擇的是 Sepolia 測試網路
  5. 點擊「Deploy」按鈕
  6. 在 MetaMask 中確認交易,點擊「確認」

部署完成後,你會在「Deployed Contracts」區域看到新部署的合約。點擊合約名稱可以展開所有可呼叫的函數。

與合約互動

現在讓我們嘗試呼叫合約的函數:

  1. 點擊「getMessage」函數旁的橙色的「Call」按鈕
  2. 觀察下方的終端輸出,應該顯示返回值「Hello, Ethereum!」(這是我們在建構函數中設定的初始值)

現在讓我們修改訊息:

  1. 在「setMessage」函數的輸入框中,輸入新的訊息,例如「Hello, Web3!」
  2. 點擊藍色的「Transact」按鈕
  3. MetaMask 會彈出交易確認視窗,顯示交易詳情:
  1. 點擊「確認」提交交易
  2. 等待交易被區塊確認(大約 10-20 秒)
  3. 再次呼叫「getMessage」函數,確認訊息已經更新

這個練習展示了智慧合約的完整生命週期:編寫、編譯、部署、以及互動。

第二部分:Solidity 基礎語法練習

資料類型練習

Solidity 提供了豐富的資料類型。讓我們通過練習來掌握這些類型的使用:

整數類型

Solidity 支持有符號和無符號整數,寬度從 8 位元到 256 位元:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract DataTypesPractice {
    // 無符號整數
    uint8 public smallUint = 255;      // 0 到 255
    uint16 public mediumUint = 65535;  // 0 到 65535
    uint256 public largeUint = 1000;   // 0 到 2^256-1
    
    // 有符號整數
    int8 public smallInt = -128;       // -128 到 127
    int256 public largeInt = -1000;    // -2^255 到 2^255-1
    
    // 地址類型
    address public owner = msg.sender;  // 160 位元地址
    
    // 位元組類型
    bytes32 public data = "Hello";     // 固定長度位元組陣列
    
    // 布林類型
    bool public flag = true;
    
    // 枚舉類型
    enum Status { Pending, Active, Completed, Cancelled }
    Status public currentStatus = Status.Pending;
    
    // 陣列
    uint[] public numbers = [1, 2, 3, 4, 5];
    
    // 結構體
    struct Person {
        string name;
        uint age;
        address wallet;
    }
    
    Person[] public people;
    
    // 新增人員
    function addPerson(string memory _name, uint _age) public {
        people.push(Person(_name, _age, msg.sender));
    }
    
    // 獲取人員數量
    function getPeopleCount() public view returns (uint) {
        return people.length;
    }
}

練習要求

  1. 部署這個合約
  2. 呼叫 addPerson 函數添加幾個人員
  3. 呼叫 getPeopleCount 確認人員已添加
  4. 嘗試將 smallUint 設定為 256,觀察會發生什麼錯誤(會發生溢位錯誤)

控制流練習

Solidity 的控制流語法與其他 C 系列語言類似:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract ControlFlowPractice {
    uint[] public numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // 條件判斷
    function getResultIfElse(uint _input) public pure returns (string memory) {
        if (_input > 100) {
            return "大於 100";
        } else if (_input > 50) {
            return "大於 50 但小於等於 100";
        } else {
            return "小於等於 50";
        }
    }
    
    // For 迴圈:計算總和
    function getSum() public view returns (uint) {
        uint sum = 0;
        for (uint i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
        return sum;
    }
    
    // While 迴圈:找最大值
    function getMax() public view returns (uint) {
        uint max = numbers[0];
        uint i = 1;
        while (i < numbers.length) {
            if (numbers[i] > max) {
                max = numbers[i];
            }
            i++;
        }
        return max;
    }
    
    // 迴圈中的 break 和 continue
    function findFirstEven() public view returns (uint) {
        for (uint i = 0; i < numbers.length; i++) {
            if (numbers[i] % 2 == 0) {
                return numbers[i];  // 找到第一個偶數就返回
            }
        }
        return 0;  // 沒有找到偶數
    }
}

函數可見性和修飾符練習

Solidity 中的函數有不同的可見性和特殊修飾符:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract FunctionModifiersPractice {
    address public owner;
    uint public counter = 0;
    bool public paused = false;
    
    constructor() {
        owner = msg.sender;
    }
    
    // 函數修飾符:檢查是否是 owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this");
        _;
    }
    
    // 函數修飾符:檢查是否暫停
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
    
    // 公開函數:任何人都可以調用
    function incrementPublic() public whenNotPaused {
        counter++;
    }
    
    // 外部函數:只能從合約外部調用
    function incrementExternal() external whenNotPaused {
        counter++;
    }
    
    // 內部函數:只能在合約內部調用
    function _internalHelper() internal view returns (address) {
        return owner;
    }
    
    // 私有函數:只能在定義它的合約內部調用
    function _privateHelper() private pure returns (bool) {
        return true;
    }
    
    // 只能從 owner 調用的函數
    function setPaused(bool _paused) public onlyOwner {
        paused = _paused;
    }
    
    // view 函數:承諾不修改狀態
    function getOwner() public view returns (address) {
        return owner;
    }
    
    // pure 函數:承諾既不讀取也不修改狀態
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }
    
    // payable 函數:可以接收以太幣
    function deposit() public payable {}
    
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

第三部分:ERC-20 代幣合約練習

什麼是 ERC-20

ERC-20 是以太坊上代幣(Token)的標準介面規範。遵循這個標準的代幣可以被錢包、交易所和其他應用程式統一識別和管理。截至 2026 年第一季度,已有數十萬種 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);
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

練習:實現完整的 ERC-20 代幣

讓我們通過練習來實現一個完整的 ERC-20 代幣合約:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract MyToken {
    // 代幣名稱
    string public constant name = "MyToken";
    
    // 代幣符號
    string public constant symbol = "MTK";
    
    // 小數位數(精度)
    uint8 public constant decimals = 18;
    
    // 總供應量
    uint256 private _totalSupply;
    
    // 餘額映射:地址 -> 餘額
    mapping(address => uint256) private _balances;
    
    // 授權映射:所有者 -> 被授權者 -> 授權數量
    mapping(address => mapping(address => uint256)) private _allowances;
    
    // 事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    
    // 建構函數:鑄造初始代幣
    constructor(uint256 initialSupply) {
        _totalSupply = initialSupply * 10 ** decimals;
        _balances[msg.sender] = _totalSupply;
        emit Transfer(address(0), msg.sender, _totalSupply);
    }
    
    // 獲取總供應量
    function totalSupply() public view returns (uint256) {
        return _totalSupply;
    }
    
    // 獲取帳戶餘額
    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }
    
    // 轉帳
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "Transfer to zero address");
        require(_balances[msg.sender] >= amount, "Insufficient balance");
        
        _balances[msg.sender] -= amount;
        _balances[to] += amount;
        
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    
    // 獲取授權額度
    function allowance(address owner, address spender) public view returns (uint256) {
        return _allowances[owner][spender];
    }
    
    // 授權
    function approve(address spender, uint256 amount) public returns (bool) {
        require(spender != address(0), "Approve to zero address");
        
        _allowances[msg.sender][spender] = amount;
        
        emit Approval(msg.sender, spender, amount);
        return true;
    }
    
    // 從其他帳戶轉帳(需要預先授權)
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "Transfer to zero address");
        require(_balances[from] >= amount, "Insufficient balance");
        require(_allowances[from][msg.sender] >= amount, "Allowance exceeded");
        
        _balances[from] -= amount;
        _allowances[from][msg.sender] -= amount;
        _balances[to] += amount;
        
        emit Transfer(from, to, amount);
        return true;
    }
    
    // 增加授權額度
    function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
        require(spender != address(0), "Increase allowance to zero address");
        
        _allowances[msg.sender][spender] += addedValue;
        emit Approval(msg.sender, spender, _allowances[msg.sender][spender]);
        return true;
    }
    
    // 減少授權額度
    function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
        require(spender != address(0), "Decrease allowance to zero address");
        require(_allowances[msg.sender][spender] >= subtractedValue, "Below zero");
        
        _allowances[msg.sender][spender] -= subtractedValue;
        emit Approval(msg.sender, spender, _allowances[msg.sender][spender]);
        return true;
    }
}

練習步驟

  1. 將這段程式碼複製到 Remix IDE 中
  2. 在部署時,輸入初始供應量(例如 1000)
  3. 部署後,你會獲得全部的代幣
  4. 嘗試轉帳一部分代幣到另一個地址
  5. 嘗試使用 approvetransferFrom 進行授權轉帳

OpenZeppelin ERC-20 合約

在實際生產環境中,通常不會從頭編寫 ERC-20 合約,而是使用經過審計的 OpenZeppelin 庫:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
}

使用 OpenZeppelin 的優勢包括:經過社區審計的安全程式碼、完整的功能實現、以及易於擴展(可以添加鑄造、銷毀等功能)。

第四部分:本地開發環境練習

Hardhat 環境搭建

對於更複雜的專案,建議使用 Hardhat 作為本地開發環境。Hardhat 提供了更強大的功能和靈活性:

  1. 首先,確保已安裝 Node.js(版本 16 或以上)
  2. 創建新項目資料夾並初始化:
mkdir my-token-project
cd my-token-project
npm init -y
npm install --save-dev hardhat
npx hardhat init
  1. 選擇「Create a JavaScript project」創建項目
  2. 項目結構如下:
my-token-project/
├── contracts/           # 智慧合約原始碼
├── scripts/             # 部署腳本
├── test/                # 測試檔案
├── hardhat.config.js    # Hardhat 配置
└── package.json         # 專案依賴

部署腳本練習

創建一個部署腳本來部署 ERC-20 代幣合約:

// scripts/deploy.js

const hre = require("hardhat");

async function main() {
    console.log("部署 ERC-20 代幣合約...");
    
    // 獲取合約工廠
    const MyToken = await hre.ethers.getContractFactory("MyToken");
    
    // 部署合約,傳入初始供應量參數
    const token = await MyToken.deploy(1000000);
    
    // 等待部署完成
    await token.deployed();
    
    // 輸出部署後的合約地址
    console.log("代幣合約已部署到:", token.address);
    
    // 獲取部署者地址
    const [deployer] = await hre.ethers.getSigners();
    console.log("部署者地址:", deployer.address);
    
    // 檢查部署者的餘額
    const balance = await token.balanceOf(deployer.address);
    console.log("部署者餘額:", hre.ethers.utils.formatEther(balance), "MTK");
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

測試腳本練習

為代幣合約編寫測試:

// test/MyToken.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function() {
    let token;
    let owner;
    let addr1;
    let addr2;
    
    // 在每個測試案例前部署合約
    beforeEach(async function() {
        const MyToken = await ethers.getContractFactory("MyToken");
        [owner, addr1, addr2] = await ethers.getSigners();
        token = await MyToken.deploy(1000);
        await token.deployed();
    });
    
    // 測試代幣名稱和符號
    it("should have correct name and symbol", async function() {
        expect(await token.name()).to.equal("MyToken");
        expect(await token.symbol()).to.equal("MTK");
    });
    
    // 測試初始供應量
    it("should assign total supply to owner", async function() {
        const ownerBalance = await token.balanceOf(owner.address);
        expect(await token.totalSupply()).to.equal(ownerBalance);
    });
    
    // 測試轉帳功能
    it("should transfer tokens between accounts", async function() {
        // 從 owner 轉帳 50 個代幣到 addr1
        await token.transfer(addr1.address, 50);
        expect(await token.balanceOf(addr1.address)).to.equal(50);
        
        // addr1 轉帳 20 個代幣到 addr2
        await token.connect(addr1).transfer(addr2.address, 20);
        expect(await token.balanceOf(addr2.address)).to.equal(20);
        expect(await token.balanceOf(addr1.address)).to.equal(30);
    });
    
    // 測試餘額不足的情況
    it("should fail when sender doesn't have enough tokens", async function() {
        const initialOwnerBalance = await token.balanceOf(owner.address);
        
        await expect(
            token.connect(addr1).transfer(owner.address, 1)
        ).to.be.revertedWith("Insufficient balance");
        
        expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
    });
    
    // 測試授權和轉帳
    it("should update allowances", async function() {
        // owner 授權 addr1 使用 100 個代幣
        await token.approve(addr1.address, 100);
        expect(await token.allowance(owner.address, addr1.address)).to.equal(100);
        
        // addr1 使用授權轉帳
        await token.connect(addr1).transferFrom(owner.address, addr2.address, 50);
        expect(await token.balanceOf(addr2.address)).to.equal(50);
        expect(await token.allowance(owner.address, addr1.address)).to.equal(50);
    });
});

運行測試:

npx hardhat test

如果所有測試通過,你應該能看到類似的輸出:

MyToken
    ✓ should have correct name and symbol
    ✓ should assign total supply to owner
    ✓ should transfer tokens between accounts
    ✓ should fail when sender doesn't have enough tokens
    ✓ should update allowances
    
  5 passing (2s)

第五部分:常見錯誤與調試技巧

常見編譯錯誤

語法錯誤

// 錯誤:忘記分號
uint256 public myVar = 42

// 正確
uint256 public myVar = 42;

類型錯誤

// 錯誤:嘗試將 address 賦值給 uint256
uint256 public myUint = msg.sender;

// 正確
address public myAddress = msg.sender;

可見性錯誤

// 錯誤:狀態變量必須指定可見性
uint256 publicBalance = 100;

// 正確
uint256 public publicBalance = 100;

常見運行時錯誤

Require 失敗

// 這個交易會失敗並回滾
function testRequire(uint256 _input) public pure {
    require(_input > 10, "Input must be greater than 10");
}

陣列越界

function getArrayValue(uint256 index) public view returns (uint256) {
    return myArray[index];  // 如果 index >= myArray.length,會失敗
}

整數溢位

function testOverflow() public pure returns (uint8) {
    uint8 public a = 255;
    a += 1;  // 會導致溢位錯誤(在 Solidity 0.8+ 中會自動檢查)
}

Remix 調試工具使用

Remix IDE 提供了強大的調試工具:

  1. 錯誤定位:點擊編譯錯誤訊息,可以直接跳轉到出錯的行號
  2. 交易調試:在「Deploy & Run Transactions」面板中,點擊已完成的交易,可以看到詳細的執行過程
  3. 日誌輸出:使用 console.log(需要引入 Hardhat 的 console.sol)可以在調試時輸出變數值

結論

通過本文的練習,我們從最基本的「Hello World」合約開始,逐步深入到 ERC-20 代幣合約的實現,以及使用 Hardhat 進行專業級的開發和測試。這些練習涵蓋了 Solidity 開發的核心知識點,包括:

建議讀者在完成這些練習後,嘗試擴展這些專案,例如:為代幣添加鑄造和銷毀功能、實現投票合約、或創建簡單的拍賣系統。這些擴展練習將幫助你進一步鞏固所學知識,並為開發更複雜的智慧合約打下堅實的基礎。


延伸閱讀

  1. Solidity 官方文檔:docs.soliditylang.org
  2. OpenZeppelin 合約庫:docs.openzeppelin.com/contracts
  3. Hardhat 文檔:hardhat.org/docs
  4. Remix IDE 文檔:remix-ide.readthedocs.io
  5. 以太坊開發者資源:ethereum.org/developers

相關文章推薦

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

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

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