學習目標

透過一個簡單的專案程式碼,來學習 sodility ,這邊會先貼出整份程式碼,底下會個別解釋 sodility 的程式語言知識

專案募資合約

簡介情境

  1. 有一個專案發起人,他發起一個募資專案,需要一筆目標金額
  2. 大家可以投資這個專案 (類似共同存錢到一個戶頭)
  3. 只有當募資成功(達到目標金額)時,這筆錢才會真正轉給專案發起人
  4. 如果募資失敗,投資的錢要能退還給各個投資人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

contract ProjectFunding {
// --- 狀態變數 (State Variables) ---
// 先定義合約的狀態
address public projectOwner; // 專案發起人
uint public fundingGoal; // 募資目標金額
bool public goalReached; // 募資是否成功
uint public totalFunds; // 目前總募資金額

// --- 事件 (Events) ---
// 當重要事件發生時,可以通知外部應用
event Funded(address indexed investor, uint amount);
event Payout(address indexed projectOwner, uint amount);

// --- Mapping 和 Array ---
// 我們需要記錄誰投了多少錢
mapping(address => uint) public contributions; // 記錄每個地址的投資金額
address payable[] public investors; // 記錄所有投資人的地址

// --- Constructor (建構子) ---
// 在部署合約時,只會執行一次的特殊函數
constructor(uint _fundingGoal) {
projectOwner = msg.sender; // msg.sender: 部署合約的人,就是專案發起人
fundingGoal = _fundingGoal; // 設定募資的目標金額
goalReached = false; // 一開始,目標當然是還沒達成
}

// --- Modifier (修飾器) ---
// 這是可重用的程式碼片段,用來檢查函數的執行條件
// 1. 只有專案發起人才能執行的操作
modifier onlyOwner {
require(msg.sender == projectOwner, "Only the project owner can call this function.");
_; // 如果條件成立,就繼續執行
}

// 2. 只有在募資成功後才能執行的操作
modifier mustBeGoalReached {
require(goalReached == true, "The funding goal has not been reached yet.");
_; // 如果條件成立,就繼續執行
}

// --- 主要功能函數 ---

// 1. 投資函數 (fund)
// `payable`關鍵字讓這個函數可以接收以太幣
function fund() public payable {
require(!goalReached, "This project has already been successfully funded."); // 如果募資成功了,就不能再投了
require(msg.value > 0, "You must send some Ether."); // 投資金額必須大於 0

// 記錄投資人與金額
address payable investor = payable(msg.sender);
contributions[investor] += msg.value; // 在 mapping 中記錄這位投資人的總投資額
totalFunds += msg.value; // 累加總募資金額

// 如果這是這位投資人的第一次投資,就把他加入投資人列表
// 這裡稍微簡化,假設每次投資都加入,實務上會先檢查是否存在
investors.push(investor);

// 發出一個 Funded 事件
emit Funded(investor, msg.value);

// 檢查是否達到募資目標
if (totalFunds >= fundingGoal) {
goalReached = true;
}
}

// 2. 付款給專案發起人 (payoutToOwner) - 這是一個 private 函數
// private: 只能被這個合約內的其他函數呼叫
function payoutToOwner() private mustBeGoalReached {
uint amount = address(this).balance; // 取得合約當下所有的以太幣餘額
if (amount > 0) {
(bool success, ) = projectOwner.call{value: amount}(""); // 使用 call 是更安全的轉帳方式
require(success, "Transfer failed.");
emit Payout(projectOwner, amount);
}
}

// 3. 觸發付款 (triggerPayout) - 這是給專案方呼叫的 public 函數
// public: 任何人都可以呼叫
function triggerPayout() public onlyOwner {
// 這個函數結合了兩個修飾器檢查:
// 1. onlyOwner 檢查:必須是專案發起人
// 2. mustBeGoalReached(在 payoutToOwner 中)檢查:必須募資成功
payoutToOwner();
}
}

在 remix 上面編譯的時候,我這邊改 EVM VERSION 為 shanghai

之後部署的時候,我改 evm version 為: cancun

部署要記得設定募資的期望金額,也就是下圖的內容

deploy sodility

部署完成之後,就可以如下圖跟合約互動

interactive

sodility 基本知識

變數

array

範例

陣列(array) 就是可以存放一組同類型資料的清單

1
address payable[] listWallets;
  • 這個 array 要存錢包地址,所以 data type 是 address
  • payable:讓地址可以 接收 Ether。
    • 如果沒有 payable,你不能往這些地址發送 Ether。
  • []:代表這是一個陣列,可以存放多個項目。

未來要放資料可以用 push

example 如下

1
2
3
4
address payable[] listWallets;

listWallets.push(wallet);

  • 上述假設 wallet 是一個變數,且 data type 是 address

address

前面提過三個

  1. uint
  2. bool
  3. string

這邊講另一個

address → 專門存放錢包地址的資料型別

舉例如下

1
address owner = msg.sender ;
  • msg.sender 是 sodility 內建的全域變數,全域變數常見的我放在最底下

建構子(constructor)

建構子(constructor)

  • Constructor 是 Solidity 裡一種特殊的函數
  • 合約被部署(deploy)時自動執行一次
  • 功能:初始化

底下為範例

1
2
3
4
5
6

constructor() payable public {


}

  • public: 在合約內外都能被呼叫
  • payable: 讓這個函數可以 接收與處理 Ether,這樣我們就能在部署合約時同時存入 Ether

modifier(修改器)

Solidity 中,modifier 是一種「附加條件」,可以套用在函數上,用來增加額外的邏輯控制。
常見用途:

  1. 限制誰可以呼叫(例如只有合約擁有者能執行)
  2. 檢查某些條件是否成立(例如判斷人是否已過世,才分配遺產)

底下範例

1
2
3
4
5
6
7
8
9
10
11
12
13
Contract Test {

address owner;
constructor() payable public {
owner = msg.sender;
}

modufier onlyOwner {
require(msg.sender == owner);
_;
}

}

上述是限制只有這個合約的擁有者,才能呼叫這個合約

  • require(msg.sender == owner):檢查呼叫合約的人(msg.sender)是否等於合約設定的擁有者(owner)
  • _:代表「繼續執行函數本身的內容」。如果 require 條件不成立,函數會直接失敗,不會繼續往下執行。

modifier 就是控制函數的行為:

  • 只能某帳號執行某函數
  • 執行函數前要檢查什麼條件
  • 函數執行結束後,要額外處理

資料結構

mapping (映射)

Solidity 裡,mapping 就像是一個「鍵值對(key-value store)」

  • Key(鍵)→ 唯一識別的東西
  • Value(值)→ 和該 key 對應的資料

底下範例

1
mapping(address => uint) testing;
  • address → key
  • uint → value
  • testing 是這個 mapping 的名稱

sodility 全域變數

內建在 sodility 可以用的全域變數

msg (訊息相關)

變數 型別 說明
msg.sender address 呼叫此函數的帳戶或合約地址
msg.value uint 呼叫時隨交易傳入的 Ether 數量(單位 Wei)
msg.data bytes 呼叫函數的完整 calldata(原始輸入資料)
msg.sig bytes4 calldata 的前 4 bytes(函數選擇器,用來識別呼叫哪個函數)

tx (交易相關)

變數 型別 說明
tx.origin address 發起這筆交易的 原始帳號(不管中間經過多少合約呼叫,永遠是最初發送交易的人的帳號)

block 相關

變數 型別 說明
block.number uint 當前區塊高度
block.timestamp uint 當前區塊的 Unix 時間戳(秒)
block.difficulty uint 當前區塊的難度(PoW)
block.gaslimit uint 當前區塊允許的最大 Gas 限額
block.coinbase address payable 當前區塊的礦工/驗證者地址

其他

變數 型別 說明
gasleft() uint256 目前剩餘的 Gas 數量
address(this).balance uint256 目前合約自身的 Ether 餘額
now uint ⚠️ 舊版本的 block.timestamp(新版建議直接用 block.timestamp

相關整理

  • 合約互動:常用 msg.sender(呼叫者) & msg.value(交易金額)
  • 安全性:避免用 tx.origin 當作身份驗證。
  • 時間/區塊控制:常用 block.timestamp(時間)、block.number(區塊高度)。
  • Gas:gasleft() 可用於檢查是否還有足夠 Gas。