本篇預計閱讀時間 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
函數調用,包含庫裏面的函數。
calldata
為 msg.data
和 external
函數的參數們。
Block
block
就是區塊鏈的訊息,可以藉由調用下列函數來知道一些資訊。
-
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;
}
}