Solidity 是什麼?智慧合約又是什麼?
Solidity 的角色
Solidity 是用來撰寫智慧合約的語言。智慧合約本質上是「部署在區塊鏈特定位址上的程式碼與資料集合」,執行時由 EVM 負責處理。這表示它不像一般伺服器程式那樣能直接存取檔案系統或作業系統程序,而是處在一個隔離的運行環境。 Source
新手最重要的區塊鏈觀念
- 交易 Transaction:改變鏈上狀態的方式,具有簽章與原子性。
- 區塊 Block:把一批交易依時間順序打包。
- Gas:執行程式的成本與限制。
- 帳戶 Accounts:分成外部帳戶與合約帳戶。
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 函式會標示可見性與狀態修飾,例如 public、view、payable。
6. returns
returns (uint256) 用來宣告回傳型別,讀取函式常搭配 view 使用。
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。
// 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:本次傳入 Etherblock.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, "不是合約擁有者");
_;
}
// 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 類似的控制語法,例如 if、else、for、while、break、continue、return。
Source
function sum(uint256 n) public pure returns (uint256 total) {
for (uint256 i = 1; i <= n; i++) {
total += i;
}
}
錯誤處理三兄弟
require:檢查輸入、權限、前置條件。revert:主動中止執行,可搭配訊息或自訂錯誤。assert:檢查不應被破壞的內部不變條件,通常用於抓 bug。
assert 偏內部錯誤與 invariant;require 與 revert 偏預期中的失敗處理。
Source
// 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
這個例子示範 payable、msg.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
投票範例是很經典的教學素材,能幫你理解 struct、mapping、陣列與權限管理。以下是簡化版。
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;
}
}
}
}
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 當成一般授權機制的核心依據。
selfdestruct 的行為已改變,不應再把它視為「真的銷毀合約」的標準手段。
Source
Solidity 小測驗
作答後按下「提交答案」,系統會立即顯示分數與解析。
建議你的下一步學習順序
require、event。
struct、mapping、陣列、memory / storage / calldata。