智能合約 Solidity 教學 (上) - Solidity 基礎語法教學

在這篇文章中,我們會從 0 基礎開始,逐步介紹智能合約的概念、Solidity 語法,以及如何部署你的第一個智能合約。 適合對區塊鏈開發有興趣但還沒有 Solidity 經驗的開發者!

目錄

Solidity 0 到 1:智能合約入門

什麼是 Solidity?

Solidity 是一種面向智能合約的編程語言,專門用於在 Ethereum(以太坊) 和其他 EVM(Ethereum Virtual Machine)兼容的區塊鏈上開發去中心化應用(DApps)。

它受到了 JavaScript、Python 和 C++ 的影響,語法簡潔且適合初學者入門。

智能合約是什麼?

智能合約(Smart Contract)是一種 自動執行的合約,當滿足特定條件時,程式會自動執行約定的內容,無需中間人。

例如:

  • 去中心化交易所(DEX) – 允許用戶直接交易代幣
  • NFT 合約 – 用於發行、管理和交易 NFT
  • 去中心化金融(DeFi)應用 – 如貸款、流動性挖礦等

當然不僅僅是這種與金融有關的產業,任何可以去中心化的應用都可以採用智能合約,好比說有些產業為了更好的紀錄各個生產鏈上下的資料,便可以用智能合約,公正無私的紀錄資料。

開始使用 Solidity

1. 安裝開發環境

目前最簡單的方式是使用線上編輯器 **Remix IDE**:

如果你想在本地開發,可以使用:

  • Node.js(安裝 npm/pnpm)
  • Hardhat 或 Truffle(Solidity 開發框架)

另外你會需要一個加密貨幣錢包來與合約互動(放心,在本教學中不會花到任何一毛錢)

  • MetaMask 錢包(測試和與合約互動)

第一個合約 Hello World

開啟 Remix (https://remix.ethereum.org)
code
創建第一個合約:HelloWorld 在 Remix 建立新檔 HelloWorld.sol
code
貼上我們的第一個範本,接下來我們將逐行介紹各個概念
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    // 儲存訊息的變數
6    string public message;
7
8    // 建構函式,合約部署時會自動執行
9    constructor() {
10        message = "Hello, Solidity!";
11    }
12
13    // 設定新的訊息
14    function setMessage(string memory _newMessage) public {
15        message = _newMessage;
16    }
17}
18
選擇程式語言的版本 因為 Solidity 會不斷更新,新舊程式可能不通用,所以需要指定版本 - 指定使用 **Solidity 0.8.0** 以上版本。 - `^0.8.0` 代表兼容 0.8.x 的版本。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
定義一個名為 HelloWorld 的智能合約 - 使用 `contract` 關鍵字定義一個 **智能合約**。 - 所有函式與變數都放在 `{}` 內。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5
6}
7
建構函式,這是一種在合約部署時會自動執行一次的特殊函式。 我們讓他在合約部署的時候,將 message 變數設定成 "Hello, Solidity"
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    // 儲存訊息的變數
6    string public message;
7
8    // 建構函式,合約部署時會自動執行
9    constructor() {
10        message = "Hello, Solidity!";
11    }
12}
13
定義一個公開函式,讓使用者呼叫來更新 message。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    // 儲存訊息的變數
6    string public message;
7
8    // 建構函式,合約部署時會自動執行
9    constructor() {
10        message = "Hello, Solidity!";
11    }
12
13    // 設定新的訊息
14    function setMessage(string memory _newMessage) public {
15        message = _newMessage;
16    }
17}
18

四、編譯與部署

點擊 Remix 右側面板 `Solidity Compiler`
code
- 選擇正確版本 (0.8.x) - 按下 **`Compile HelloWorld.sol`** 按鈕
code
沒有錯誤就會顯示綠色打勾
code
轉到 Deploy & Run Transactions 面板
code
- Environment 記得選擇 Remix VM (Cancun) 他是專門給你測試用的區塊鏈 - Contract 記得別部署錯 - 最後點擊 Deploy
code
下方出現 `Deployed Contracts` 就表示部署成功 另外你也會發現上頭的 Account 默默的被扣了點以太幣 部署合約是會花真金白銀的,不過由於目前是測試環境,所以扣的也只是可無限使用的測試用以太幣 另外部署合約,這份合約將永遠的存在於區塊鏈上頭 無法撤銷、無法移除,請務必注意安全性。
code
- 展開 `HelloWorld` 合約介面 - 按 `message` 便可以查看此時此刻區塊鏈上 message 的數值是多少 (查看數值是免費的
code
在 setMessage 文字框輸入新訊息,如 "貓貓真可愛" 再來點擊 SetMessage 的按鈕,便可以觸發合約中的該函數 可以重新再點 message 一次,你會發現區塊鏈中的 message 變數也發生了更新 (修改區塊鏈上頭的數值是需要花錢的
code

五、Solidity 中的變數型別

我們來介紹一下各種 Solidity 常用的型別 先從布林(bool) 開始 布林型別只允許存 `true` 或 `false` 兩種型別
1bool public myBool = true;
2
**整數 (int, uint)** - `int`:可以為正或負 - `uint`:只能是正整數 (0, 1, 2, ...) - 常見長度:`int8`, `int16`, ..., `int256` (以 8 為增量),`int` 等同 `int256` - 同理 `uint8`, `uint16`, ..., `uint256`
1uint256 public myUint = 123;
2int public myInt = -123;
3
地址 (address) 用於存放以太坊地址(錢包或合約地址)
1address public myAddress = 0x1234567890123456789012345678901234567890;
2myAddress.balance // 取得該錢包目前剩餘的存款
字串 (string) 就是一段文字 不過不像其他程式語言,在 Solidity 中,字串並沒有 length 屬性,也不能用 index 的方式取得字元
1string public myString = "Hello";
2
3myString.length; //error
4myString[0]; //error
陣列
1uint[] public numbers; // 動態大小的陣列
2uint[5] public fixedSizeNumbers; // 大小固定為 5 的陣列
3
4numbers.push(123);
struct 可以自定義一個資料結構
1struct Person {
2    string name;
3    uint age;
4}
5
6Person public alice = Person("Alice", 30);
7
enum (列舉) - 用於定義一組狀態,方便管理
1enum Status { Pending, Shipped, Completed, Rejected }
2
3Status public currentStatus;
4

變數類型:Local、State、Global

在 Solidity 中的變數根據其宣告位置可以分為 三大類

  1. Local 變數(函式內部變數)
  2. State 變數(合約狀態變數)
  3. Global 變數(區塊鏈資訊變數)

1. Local 變數

定義:

  • 宣告在函式內部
  • 僅存於函式執行期間(不會永久儲存到區塊鏈)
  • 默認存於記憶體(memory)
  • 不會影響智能合約的狀態,因此不會產生 Gas 費用

2. State 變數

定義:

  • 宣告在函式之外,屬於整個合約
  • 儲存在區塊鏈上(storage)
  • 修改 State 變數時,會消耗 Gas(因為改變了區塊鏈狀態)

3. Global 變數

定義:

  • 提供區塊鏈上的資訊
  • 不需要手動宣告,直接可用
  • 儲存方式根據類型不同,有些來自 storage,有些只是暫時變數

常見 Global 變數:

msg.sender呼叫合約的發送者地址 msg.value交易發送的 ETH 數量 block.timestamp當前區塊的時間戳 block.number當前區塊號 gasleft()剩餘 Gas 數量 tx.origin原始交易發起者 address(this).balance合約帳戶的 ETH 餘額


Data Locations:memory、storage、calldata

除了變數的類型(local、state、global),Solidity 也提供了 三種資料儲存方式 適用於陣列 (array)、結構 (struct)、字串 (string)、映射 (mapping) 等參考型別 (Reference Types)

storage表示這個資料永遠儲存於區塊鏈上頭,**改變會消耗 Gas,**剛剛提到的 State 變數就屬於這一類

memory表示這個資料只是短暫出現在記憶體上,**交易結束後消失,**剛剛提到的 Local 變數就屬於這一類

calldata只讀記憶體(交易輸入)外部函式參數專用,不允許修改external 函式的輸入參數

八、瓦斯費,什麼是 Gas?

眼尖的同學應該發現,如果你修改了區塊鏈的資料是要花錢的! 不僅如此,在以太坊上,每當你執行一筆交易(例如呼叫智能合約、轉帳等),都需要向網路支付一筆 交易手續費。 這筆費用的計算方式並不是固定的,基本上對區塊鏈做越多操作會越貴。 而以太坊使用一種叫做 Gas 的單位來衡量交易在網路上執行所需的運算資源。

  • 付款時使用的是 以太幣 (ETH),當然1以太幣快 3000 美元,單位太大了,所以瓦斯費習慣以以 Gwei(10^-9 ETH)為單位計價。
  • 哪怕是相同的操作,在不同時間段的瓦斯費都不同,在尖峰時刻的瓦斯費會比較昂貴。

計算交易手續費 (以下僅供參考)

  1. Gas Used:某次交易實際執行用掉多少 Gas
  2. Gas Price:每單位 Gas 的價格,通常以 Gwei 為單位
  3. 交易手續費 (Tx Fee) = Gas Used × Gas Price

舉例:

  • 你的交易執行消耗了 50,000 Gas
  • 當前 Gas Price = 20 Gwei = 20 × 10−9^{-9} ETH
  • 交易費用 = 50,000 × (20 × 10^-9) ETH = 0.001 ETH

哪些操作會消耗 Gas?

在智能合約中,大多數操作都會消耗 Gas,尤其是對區塊鏈「狀態 (state)」造成變化的操作。 下面列出一些常見「高 Gas 消耗」的動作:

  1. 寫入/修改區塊鏈上儲存 (storage)
    • 最昂貴 的操作之一就是對合約的 storage寫入更新
    • 例如:改變一個 state variable。
    • 原因:永久儲存到區塊鏈資料庫,需要網路中所有節點同步保存。
  2. 新增資料到 mapping 或 array
    • 與一般的 storage 更新類似,只要有寫入都會消耗較多 Gas。
    • 動態陣列的 push() 也會往合約的永續狀態中寫入新元素。
  3. 刪除資料 (釋放 storage)
    • 相較寫入,可以回收部分 Gas(因為釋放空間會有 Gas Refund),但一般來說整體仍需付出操作成本。
  4. 合約部署
    • 部署(Deploy)是一項非常昂貴的操作,因為需要將整個智能合約程式碼上傳到區塊鏈。
    • 智能合約一旦部署,就成為鏈上的永久程式碼。
  5. 呼叫其他合約
    • 在合約中透過 calldelegatecallinterface 呼叫其他合約函式,需要額外的 Gas 來執行被呼叫的邏輯。
  6. 觸發事件 (Event)
    • 在合約中 emit Event 會產生一筆鏈上日誌 (Log),也會消耗 Gas。(一般比寫 storage 便宜,但還是要考量頻繁記錄事件帶來的額外開銷)
  7. 迴圈操作 (for/while loop)
    • 每次迭代都需要額外的計算成本。
    • 如果迴圈裡包含寫入 storage、呼叫其他合約、更複雜的邏輯,Gas 量會隨迴圈次數成倍增加。

哪些操作相對不會「直接」消耗 Gas?

  1. 只讀取狀態變數 (view function)
    • 用錢包或前端呼叫 viewpure 函式,不會執行交易,也不會在鏈上留下任何記錄,因此不需支付額外 Gas。
    • 但若在合約內部使用 viewpure 函式,仍然要計入該交易整體的執行成本——只是相對來說,只有計算的成本,但沒有對 storage 寫入的成本。
  2. 在函式內做記憶體層面的運算 (memory)
    • memorycalldata 中的操作只會產生交易執行中計算的 Gas,通常比修改 storage 要便宜許多,因為它不需要永久寫入區塊鏈。

注意: 雖然「讀取數據」或「單純做計算」比寫入便宜很多,但「執行交易」本身仍有基礎費用,因為必須產生交易並在鏈上被礦工/驗證者驗證。


如何節省 Gas?

  1. 減少對 storage 的寫入
    • 能在 memorycalldata 中處理就盡量不要寫進 storage
    • 例如先在 memory 中計算好,再一次性寫回 storage,或只在需要時更新 storage
  2. 考慮資料結構選擇
    • mapping 在查詢方面通常比 array 高效(尤其針對索引操作);
    • 但要根據具體邏輯權衡。
  3. 避免在迴圈中使用寫入或呼叫
    • 如果可以把大量資料處理切分成多筆交易,或使用批量更新/分段的方式,讓單次交易的迴圈不至於過大。
  4. 事件 (Event) 的使用
    • 雖然事件會消耗 Gas,但通常比 storage 寫入便宜且可以提供鏈上日誌。
    • 取代在鏈上保存大型文字資料,而是用事件紀錄,日後可從鏈下檢索到。
  5. 版本與編譯器優化
    • 使用新版本 Solidity 通常會有更多優化(如 0.8.x),
    • 在 Remix 或 Hardhat 中開啟編譯器優化 optimizer(如設置 runs = 200 或其他數值)。

、控制流程

if / else if / else
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    string public result = "";
6
7    constructor() {
8        int256 val = 1;
9        if (val == 0) {
10            result = "Value is zero";
11        } else if (val == 1) {
12            result = "Value is one";
13        } else {
14            result = "Value is something else";
15        }
16    }
17}
18
for 迴圈
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    uint public sum = 0;
6
7    constructor() {
8        uint max = 100;
9        for (uint i = 0; i < max; i++) {
10            sum += i;
11        }
12    }
13}
14
while 迴圈 範例程式碼的功能與上頭完全相同
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    uint public sum = 0;
6
7    constructor() {
8        uint max = 100;
9        uint i = 0;
10        while (i < max) {
11            sum += i;
12            i ++;
13        }
14    }
15}
16

九、函式 (Function)

函數就像是一個魔法盒子,我們可以把一系列的指令放進這個盒子裡,替這個盒子取一個名字 然後以後只要叫這個名字,就可以讓這些指令執行。

在 Solidity 中為了更方便使用者開發,我可以替函式加上一些設定

`public` 函式 所有人(包括外部帳戶和其他合約)都可以呼叫該函式。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    uint public count;
6
7    // 這是一個 public 函式
8    function increment() public {
9        count += 1;
10    }
11}
12
`private` 函式 只能在合約內部使用 外部或者其他合約無法訪問 * 變數也可以設定成 external
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    uint public count;
6
7    // 外部無法直接訪問該變數
8    // 請注意!該資料仍然公開於區塊鏈上頭,仍然可以爬取到該變數
9    uint private innerCount;
10
11    // 這是一個 public 函式
12    function increment() public {
13        _increment(); // ✅ 合約內部可存取
14    }
15
16    // 這是一個 private 函式,習慣命名上最前面加上 _ 來表示這是 private
17    function _increment() private {
18        count += 1;
19    }
20}
21
`external` 函式 只能從合約外部存取,內部不可直接存取 * 不過還是可以間接地利用 `this.函式名()` 來存取 * 不適用於變數:變數無法標記為 external。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5    uint public count;
6
7    // 這是一個 public 函式
8    function increment() public {
9        this.externalIncrement(); // 內部調用必須透過 `this`
10    }
11
12    // 這是一個 external 函式
13    function externalIncrement() external {
14        count += 1;
15    }
16}
17
上面都是偏向描述權限 接下來則是針對功能
`view` 函式 view 函式不會修改區塊鏈上的狀態 - 不能修改合約內的狀態變數 - 不能發送以太幣(ETH) - 不能調用其他會修改狀態的函式
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract ViewExample {
5    uint public number = 42; // 狀態變數
6
7    // 這個函式只是讀取狀態變數,並沒有改變它
8    function getNumber() public view returns (uint) {
9        return number;
10    }
11}
12
`pure` 函式 pure 純計算,不讀取也不修改狀態 - pure 函式不能讀取也不能修改區塊鏈上的狀態變數。 - 只能使用函式內的變數或傳入的參數進行運算。 - 適合用來做純計算,例如數學運算。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract PureExample {
5    // 這個函式純粹進行計算,沒有存取合約內的狀態變數
6    function add(uint a, uint b) public pure returns (uint) {
7        return a + b;
8    }
9}
10
`payable` 函式 payable 函式允許該函式接收 ETH - 沒有 payable 的函式無法接收 ETH,如果用戶試圖發送 ETH 會報錯。 - 有 payable 的函式可以讓合約接收 ETH,常用於: - 付款交易 - 捐款功能 - NFT 或其他商品購買
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract PayableExample {
5    uint public balance;
6
7    // 這個函式允許用戶發送 ETH 到合約
8    function deposit() public payable {
9        balance += msg.value; // msg.value 是發送的 ETH 數量
10    }
11
12    // 這個函式回傳合約內的 ETH 餘額
13    function getBalance() public view returns (uint) {
14        return address(this).balance;
15    }
16}
17

十、錯誤處理 (Error Handling)

require() - 用於檢查條件,不符合時將 revert 並顯示錯誤訊息。 - 常用於權限判斷或輸入參數檢查。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract PayableExample {
5    uint public balance;
6
7    function withdraw(uint _amount) public {
8        // _amount 須小於 balance 的金額,否則報錯
9        require(_amount < balance, "合約中資金不足");
10        balance -= _amount;
11    }
12
13}
14
revert() - 直接報錯,
1function doSomething() public {
2    if (someConditionNotMet()) {
3        revert("Condition not met");
4    }
5    // ...
6}
7
assert() - 用於檢查不應該發生的狀況(通常是內部錯誤或不變量)。
1function testAssert(uint _x) public pure {
2    assert(_x != 0); // _x 絕對不能為 0
3}
4

進一步學習建議

  1. 掌握測試網部署:嘗試使用 MetaMask 連接測試網 (如 Sepolia、Goerli) 進行部署。
  2. 熟悉 Hardhat / Truffle:在本地進行單元測試與合約部署管理。
  3. 安全性考量:了解常見攻擊手法(Reentrancy, Integer Overflow 等)。
  4. 前端整合:透過 Web3.js 或 Ethers.js 連接你的 DApp 與合約。

下一篇文章

喜歡這篇文章嗎?
歡迎給我一點支持!