📖

Chapter4: グローバル変数と関数 | Solidity Programming Essentialsを読む

2022/08/03に公開

イーサリアムの知識を整理するために2022年6月発売のSolidity Programming Essentials 2nd Editionを読み進める試みです。この記事ではChapter4「グローバル変数と関数」を読み進めます。

Solidity Programming Essentials: A guide to building smart contracts and tokens using the widely used Solidity language, 2nd Edition (English Edition)

読書ログは以下のスクラップで逐次更新していきます。
https://zenn.dev/mah/scraps/ea8c79961ae8c8


この章で扱われるトピック

  • 変数のスコーピング(Variable scoping)
  • 型変換(Type conversion)
  • ブロック関連のグローバル変数(Block-related global variables)
  • トランザクション関連のグローバル変数(Transaction-related global variables)
  • 暗号化グローバル変数(Cryptographic global variables)
  • アドレス関連のグローバルプロパティと関数(Address-related global properties and functions)
  • コントラクト関連のグローバル変数と関数(Contract-related global variables and functions)
  • ecrecoverを使ったアドレスの復元(Recovering addresses using ecrecover)

変数のスコーピング

Solidityでは変数を宣言できる場所として以下の2つがある。

  • コントラクトレベルのグローバル変数(ステート変数とも呼ばれる)
  • 関数レベルのローカル変数

ステート変数には以下のスコープがある。

  • public: 外部からの呼び出しで直接アクセスできる。コンパイラによってgetter関数が暗黙に生成される。
  • internal: 外部から直接アクセスはできない。現在のコントラクトまたはそこから派生した子コントラクト内の関数からアクセス可能。
  • private: 外部から直接アクセスできず、子コントラクトの関数からもアクセスできない。現在のコントラクト内の関数からのアクセス可能。

型変換

Solidityは静的型付け言語であり、コンパイル時に定義されたデータ型は変数の有効期間中は変更することができない。一方で、ある型の変数から別の型の変数に値をコピーするために変換が必要であり、これを型変換と呼ぶ。

暗黙的な変換

Solidityでは小さい整数型から大きい整数型への暗黙の変換が可能である。例えばuint8からuint16への変換は暗黙のうちに行われる。

明示的な変換

明示的な変換は、データの損失が起こるなどの理由でコンパイラが暗黙的な変換を行わない場合に必要となる。

以下の場合は明示的な変換は必要ない。

function ConversionExplicitUINT8toUINT256() pure public returns (uint) {
  uint8 myVariable = 10;
  uint256 someVariable = myVariable; // uint8 → uint256
  return someVariable;
}

以下の場合は明示的な変換が必要となる。

function ConversionExplicitUINT256toUINT8() pure public returns (uint) {
  uint256 myVariable = 10;
  uint8 someVariable = uint8(myVariable); // uint256 → uint8
  return someVariable;
}

以下、様々なバリエーションを示す。

function Conversions() pure public {
  uint256 myVariable = 10000134;
  uint8 someVariable  = 100;
  bytes4 byte4 = 0x65666768;

  // bytes1 byte1 = 0x656667668; // 範囲外の値を代入しようとするとエラー
  bytes1 byte1 = 0x65;
  // byte1 = byte4; // データ損失があるため暗黙的な変換はできない

  byte1 = bytes1(byte4) ; // 明示的な変換
  byte4 = byte1;  // 暗黙的な変換
  // uint8 someVariable = myVariable; // データ損失があるため暗黙的な変換はできない

  myVariable = someVariable; // 暗黙的な変換

  string memory name = "Ritesh";
  bytes memory nameInBytes = bytes(name); // stringからbytesへの明示的な変換
  name = string(nameInBytes); // bytesからstringへの明示的な変換
}

ブロックとトランザクションのグローバル変数

Solidityではコントラクト内で宣言されていないがコントラクト内のコードからアクセス可能なグローバル変数を提供している。

変数名 (返り値の型) 説明
block.coinbase (address payable) ブロックのマイナーのアドレスを取得する(etherbaseと同様)
block.difficulty (uint) 現在のブロックの採掘難易度
block.gaslimit (uint) 現在のブロックのガスリミット
block.number (uint) ブロックナンバー
block.timestamp (uint) ブロックが作成された時間
msg.data (bytes) トランザクションを作成した関数とそのパラメータに関する情報
msg.sender (address) 関数を呼び出した呼び出し元のアドレス
msg.sig (bytes4) 関数識別子としてハッシュ関数のシグネチャの後の4バイトを返す
msg.value (int) トランザクションで送信されたweiの量
Gasleft() (uint256) ガス残量
tx.gasprice (uint) トランザクションのガス料金
tx.origin (address) トランザクションの最初の呼び出し元アドレス
block.blockhash(uint blockNumber) (bytes32) トランザクションを含むブロックのハッシュ値
Block.chainid (uint) 現在のチェーンID
Block.basefee (uint) 現在のブロックのbase fee

tx.originとmsg.senderの違いについて

グローバル変数tx.originはトランザクションを開始した元の外部アカウントを指し、msg.senderは関数を呼び出した直後のアカウントを指す。tx.originは常に外部アカウントを参照するが、msg.senderは外部アカウントでもコントラクトアカウントでも良い。

複数のコントラクトで複数の関数を呼び出した場合、呼び出されたコントラクトのスタックに関係なく、tx.originは常にトランザクションを開始した外部アカウントを参照する。しかし、msg.senderは次のコントラクトを呼び出す直前のアカウント(コントラクト/外部)を参照する。従って関数の呼び出し元を参照したい場合はtx.originではなくmsg.senderを使用することが推奨される。

暗号化グローバル変数

Solidityではコントラクト関数内でハッシュ化するための暗号化関数を提供している。ハッシュ関数にはSHA2とSHA3の2種類がある。

sha3またはkeccak256関数は入力をSHA3アルゴリズムに基づくハッシュに変換し、sha256は入力をSHA2アルゴリズムに基づくハッシュに変換する。ハッシュ化する必要がある場合はkeccak256関数を使用するが推奨される。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract HashTest {
  function CryptoDemo() pure public returns (bytes32, bytes32) {
    return (sha256("r"), keccak256("r"));
  }
}

実行結果は以下の画像のようになる(Remix IDEで実行)

アドレスグローバル変数

アドレスに関連するグローバル変数は3つ、グローバル関数は5つ。グローバル変数は以下がある。

  • <address>.balance (uint256):アドレスが持つ現在のEther残高(Wei単位)
  • <address>.code (bytes memory):EVMバイトコード
  • <address>.codehash (bytes32):EVMバイトコードのkeccak256ハッシュ

グローバル関数は以下の通り。

  • <address>.transfer(uint256 amount):指定された量のweiをアドレスに送信し、失敗した場合は例外を投げる。
  • <address>.send(uint256 amount) returns(bool):指定された量のweiをアドレスに送信し、失敗した場合はfalseを返す。
  • <address>.call(...) returns(bool):この関数は低レベルのcallを発行し、関数呼び出しによるステータスと結果を含むタプルを返す。
  • <address>.staticcall(...) returns(bool):この関数は低レベルのcallcodeを発行し、関数呼び出しによるステータスと結果を含むタプルを返す。
  • <address>.delegatecall(...) returns(bool):この関数は低レベルのdelegatecallを発行し、関数呼び出しによるステータスと結果を含むタプルを返す。

これらの関数について詳しくはChapter6で解説されるとのこと。

コントラクトグローバル変数

コントラクトの内側からはthisを通じて現在のコントラクトにアクセスすることができる。コントラクトのアドレスを取得したい場合はaddress(this)とする。

またコントラクトを破棄する場合はselfdestruct(address payable)を使用する。寿命のあるコントラクトを表現する際に活用できる。

ecrecoverを利用したアドレスのリカバリ

Solidityにはecrecoverという強力な関数があり、電子署名を元に送信者のアドレスを導き出すことができる。ecrecover関数はSolidityでのみ利用可能であり、ecrecover(messageHash, v, r, s)という4つのパラメータをとる。

  • messageHash:コントラクトに送られたハッシュ化されたメッセージ
  • s:デジタル署名の最初の32バイト
  • r:デジタル署名の66ポジションから130ポジションまでの32バイト
  • v:デジタル署名の最後の1バイト

s, r, vはそれぞれこのような形で取得することができる。

$ geth attach http://127.0.0.1:8545
> eth.accounts[0]
"0x1eba08d92a13df7565a48577cef0db94cd27f3f0"
> var msg = web3.sha3("hello, mah!")
> console.log(msg)
0x24e77fefc96ddfa7beeb1721f9eb46f1d0698e6b858afd716ab37aa02dcf6cd0
> var sig = eth.sign(eth.accounts[0], msg)
> console.log(sig)
0x21aa1f5f54bc36ea975e2bac529a43f29f135e48830ba3d2e8e52e6336c8bed75b94bc259a7277c7157d75ea424bea44ce25e3991e2bb1292f318bfaeed58cff1b
> var r = sig.substr(0, 66)
> var s = "0x" + sig.substr(66, 64)
> var v = "0x" + sig.substr(130, 2)
> console.log(r)
0x21aa1f5f54bc36ea975e2bac529a43f29f135e48830ba3d2e8e52e6336c8bed7
> console.log(s)
0x5b94bc259a7277c7157d75ea424bea44ce25e3991e2bb1292f318bfaeed58cff
> console.log(v)
0x1b

Discussion