[Solidity 實戰全書] 第 2 章 型別 Types & 變數 Variables

Solidity 有別於 Python、Javascript 這樣的弱型別語言,它是一個更像是 C/C++、Java 的強型別語言。所以每次在宣告變數時,或者定義函數回傳值時,都需要去標明定義型態。

在Solidity 中所有變數都會被預設地初始化:

所以型態裡面不存在null或undefined (u)int = 0 bool = false string = "" 其中所有可視性為 public 的變數都會自動生成一個相同名稱的 Getter Function,我們在 Remix IDE 可以看見它的存在。就像是我們在上一個章節所做的 Hello World 合約一樣,我們可以點選 REMIX 上的 Hello World 變數來看見 “Hello World!” 這個變數回傳值。

而參考型別(reference types) 的變數需要標明記憶體配置(memory location: memory 或 storage),這部分在未來會詳述。

【Integer】

在 Solidity, uint 就是 uint256,為一個 256-bit 的 unsigned integer。同時也可以宣告其他 bits,或使用 int 來宣告其他的負整數。

uint 在其後宣告完 bits 之後該整數的數值範圍為 0 ~ (2^n) - 1int 在其後宣告完 bits 之後該整數的數值範圍為 -2^(n-1) ~ (2^(n-1)) - 1

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract MyInt {
    uint256 public myUint = 566778778787;
    uint32 public myUint32 = 4294967295;
    uint16 public myUint16 = 65535;
    uint8 public myUint8 = 255;

    int256 public myInt = -566778778787;
    int32 public myInt32 = -2147483648;
    int16 public myInt16 = -32768;
    int8 public myInt8 = -128;
}

需要注意的是在 Solidity 中沒有 doublefloat 這些小數型態,也就是說如果我們要使用小數在當前單位,則要作單位換算成更小單位,如此一來就能表現成整數型態了。

【Bool】

在 Solidity 中的布林值以 truefalse 表示,但其實 Boolean 只是一個 binary type。

pragma solidity ^0.8.11;
contract MyBoolean {
    bool public myBoolean = false;
}

可以使用反面表述:myVar = !myVar。也可以使用邏輯運算:&&, ||。但需要注意的是 Solidity 並不支援布林值的自動轉換,所以我們不可以在 condition statement 中(if-else, while⋯),或賦值給 bool type 變數時使用 1 和 0 分別代表 true 和 false。

【Address】

Address 可以被用來儲存一個以太坊地址指向的帳戶或智能合約。表面上來看是一個長度為 20 byte 的十六進制字串(等於 160 bits 或 40 hex characters),只要通過 Address Checksum Test 就會被視為 Address。任何在 Ethereum 的互動都是 Address based 的。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract MyAddress {
    address public myAddress = address(0xB42faBF7BCAE8bc5E368716B568a6f8Fdf3F84ec);
// 歡迎匯 ETH 到這個地址(開玩笑地)!
    address public myContractAddress = address(this);
    uint256 public balanceOfMyContract = myContractAddress.balance;
}

當我們在宣告 address 時加上 payable 這個修飾詞(modifier)時,則此 address 就會多兩個成員(members),分別為 transfersendaddress payable 代表可用於支付的地址,反之 address 就不可用於支付任何 eth,也不包含以上多的兩個成員。

Address and Msg.Object

這邊提供外部帳戶和合約帳戶這兩種 Account 的概念,EOA 比較像是我們使用 MetaMask 等錢包或媒介使用的帳戶,而 Contract Account 類似於在區塊鏈「內」自動化運作的一個帳戶。

  • Externally Owned Account(EOA)

    *   用於交易資金、初始化交易、佈署智能合約、提領
    
    • 擁有一個私鑰來管理帳戶
  • Contract Account

    *   沒有私鑰來管理帳戶,程式碼決定了整個合約帳戶的運作
    

對於 Contract Account 來說,只要我們沒有在程式碼裡面寫上 owner 或相關的操作定義,這個 Contract Account 就會失去人為的控制。但反之只要我們寫上了完善、完整的函式之後,就可以透過「操控特定函式」這樣一個「發送交易」的行為來控制合約。

換句話說如果這個合約裡面有非常多供特定人士操作的合約,那這個合約從廣義來說便不是真正去中心化的!

智能合約的互動與帳戶關係

在區塊鏈上我們只能使用 EOA 初始化交易,也就是說在任何的互動過程中,只要原點是 EOA,接下來無論是 Contract Account 或 EOA 皆可與彼此交互運作。

  • 使用EOA 初始化交易

    *   途徑常為:EOA -> Contract Account -> Contract Account...(EOA as beginner)
    
    • 途徑不可為:Contract Account -> Contract Account...(Contract Account as beginner)
    • 只要原點是EOA,接下來無論是Contract Account或EOA皆可與彼此交互運作

成員函式與合約之間互動的方法

  1. .balance

    *   可以得到當前地址的餘額,以 wei 為單位。
    
    • <address>.balance()
  2. .transfer

    *   有 gas 限制,最大 2300,這個限制可以用來防止重送攻擊(Re Entrancy Attacks)
    
    • <address payable>.transfer(uint256 amount)
    • 此函式會從合約轉入帳號 amount 價值的以太幣。這邊需要注意當我們沒有特別宣告單位時,uint 型態變數的輸入值都會是以 wei 為單位
    • 如果執行失敗會拋出錯誤,接收對象若是合約需要注意其中是否有 fallback function 存在
    • 自智能合約傳送相對應數量(以 wei 為單位)的 ether 給指定地址。在這裡和我們平常撰寫物件導向程式語言的寫法方向不太一樣,可以稍微注意一下
  3. .send

    *   有 `gas` 限制,最大2300
    
    • <address payable>.send(uint256 amount) returns (bool)
    • 此函式會從合約轉入帳號 amount 價值,wei 單位的 eth
    • 是一個屬於 .transfer 的低階部件,如果執行 .send 失敗的話不會把整個合約停止,而是會 return false。不會改變任何的「狀態」。
  4. .call

    *   沒有 `gas` 限制
    
    • <address>.call(bytes memory) returns (bool, bytes memory)
    • send 一樣,如果執行失敗不會停止而是 return false
    • 是較為適合轉入合約的狀況使用,因為 call 可以調整 gas ,也可以註明 Function Signature 來與轉入合約的 Function 做對接
    • .call(bytes memory) returns (bool, bytes memory)
    • .call{value: amount}( bytes memory ) 功能類似
      .transfer 會從合約轉入帳號 amount 價值的 eth
    • .call{gas: amount}( bytes memory ) 可以調整供給的 gas 數量,並且 returns 一個 boolean 值
  5. .delegatecall:和 call 基本上一樣,只是使用 delegatecall 時不能使用 value 但可以附帶註明 gas
  6. .staticcall:和 call 基本上一樣,只是使用 staticcall 時不能改變到 contract 中的任何狀態(static)

為了確保匯款或提款時的安全性,實作以上函式時我們都必須不斷地檢查返回結果(Return 或 Console Window 的資訊)。

必須要知道的是 .call、.delegatecall、.staticcall 以上三者都屬於底層呼叫,可以呼叫 gas 參數,而且在發生錯誤時不會拋出異常只會回傳 false。此三者都是非常底層的函數,所以我們應該要盡量避免直接在我們的智能合約 Hard￾Code Gas,除非我們十分確定為何要這麼做。

Call() 屬於一個底層的函式,也是底層對外的接口。我們能用 Call() 來對一個合約發送 message。這個函式支援任意型態的參數,它會將這些參數包裹成32bytes 的形式傳遞給合約。

function myFunction(uint _x, address _addr)
public
returns(uint, uint) {
// do something
return (a, b);
}
// function signature 的字串中不可以有任何的空白
(bool success, bytes memory result)
= addr.call(abi.encodeWithSignature("myFunction(uint,address)",
10, msg.sender));

在回傳值中除了 boolean 以外,還有一個 bytes array。想要看到函式回傳結果就得使用以下方法來 decode。

(uint a, uint b) = abi.decode(result, (uint, uint));

我們還可以使用以下方法來呼叫:

address(nameReg).call{
gas: 1000000,
value: 1 ether
}(abi.encodeWithSignature("myFunction(uint, address)",
myIntParameter, myAddress));

而 delegatecall() 和 call() 基本上功能是類似的,區別在於 delegate 只會使用參數地址的代碼,其他 message 則使用當前合約的內容,像是 balance 等。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract MyPayable {
function transferEth(address payable _user) public payable {
_user.transfer(msg.value);
}
function sendEth(address payable _user) public payable {
bool didSend = _user.send(msg.value);
require(didSend, "This failed to send");
}
function callEth(address payable _user) public payable {
(bool didSend, ) = _user.call{value: msg.value}("");
require(didSend);
// 如果要使用 <address>.call.value() 這個方法,那麼被呼叫的函數必須添
// 加 payable 修飾符,否則轉帳會失敗
}
function ContractBalance() public view returns(uint){
address contractAccount = address(this);
return contractAccount.balance;
}
function myBalance() public view returns(uint){
address myAccount = msg.sender;
return myAccount.balance;
}
receive() external payable {}
fallback() external payable {}

如果使用

.transfer(uint amount)、
.send(uint amount),這個
代稱的合約地址中必須增加 fallback 回調函數!Call() 需要與 fallback 和 receive 函式進行互動,當我們「呼叫一個函式,並傳了 ether 且沒有提供其他訊息」會與 receive function 對應上。當我們沒有任何 Function Signature 與之對應時就會和 fallback function 對應上。這部分在函式章節會特別詳述。  
 
#### address vs. address payable 在這裡更深入地探討 <address><address payable>
 
兩者的區別僅在編譯時存在,在編譯後的合約代碼中就沒有區別了。
 
而在編譯時兩者最大的區別就在型別轉換:
// address payable類型的變量可以顯式或隱式地轉換為address類型
address payable addr1 = msg.sender; address addr2 = addr1;
// 正確 address addr3 = address(addr1);
// 正確
// address類型的變量只能顯式地轉換為address payable,需要首先轉換為整數類型(例如uint160),
// 然後再將該整型值轉換為address類型,就可以得到address payable: address addr1 = msg.sender; address payable addr2 = addr1;
// 錯誤,address不能隱式地轉換為address payable address payable addr3 = address(uint160(addr1));
// 正確,先轉換為uint160,然後轉換為address payable address payable addr4 = payable(addr1); // 可以直接使用 payable() 來顯式轉換
 

【Contract & This】

每一個智能合約的基本單位都是一個一個的 contract,在這裡我們將這個contract 命名為 HelloWorld。也就是說在一個 .sol 檔案裏面,我們可以有多個contract。Contract 這個概念很像是物件導向程式設計語言中的類別,這樣去理解便可以很好地體會 Contract 的意涵。

所有的合約都可以被轉為 address 型態 , 所以當然也可以使用 address 的相關成員,像是 address(this).balance.。而 this 指的是當前的合約對象,屬於一全局變數(區塊鏈提供的訊息)。

同時我們也可以使用 selfdestruct(address recipt) 來摧毀當前合約,銷毀這個合約之後會把裡面的資金發送到特定的地址。

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;
contract myContract{
    function get_balance() public view returns (uint){
    return address(this).balance;
    }
}

當我們創建一個合約,或者是透過 web3.js 等途徑創建合約時,某種程度上都是呼叫一個和合約同名的 constructor,又被稱作建構子。

【String】

在 Solidity 中和 Python 一樣可以使用雙引號 "" 或者單引號 '' 來表示字串。也同樣可以用反斜槓來代表跳脫字元 "\",並且使用各種跳脫字元的命令。

由於字串可以隱性轉換為 bytes 型態,所以理想的情況下我們在宣告字串的時候可以使用 bytes 或者 uint 來節省更多的 gas:

pragma solidity ^0.8.11;

contract SolidityTest {
    string data_moreGas = "test";
   bytes32 data_lessGas = "test";
}

在 bytes32 的型別之下回傳的結果並不是我們想像中的英文單字 test,可以透過以下這個函式將其轉回 string 的型態,或說我們人類可讀的單字。

//SPDX-License-Identifier: MIT
function bytes32ToString(bytes32 _bytes32)
    public
    pure returns (string memory) {
        uint8 i = 0;
        while(i < 32 && _bytes32[i] != 0) {
        i++;
    }
    bytes memory bytesArray = new bytes(i);
    for (i = 0; i < 32 && _bytes32[i] != 0; i++) {
        bytesArray[i] = _bytes32[i];
    }
    return string(bytesArray);
}

從節省 gas 的角度來看,我們應該要盡量使用最小的空間來儲存字串內容, 例 如 keccak256(0) == keccak256(uint8(0))、keccak256(0x12345678) ==keccak256(uint32(0x12345678))。

常見用法

String 沒辦法直接比較,所以字串比較要用 keccak256 和 abi.encodePacked 來將其轉成可比較的 bytes 型態。相關加密法可見後續角色和全局訊息章節。

pragma solidity ^0.8.11;

contract String {

    function compare(string memory _name) public view returns(bool){
        if (keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik"))){
            return true;
        }
        else{
            return false;
        }
    }
}

把兩個 String 連接起來:

pragma solidity ^0.8.11;

contract SolidityTest {
    function concat(string memory a, string memory b) public view returns(string memory){
        return string(abi.encodePacked(a, b));
    }
}

調用一個字串的長度可以使用以下方法:

pragma solidity ^0.8.11;

contract String {
    function len(string memory _name) public view returns(uint){
        return bytes(_name).length;
    }
}

String 如何在 Solidity 運作是一個非常深奧的事情,從 bytesstring 不僅僅只是 string() 這樣 casting 這麼簡單,之後我們在加密函數的時候再來詳述這個過程!

String vs. Bytes

  1. 兩者都屬於特殊的陣列型態
  2. String 某種程度上等於Bytes,但不具有長度和陣列取值的運算子([]
  3. String 較為昂貴

String 和 Bytes 都屬於複雜的動態陣列,bytes 可以視為普通字元陣列,而 string 的陣列元素型態是 UTF-8 的字元。

【Scope】

Scope 被翻譯為作用域,規範著變數的生命週期與記憶體配置方式。

  • Local
    • 如果宣告在函式內則為範圍變數
    • 並不是儲存在區塊鏈上
  • State
    • 如果宣告在函式外則為狀態變數
    • 儲存在區塊鏈上
    • 預設為 private
  • Global
    • 區塊鏈提供的訊息,又稱為全局訊息,之後會有專門的篇章講解

State variables

狀態變數從我們執行它開始它就會一直存在,也就是說它們是永遠存在區塊鏈上的,所以如果要改他們就要花錢。然而 Local variable 只有在執行函數時才會存在。

狀態變數的 stringvalues type 還可以加上以下兩者 modifier

  • constant: 宣告了 constant 的變數在編譯後就不可以再被更改,不會佔據Storage Slot。
  • immutable: 宣告了 immutable 的變數可以在建構子(constructor)時被修改,在此之後就不可以被更改。建構子的部分之後會講解。

【Practice】

  • Practice 1
    • uint 在其後宣告完 bits 之後該整數的數值範圍為__________。
    • int 在其後宣告完 bits 之後該整數的數值範圍為__________。
  • Practice 2
    • 在 Solidity 之中如何表示浮點數
  • Practice 3
    • 狀態變數 State variables 預設的可視性是 public 還是 private
  • Practice 4
    • 請舉出三種我們可以用來節省 String gas 花費的儲存型態。
  • Practice 5
    • 全局訊息 Global Variables 指的是何者提供的資訊
  • Practice 6
    • 請嘗試舉例如何在 Solidity 之中進行字串比較
  • Practice 7
    • 在一個 .sol 檔案裏面,我們可以有___個 contract
  • Practice 8
    • 在宣告 address 時加上__________ ,代表可用於支付的地址。
  • Practice 9
    • <account>.balance 回傳的單位是__________。
  • Practice 10
    • Solidity 是強型別語言還是弱型別語言,並簡單說明它們兩者的差別。