Solidity 中文教學網站 | 香港編程學院
繁體中文 Solidity 入門到進階基礎 單檔 HTML

從零開始學 Solidity:智慧合約開發完整指南

Solidity 是用來撰寫以太坊智慧合約的主要語言。智慧合約可以理解成部署在區塊鏈上的程式,會在特定地址保存程式碼與狀態,並在 EVM(Ethereum Virtual Machine)中執行。你可以把這個網站當成一份可以直接教學、展示、練習與自測的教材頁面。

基礎總覽

Solidity 是什麼?智慧合約又是什麼?

Solidity 的角色

Solidity 是用來撰寫智慧合約的語言。智慧合約本質上是「部署在區塊鏈特定位址上的程式碼與資料集合」,執行時由 EVM 負責處理。這表示它不像一般伺服器程式那樣能直接存取檔案系統或作業系統程序,而是處在一個隔離的運行環境。 Source

新手最重要的區塊鏈觀念

  • 交易 Transaction:改變鏈上狀態的方式,具有簽章與原子性。
  • 區塊 Block:把一批交易依時間順序打包。
  • Gas:執行程式的成本與限制。
  • 帳戶 Accounts:分成外部帳戶與合約帳戶。
學習目標 1 看懂 Solidity 基本語法與檔案結構
學習目標 2 理解型別、記憶體位置與函式修飾
學習目標 3 能閱讀簡單智能合約範例
學習目標 4 具備安全與錯誤處理基礎意識
語法教學

Solidity 基本語法與檔案骨架

最小可讀範例:一個可以儲存與讀取數值的合約 基礎語法
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleStorage {
    uint256 public value;

    constructor(uint256 initialValue) {
        value = initialValue;
    }

    function setValue(uint256 newValue) public {
        value = newValue;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

1. pragma

pragma solidity ^0.8.20; 用來宣告編譯器版本範圍。實務上應固定在團隊支援的版本區間,避免語意差異。

2. contract

contract 類似物件導向中的類別,用來包住狀態變數與函式。

3. 狀態變數

uint256 public value; 會儲存在鏈上 storage,且 public 會自動產生 getter。

4. constructor

建構子只會在部署時執行一次,常用來初始化合約狀態。

5. 函式

Solidity 函式會標示可見性與狀態修飾,例如 publicviewpayable

6. returns

returns (uint256) 用來宣告回傳型別,讀取函式常搭配 view 使用。

重點:合約本身是「函式 + 狀態資料」的集合,部署後存在鏈上位址,並由 EVM 執行。這是理解 Solidity 的核心出發點。
型別系統

Solidity 資料型別與資料位置

類別 常見型別 說明 新手用途
布林 bool true / false 開關、權限、狀態標記
整數 uint, uint256, int Solidity 常用數值型別,uint 預設是 uint256 餘額、計數器、投票數
地址 address, address payable 20 位元組位址;可付款地址可收發 Ether 記錄使用者、合約地址
位元組 bytes1 ~ bytes32, bytes 固定或動態位元組資料 雜湊、低階資料表示
字串 string 動態字串 名稱、說明文字
列舉 enum 自訂有限狀態 流程狀態機
結構 struct 組合多個欄位成一個資料型別 使用者資料、提案資料
映射 mapping(key => value) 類似鍵值對查找表 餘額表、投票權、授權清單
陣列 uint[], address[] 可固定或動態長度 清單資料

值型別 vs 參考型別

整數、布林、地址這些屬於值型別,在賦值或傳參時通常是直接複製;而結構、陣列、映射則屬於參考型別,需要特別標示資料位置。 Source

storage / memory / calldata

  • storage:合約永久儲存區,寫入成本高。
  • memory:函式呼叫期間的暫存資料。
  • calldata:外部函式輸入資料,通常唯讀、較省 gas。
型別、struct、mapping、array 綜合示範 資料結構
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DataTypesDemo {
    bool public isActive = true;
    uint256 public totalUsers = 0;
    address public owner;

    struct User {
        string name;
        uint256 score;
        bool registered;
    }

    mapping(address => User) public users;
    address[] public userList;

    constructor() {
        owner = msg.sender;
    }

    function register(string calldata name) external {
        require(!users[msg.sender].registered, "已註冊過");
        users[msg.sender] = User(name, 0, true);
        userList.push(msg.sender);
        totalUsers++;
    }

    function setScore(uint256 newScore) external {
        require(users[msg.sender].registered, "尚未註冊");
        users[msg.sender].score = newScore;
    }
}
合約核心

函式、可見性、修飾器、事件與全域變數

函式可見性

  • public:內外都可呼叫
  • external:偏向外部呼叫
  • internal:本合約與繼承合約可用
  • private:只有本合約內部可用

狀態修飾

  • view:只讀,不改變狀態
  • pure:不讀也不寫狀態
  • payable:可接收 Ether

重要全域變數

  • msg.sender:目前呼叫者
  • msg.value:本次傳入 Ether
  • block.timestamp:區塊時間
  • tx.origin:原始交易發起者

事件 Event

事件讓前端或鏈下服務知道鏈上發生了什麼。範例常使用 event 來記錄出價、投票或狀態變化。這是 DApp 前後端整合很重要的一環。 Source

event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);

修飾器 Modifier

修飾器可以把重複的檢查邏輯抽出來,例如只有合約擁有者才能執行某些函式。 modifiers 是宣告式改變函式行為的重要方式。 Source

modifier onlyOwner() {
    require(msg.sender == owner, "不是合約擁有者");
    _;
}
函式、modifier、event 綜合範例 合約核心
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract OwnedCounter {
    address public owner;
    uint256 public count;

    event CountChanged(uint256 newCount);

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "只有擁有者可操作");
        _;
    }

    function increment() external onlyOwner {
        count += 1;
        emit CountChanged(count);
    }

    function reset() external onlyOwner {
        count = 0;
        emit CountChanged(count);
    }

    function current() external view returns (uint256) {
        return count;
    }
}
控制流程

if、for、while、require、revert、assert 怎麼用?

基本控制結構

Solidity 支援和 C / JavaScript 類似的控制語法,例如 ifelseforwhilebreakcontinuereturnSource

function sum(uint256 n) public pure returns (uint256 total) {
    for (uint256 i = 1; i <= n; i++) {
        total += i;
    }
}

錯誤處理三兄弟

  • require:檢查輸入、權限、前置條件。
  • revert:主動中止執行,可搭配訊息或自訂錯誤。
  • assert:檢查不應被破壞的內部不變條件,通常用於抓 bug。

assert 偏內部錯誤與 invariant;requirerevert 偏預期中的失敗處理。 Source

require / revert / assert 實際示範 錯誤處理
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ErrorDemo {
    address public owner;

    error NotOwner(address caller);

    constructor() {
        owner = msg.sender;
    }

    function restrictedAction() external view {
        if (msg.sender != owner) {
            revert NotOwner(msg.sender);
        }
    }

    function divide(uint256 a, uint256 b) external pure returns (uint256) {
        require(b != 0, "除數不可為 0");
        uint256 result = a / b;
        assert(result <= a || b == 1);
        return result;
    }
}
範例應用

三個實用 Solidity 範例

範例一:計數器 Counter

用途很單純,但可以學會狀態變數、事件、函式與 getter 的基本配合。

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

contract Counter {
    uint256 public count;
    event Increased(uint256 newCount);

    function increment() external {
        count += 1;
        emit Increased(count);
    }

    function decrement() external {
        require(count > 0, "count 不能小於 0");
        count -= 1;
        emit Increased(count);
    }
}

範例二:簡易銀行 SimpleBank

這個例子示範 payablemsg.value、提款與餘額管理,適合練習 Ether 流程。

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

contract SimpleBank {
    mapping(address => uint256) public balances;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);

    function deposit() external payable {
        require(msg.value > 0, "必須存入 ETH");
        balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "餘額不足");

        // Checks
        // Effects
        balances[msg.sender] -= amount;

        // Interactions
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        require(success, "轉帳失敗");

        emit Withdrawn(msg.sender, amount);
    }

    function getMyBalance() external view returns (uint256) {
        return balances[msg.sender];
    }
}

範例三:簡易投票 Voting

投票範例是很經典的教學素材,能幫你理解 structmapping、陣列與權限管理。以下是簡化版。 Source

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

contract SimpleVoting {
    struct Proposal {
        string name;
        uint256 voteCount;
    }

    address public chairperson;
    mapping(address => bool) public hasVoted;
    Proposal[] public proposals;

    constructor(string[] memory proposalNames) {
        chairperson = msg.sender;
        for (uint256 i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    function vote(uint256 proposalIndex) external {
        require(!hasVoted[msg.sender], "你已投票");
        require(proposalIndex < proposals.length, "提案不存在");

        hasVoted[msg.sender] = true;
        proposals[proposalIndex].voteCount += 1;
    }

    function winningProposal() external view returns (uint256 winnerIndex) {
        uint256 highest = 0;
        for (uint256 i = 0; i < proposals.length; i++) {
            if (proposals[i].voteCount > highest) {
                highest = proposals[i].voteCount;
                winnerIndex = i;
            }
        }
    }
}
建議練習:把以上三個合約貼到 Remix 測試,實際部署、呼叫函式、查看事件紀錄,你對 Solidity 的理解會進步很快。
安全與實務

Solidity 初學者一定要知道的安全重點

1. Gas 與回滾

若執行過程耗盡 gas,當前呼叫框架的狀態變更會回滾。所以設計上要避免無上限迴圈與過度昂貴的 storage 操作。 Source

2. Storage 很貴

storage 初始化與修改成本高,且合約內部無法直接列舉所有 storage key,因此資料結構設計很重要。 Source

3. require 先檢查

所有輸入、權限、索引範圍、餘額條件,都應先檢查再執行邏輯。

4. Checks-Effects-Interactions

與外部地址互動前,先做檢查,再更新內部狀態,最後才進行轉帳或外部呼叫。這是降低重入風險的基本手法。

5. tx.origin 慎用

權限判斷通常應使用 msg.sender,不要把 tx.origin 當成一般授權機制的核心依據。

新版 EVM 中,selfdestruct 的行為已改變,不應再把它視為「真的銷毀合約」的標準手段。 Source
互動小測驗

Solidity 小測驗

作答後按下「提交答案」,系統會立即顯示分數與解析。

1. 哪一個關鍵字用來宣告編譯器版本範圍?

2. 哪一個最適合表示使用者餘額表?

3. 哪一種函式可以接收 Ether?

4. 哪一個最常用來檢查外部輸入與前置條件?

5. msg.sender 代表什麼?

6. 哪個資料位置通常用於外部函式輸入且偏唯讀?

學習地圖

建議你的下一步學習順序

先學會基本語法:變數、函式、可見性、requireevent
熟悉資料結構:structmapping、陣列、memory / storage / calldata
練習三類範例:計數器、銀行、投票。
學部署與測試工具:Remix、Hardhat、Foundry。
深入 ERC-20、NFT、DAO、權限控制、重入防護與審計觀念。
Remix 練習部署 Hardhat 自動化測試 Foundry 高速開發 OpenZeppelin 合約庫 事件監聽與前端整合