智能合約並不智能,也不是合約,它只是可以執行在 EVM (Ethereum Virtual Machine) 中的一段 ByteCode 而已。之所以叫這個名字應該只是比較潮ㄅ,而且我覺得「智能」還有一點支語的味道,但在台灣也很少聽到「智慧型合約」這種說法。
合約不會自動執行,也不會自動結算跟驗證之類的事情,如果需要執行合約內的 Function,需要呼叫指定的 Transaction。
Account
在 Ethereum 底下,有兩種的 Account,分別是具有 Contract Code 的 Contract Account 以及沒有 Contract 的 EOA (External Owned Account)。 創建 Contract 需要使用到 EOA,且 Contract Account 比起 EOA 增加了 Storage hash 以及 Code hash 的欄位。
在 EOA 底下,總共有三種 Transaction
- 轉 Ether
- 部署 Contract
- 呼叫 Contract 裡面的特定 Function
在進行轉 Ether 的 Transaction 時,總共會有 4 個欄位:From, To, Value 以及 Data,其中,Data 為備註欄位,可以填入任意值。
而透過 EOA 執行部屬 Contract 時,在做的事情跟轉 Ether 一樣,同樣需要填入上述的 4 個欄位。只是 To 需要留空,只要 To 留空就代表該筆 Transaction 是 Create New Contract。 而 Data 欄位則輸入填入 Byte Code, Value 則可以塞一些 Ether 進入合約內。
為了避免裝置差異,所以 Contract 的 OPCode 會放置於 Ethereum Virtual Machine 內執行,所有的合約都會嚴格限制版本以避免各種問題發生。通常 EVM ByteCode 的開頭都是 0x60806040
,每一條指令都是一個 Bytes。
User 可以透過 ABI (Application Binary Interface) 與 Contract 進行溝通。
Remix IDE
以太坊提供了瀏覽器版本的 IDE 可以快速部署以及測試合約內容,這套 IDE 被稱為 Remix,透過外觀可以發現它是魔改 VSCode 的產物。
透過 Remix Ethereum IDE 可以直接連結 Metamask 進行部署,也可以使用運行在瀏覽器上的 Remix VM 鏈進行測試。
Solidity 基礎
架構
通常一個 Contract 的程式碼架構如下
// SPDX-License-Identifier: MIT
// 定義 License,如果不寫會跳 Warning
// 定義版本
pragma solidity >0.8.0;
// 合約開頭
contract HelloWorld{
// 宣告變數
int x;
// 建構子
constructor(int _y){
// xxx
}
// 函數
function getx() public view returns (int){
return x;
}
}
資料型態
Solidity 是一個強型別語言,由於浮點誤差的緣故,通常不會使用到浮點數運算,也不提供 float 以及 double,而是全部都使用 int。 常見的解法是使用 decimal 之類的方法,把值同乘以某個數字來代表。舉例來說 decimals = 8
, 則 7000000000
代表的是 70
。
如果直接使用 int
則預設代表 int256
,變數如果不初始化會預設為 0。
Array 有兩種,分別是靜態以及動態,動態陣列宣告方式為不宣告陣列長度 []
,並且可以透過 .push()
在後面塞入值 (類似 Python 的 append),並且可以透過 .length
取得當前陣列長度。
Mapping 等於 Python 的 Dict,宣告方式為 mapping(typeA => typeB)
,舉例來說,mapping(string => int) public mp
就可以宣告一個 mp 的 mapping (字典),其中 key 為 string,而 value 為 int。
Function
Solidity 的 Function 比起其他語言的 Function,有一點點不一樣的點在於,需要額外定義 Visibility 以及 State Mutability。
Visibility (能見性) 代表這個 Function 可以被怎麼樣的合約呼叫,有一點點像是物件導向程式語言的 Public 以及 Private,但除了這兩個之外,還有一些其他的定義如下。
- Public : 任何合約或帳戶都可以呼叫
- Private : 只有目前的合約可以呼叫
- External : 除了目前合約與其繼承合約之外可以呼叫
- Internal : 只有目前合約可以呼叫
Mutability (可變性)代表了這個 Function 是否會對狀態進行寫入
- Pure : 不會讀取也不會改變區塊鏈內的東西 (不需要花 Gas Fee)
- View : 只會讀取合區塊鏈內的東西 (不需要花 Gas Fee)
- nonPayable : 可以對區塊鏈進行寫入,但不能接受 Ether
- Payable : 可以對區塊鏈進行寫入,且可以額外接受 Ether
Solidity 的 Function 可以接受多個回傳值,而他們的格式長的如下列。
function FnName [V] [SM] [returns ()] {}
會在函數宣告時直接宣告變數的回傳型態,除了使用傳統 return 之外,也可以直接在 returns 上面定義要回傳的變數名稱。
function ret2var() public pure returns (int xx,int yy){
xx = 9487;
yy = 9453;
}
msg object
在名為 msg 的這個 Object 中,可以取得一些當前 Transaction 中的資料。
- msg.sender : 發送者的 address
- msg.value : 合約內的以太數量 (以 Wei 為單位)
- msg.data : 完整的調用資料
而在 address 中,我們也可以接使用 .balance
來查詢使用者的 ETH 餘額
Summary
以下我寫了一個簡單的小例子,理論上讀懂上面的所有程式碼,就可以搞懂基礎的 Solidity 語法了!!
// SPDX-License-Identifier: MIT
pragma solidity >0.8.0;
// 合約開頭
contract HelloWorld{
// int 等於 int256
int x; // 不寫等於初始化成 0
int y;
// mapping 等於 python 的 dict, 裡面需要寫變數型態 to 變數型態
mapping (string => int) public mp;
// 建構子,創建合約時可以丟進去的東西
constructor(int _y){
y = _y;
mp["meow"] = 87;
mp["dog"] = 88;
}
/*
function FnName [V] [SM] [returns ()] {}
V : Visibility
- Public : 任何合約或帳戶都可以呼叫
- Private : 只有目前的合約可以呼叫
- External : 除了目前合約與其繼承合約之外可以呼叫
- Internal : 只有目前合約可以呼叫
SM : State Mutability
- Pure : 不會讀取或寫入任何狀態
- View : 只會讀取狀態
returns
*/
// getx function 可以直接簡化為下面的 uint public x;
// public 代表外面可以 call
// view 代表不會修改到內部的東西,執行不需要花 gas fee
function getx() public view returns (int){
return x;
}
string public meow = "meowmeow"; // 直接給變數 public = getter 方法
// 會修改到全域變數 x 以及回傳 sum,需要花 gas
function add(int a, int b) public returns (int){
int sum = a + b;
x = sum;
return sum;
}
// 可以不用寫 return,直接在宣告時定義回傳的是哪個變數
function mul(int a, int b) public returns (int sum){
sum = 0;
for(int i=0; i<b; i++){
sum += a;
}
x = sum;
}
// 可以回傳多個值
function ret2var() public pure returns (int xx,int yy){
xx = 9487;
yy = 9453;
}
// call function 範例
function call2func() public pure{
(int a,int b) = ret2var();
a += 1;
b += 2;
}
/*
資料位置有三種
memory, storage, calldata
*/
// mapping 範例,總之就是類似 py 的 dict
function write_dict(string memory name, int val) public{
mp[name] = val;
}
function dict(string memory name) public view returns(int){
return mp[name];
}
// Array
int [] ar;
function push_array(int val) public returns(int [] memory, uint256){
ar.push(val);
uint256 len = ar.length;
return (ar, len);
}
// 要碰到 msg.value 需要使用 payable
function msg_info() public payable returns(address, uint256, bytes memory, uint256){
return (
msg.sender, // 發送者訊息
msg.value, // 合約內的以太數量 (wei)
msg.data, // 完整的調用資料
gasleft() // 剩下的 gass
);
}
// message sender 的 address
address public owner = msg.sender;
function adr() public view returns(uint balance){
// 用戶餘額 Ether
balance = owner.balance;
}
}