目錄
- 智能合約 Solidity 教學 (上) - Solidity 基礎語法教學
- 智能合約 Solidity 教學 (中) - MetaMask 與以太坊測試網
- 智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端
一、Solidity 0 到 1:智能合約入門
什麼是 Solidity?
Solidity 是一種面向智能合約的編程語言,專門用於在 Ethereum(以太坊) 和其他 EVM(Ethereum Virtual Machine)兼容的區塊鏈上開發去中心化應用(DApps)。
它受到了 JavaScript、Python 和 C++ 的影響,語法簡潔且適合初學者入門。
智能合約是什麼?
智能合約(Smart Contract)是一種 自動執行的合約,當滿足特定條件時,程式會自動執行約定的內容,無需中間人。
例如:
- 去中心化交易所(DEX) – 允許用戶直接交易代幣
- NFT 合約 – 用於發行、管理和交易 NFT
- 去中心化金融(DeFi)應用 – 如貸款、流動性挖礦等
當然不僅僅是這種與金融有關的產業,任何可以去中心化的應用都可以採用智能合約,好比說有些產業為了更好的紀錄各個生產鏈上下的資料,便可以用智能合約,公正無私的紀錄資料。
二、開始使用 Solidity
1. 安裝開發環境
目前最簡單的方式是使用線上編輯器 **Remix IDE**:
- 網址:https://remix.ethereum.org/
- 不需要安裝,打開瀏覽器即可編寫與部署智能合約。
如果你想在本地開發,可以使用:
- Node.js(安裝 npm/pnpm)
- Hardhat 或 Truffle(Solidity 開發框架)
另外你會需要一個加密貨幣錢包來與合約互動(放心,在本教學中不會花到任何一毛錢)
- MetaMask 錢包(測試和與合約互動)
三、第一個合約 Hello World


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
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract HelloWorld {
5
6}
7
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
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 中的變數型別
1bool public myBool = true;
2
1uint256 public myUint = 123;
2int public myInt = -123;
3
1address public myAddress = 0x1234567890123456789012345678901234567890;
2myAddress.balance // 取得該錢包目前剩餘的存款
1string public myString = "Hello";
2
3myString.length; //error
4myString[0]; //error
1uint[] public numbers; // 動態大小的陣列
2uint[5] public fixedSizeNumbers; // 大小固定為 5 的陣列
3
4numbers.push(123);
1struct Person {
2 string name;
3 uint age;
4}
5
6Person public alice = Person("Alice", 30);
7
1enum Status { Pending, Shipped, Completed, Rejected }
2
3Status public currentStatus;
4
六、變數類型:Local、State、Global
在 Solidity 中的變數根據其宣告位置可以分為 三大類:
- Local 變數(函式內部變數)
- State 變數(合約狀態變數)
- 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)為單位計價。
- 哪怕是相同的操作,在不同時間段的瓦斯費都不同,在尖峰時刻的瓦斯費會比較昂貴。
計算交易手續費 (以下僅供參考)
- Gas Used:某次交易實際執行用掉多少 Gas
- Gas Price:每單位 Gas 的價格,通常以 Gwei 為單位
- 交易手續費 (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 消耗」的動作:
- 寫入/修改區塊鏈上儲存 (storage)
- 最昂貴 的操作之一就是對合約的
storage
做 寫入 或 更新。 - 例如:改變一個 state variable。
- 原因:永久儲存到區塊鏈資料庫,需要網路中所有節點同步保存。
- 最昂貴 的操作之一就是對合約的
- 新增資料到 mapping 或 array
- 與一般的
storage
更新類似,只要有寫入都會消耗較多 Gas。 - 動態陣列的
push()
也會往合約的永續狀態中寫入新元素。
- 與一般的
- 刪除資料 (釋放 storage)
- 相較寫入,可以回收部分 Gas(因為釋放空間會有 Gas Refund),但一般來說整體仍需付出操作成本。
- 合約部署
- 部署(Deploy)是一項非常昂貴的操作,因為需要將整個智能合約程式碼上傳到區塊鏈。
- 智能合約一旦部署,就成為鏈上的永久程式碼。
- 呼叫其他合約
- 在合約中透過
call
、delegatecall
或interface
呼叫其他合約函式,需要額外的 Gas 來執行被呼叫的邏輯。
- 在合約中透過
- 觸發事件 (Event)
- 在合約中
emit Event
會產生一筆鏈上日誌 (Log),也會消耗 Gas。(一般比寫storage
便宜,但還是要考量頻繁記錄事件帶來的額外開銷)
- 在合約中
- 迴圈操作 (for/while loop)
- 每次迭代都需要額外的計算成本。
- 如果迴圈裡包含寫入
storage
、呼叫其他合約、更複雜的邏輯,Gas 量會隨迴圈次數成倍增加。
哪些操作相對不會「直接」消耗 Gas?
- 只讀取狀態變數 (view function)
- 用錢包或前端呼叫
view
或pure
函式,不會執行交易,也不會在鏈上留下任何記錄,因此不需支付額外 Gas。 - 但若在合約內部使用
view
或pure
函式,仍然要計入該交易整體的執行成本——只是相對來說,只有計算的成本,但沒有對storage
寫入的成本。
- 用錢包或前端呼叫
- 在函式內做記憶體層面的運算 (memory)
- 在
memory
或calldata
中的操作只會產生交易執行中計算的 Gas,通常比修改storage
要便宜許多,因為它不需要永久寫入區塊鏈。
- 在
注意: 雖然「讀取數據」或「單純做計算」比寫入便宜很多,但「執行交易」本身仍有基礎費用,因為必須產生交易並在鏈上被礦工/驗證者驗證。
如何節省 Gas?
- 減少對
storage
的寫入- 能在
memory
、calldata
中處理就盡量不要寫進storage
。 - 例如先在
memory
中計算好,再一次性寫回storage
,或只在需要時更新storage
。
- 能在
- 考慮資料結構選擇
mapping
在查詢方面通常比array
高效(尤其針對索引操作);- 但要根據具體邏輯權衡。
- 避免在迴圈中使用寫入或呼叫
- 如果可以把大量資料處理切分成多筆交易,或使用批量更新/分段的方式,讓單次交易的迴圈不至於過大。
- 事件 (Event) 的使用
- 雖然事件會消耗 Gas,但通常比
storage
寫入便宜且可以提供鏈上日誌。 - 取代在鏈上保存大型文字資料,而是用事件紀錄,日後可從鏈下檢索到。
- 雖然事件會消耗 Gas,但通常比
- 版本與編譯器優化
- 使用新版本 Solidity 通常會有更多優化(如 0.8.x),
- 在 Remix 或 Hardhat 中開啟編譯器優化
optimizer
(如設置runs = 200
或其他數值)。
九、控制流程
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
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
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 中為了更方便使用者開發,我可以替函式加上一些設定
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
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
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
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
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
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)
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
1function doSomething() public {
2 if (someConditionNotMet()) {
3 revert("Condition not met");
4 }
5 // ...
6}
7
1function testAssert(uint _x) public pure {
2 assert(_x != 0); // _x 絕對不能為 0
3}
4
進一步學習建議
- 掌握測試網部署:嘗試使用 MetaMask 連接測試網 (如 Sepolia、Goerli) 進行部署。
- 熟悉 Hardhat / Truffle:在本地進行單元測試與合約部署管理。
- 安全性考量:了解常見攻擊手法(Reentrancy, Integer Overflow 等)。
- 前端整合:透過 Web3.js 或 Ethers.js 連接你的 DApp 與合約。