Solidity 教學: 角色和全局訊息 Global Variables

本篇預計閱讀時間 15 分鐘, 實作時間 10 分鐘。

在開始之前先來思考底下的練習題,閱讀後即可練習完成。

練習題

  • 練習題 1

    • 如何使用一個型別為 address 的變數 owner 儲存合約的持有者,將持有者定義為自己?

  • 練習題 2

    • 如何寫出一個可以計算當前時間過了一天後的時間?

 

全局訊息(或稱全局變數)和平常我們在寫其他程式語言的定義稍有不同,

這裡指的是區塊鏈提供給我們的訊息,而非指我們自己定義出來的變數當前的資訊是什麼。

通常使用全局變數來聆聽區塊鏈上的交易資訊和訊息。

Msg

msg 代表這個 contracts 當前收到什麼訊息,msg 的所有成員都可以因為每一次外部呼叫函式時的呼叫方不同,而產生變化。

  • msg.data (bytes): 完整的 calldata (呼叫資料)

  • msg.sender (address): 誰傳遞了這個訊息,或指訊息的來源地址是什麼

  • msg.value (uint): 傳遞來的訊息,傳遞了多少 wei

  • msg.gas: 剩餘的 gas 是多少,已被 gasleft() 取代

在 Solidity 中,要執行一個函數通常會需要外部有一個 caller。如果今天沒有人呼叫這個函數,這個函數就會一直靜靜地躺在區塊鏈之中什麼都不做。

反之,msg.sender 就會一直存在。

pragma solidity ^0.8.11;

contract myContract {
    mapping (address => uint) favoriteNumber;
    function setMyNumber(uint _myNumber) public {
    favoriteNumber[msg.sender] = _myNumber;
}

    function whatIsMyNumber() public view returns (uint) {
// 如果這個 sender 還沒有呼叫過 setMyNumber,那最後會回傳 0
    return favoriteNumber[msg.sender];
    }
}

msg 有兩個屬性,一個是 msg.sender,另一個是 msg.value,這兩個值可以被任何 external 函數調用,包含庫裏面的函數。

calldatamsg.dataexternal 函數的參數們。

Block

block 就是區塊鏈的訊息,可以藉由調用下列函數來知道一些資訊。

  • block.basefee (uint): 當前區塊的基本費用 (EIP-3198 and EIP-1559)

  • block.chainid (uint): 當前的 chain id

  • block.coinbase (address payable): 當前區塊礦工地址

  • block.difficulty (uint): 當前區塊難度

  • block.gaslimit (uint): 當前區塊的 gas limit

  • block.number (uint): 當前區塊編號

  • block.timestamp (uint): 當前區塊的 timestamp,使用 UNIX 時間秒

官方強烈建議不要依賴 block.timestamp 來做為隨機數的種子,除非我們真的知道他要用來做什麼。

block.timestamp 可能被礦工有意無意的影響。壞礦工可能會使用特定的 Hash 來執行賭場支付函數(casino payout function on a chosen hash),如果沒有得到報酬,只需要重複嘗試不同的 hash。

當前的 block.timestamp 必須「嚴格大於」前一個區塊,但唯一的保證是這個區塊會在兩個執行區塊的 timestamps 之間。不一定代表正確的時間,只是兩者之間的某處。

在 version 0.7.0, now 被移除了,現在我們必須只能使用 block.timestamp。此外我們還需要謹慎使用 block.timestamp 和 blockhash,因爲他們都是有可能被篡改的。

ABI

encode(編碼)、decode(解碼)

  • abi.decode(bytes memory encodedData, (...)) returns (...): 對特定資料進行 ABI 解碼

  • abi.encode(...) returns (bytes memory): 對特定參數進行 ABI 編碼

  • abi.encodePacked(...) returns (bytes memory): 對給定參數執行非特定的包裝模式編碼(Non-standard Packed Mode)

  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): 對特定參數從第二個或前置特定的 4bytes selector 來做 ABI-encodes

    • 換句話說,對給定參數進行編碼,並以給定的函數選擇器 Function Selector 的前 4 個字節 bytes 數據回傳

  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): 效用等同於 abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)

abi.encodePacked 為非特定的包裝模式(Non-standard Packed Mode)

  • 長度低於 32 bytes 的類型不會進行符號擴展也不會補零。反之在 abi.encode 的編碼中,如果資料低於 32 bytes 就會用零補滿

  • 動態類型會直接進行編碼,並不會包含長度訊息

(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

 

對以下訊息進行編碼 int16(-1), bytes1(0x42), uint16(0x03), string("Hello, world!") :

0xffff42000348656c6c6f2c20776f726c6421^^^^                                 int16(-1)^^                               bytes1(0x42)^^^^                           uint16(0x03)^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!")without a length field

Cryptographic Functions

  • blockhash(uint blockNumber) returns (bytes32):給定一個區塊,返回它的 hash 值 , 只有最近工作的 256 個塊的 hash 值

  • keccak256(bytes memory) returns (bytes32):使用 Keccak-256 加密演算法計算之後的結果

  • sha256(bytes memory) returns (bytes32):使用 SHA-256 加密演算法計算之後的結果。等同於 Ethereum-SHA3 hash 演算法

  • ripemd160(bytes memory) returns (bytes20):使用 RIPEMD-160 加密演算法計算之後的結果

  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns(address):通過簽名訊息來恢復非對稱加密的公鑰地址,出錯會回傳 0

TX

  • tx.gasprice (uint): 發起呼叫交易中的 gas 價格

  • tx.origin (address):(從EOA, External Owned Accounts)交易發送方地址

搜尋資料時發現在智慧合約中使用 tx.origin (address) 進行身份驗證會使合約容易受到類似網路釣魚的攻擊的言論,更多的時候是使用 msg.sender == owner 來進行判斷。

拒絕外部合約呼叫當前合約則可使用 require(tx.origin ==msg.sender)來進行實現。

這三者的內置變量都預設式 address payable

  • msg.sender

  • tx.origin

  • block.coinbase

除了上述提到的全局訊息(變量)們,this 以及之後在繼承會提到的 super 都屬於全局訊息。

msg.sender 和 tx.origin 的差別是什麼呢?

主要就是在 msg.sender 可以是一個合約帳戶,而 tx.origin 會回傳最初發送交易的地址,其不能是一個合約帳戶。

而 Vitalik Buterlin 在 Stackoverflow 中曾表示過相關想法:How do I make my DAPP "Serenity-Proof?"

練習題解答

  • 練習題解答 1

    • 如何使用一個型別為 address 的變數 owner 儲存合約的持有者,將持有者定義為自己?

//SPDX-License-Identifier: MIT 

pragma solidity ^0.8.11;

contract Practice{ 

    address public owner;



    constructor() public { 

        owner = msg.sender;

    } 

}
  • 練習題解答 2

    • 如何寫出一個可以計算當前時間過了一天後的時間?

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.11;

contract Practice{

    function tomorrow() public view returns(uint){ 
        uint now = block.timestamp;
        return now + 86400;
    }
}