Yulのコントラクト呼び出しの仕組み
執筆者 Mark Jonathas
Yulは、スマートコントラクト内でアセンブリ言語の一種を記述するために使用できる中間プログラミング言語です。構文、ストレージ、Yulにおけるメモリの仕組みを理解したところで、次はコントラクトの呼び出し方を見ていきましょう。
Yulにおけるコントラクト呼び出しの仕組み
このシリーズの最終セクションでは、Yulにおけるコントラクト呼び出しの仕組みを見ていきます。いくつかの例に入る前に、いくつかのYulオペレーションについて学ぶ必要があります。見ていきましょう。
では、これらの例で使用する新しいコントラクトを見ていきましょう。まず、これから呼び出す対象のコントラクトを見てみます。
このコントラクトには、それぞれストレージスロット1と2に格納されているvar1とvar2という2つのストレージ変数があります。
関数a()は、ユーザーがコントラクトに少なくとも1 ether送金することを要求しており、そうでない場合はrevertします。
続いて、関数a()はvar1とvar2を更新し、それらを返します。
関数b()は単にvar1とvar2を読み取り、それらを返します。
コントラクトCallMeを呼び出す私たちのコントラクトに進む前に、少し時間を取ってfunction selector(関数セレクタ)を理解しておく必要があります。
トランザクション0x773d45e000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002の以下のcalldataを見てみましょう。
calldataの最初の4バイトは、function selector(0x773d45e0)と呼ばれるものです。これはEVMがどの関数を呼び出したいのかを知る方法です。function selectorは、関数シグネチャの文字列のハッシュの最初の4バイトを取得することで導出されます。
したがって、関数a()のシグネチャはa(uint256,uint256)になります。
この文字列のハッシュを取ると、次のようになります: 0x773d45e097aa76a22159880d254a5f1db8365bc2d0f0987a82bda7dfd3b9c8aa。
最初の4バイトを見ると、0x773d45e0であることがわかります。
シグネチャにスペースがないことに注目してください。これは重要な点で、スペースを追加すると全く異なるハッシュになってしまいます。今回のコード例ではselectorを取得する心配はありません。私が提供します。
ストレージレイアウトから見ていきましょう。
var1とvar2がコントラクトCallMeと同じレイアウトになっていることに注目してください。delegatecall()が正しく機能するためには、レイアウトが他方のコントラクトと同じである必要があると述べたのを覚えているかもしれません。
新しい変数(selectorAとselectorB)を末尾に追加する限り、この条件を満たしつつ他の変数を持つことができます。これにより、ストレージの衝突を防ぐことができます。
これで最初のコントラクト呼び出しを行う準備が整いました。
Yulでstaticcall()を使う方法
まずは簡単なもの、staticcall()から始めましょう。以下が私たちの関数です:
行われていることは以下の通りです:
- スロット2をロードして、ストレージからb()のfunction selectorを取得する(両方のselectorが1つのスロットにパックされている)
- selectorBを取り出すために4バイト(32ビット)右にシフトする
- function selectorをメモリのscratch spaceに格納する
- staticcallを実行する
- gas()を渡す(ガス量を指定することも可能)
- コントラクトアドレスとしてパラメータ_callMeを渡す
- 関数呼び出しが成功したか確認し、失敗した場合はデータなしでreturnする
- メモリからデータをreturnし、値1と2を確認する
注: 0x1cと0x20は、格納した値の最後の4バイトをscratch spaceに渡したいことを表しています。staticcall()の最後の2つのパラメータは、返り値をメモリ位置0x80〜0xcoに格納したいことを指定しています。
Yulでcall()を使う方法
次にcall()を見ていきましょう。CallMeの関数a()を呼び出します。コントラクトに少なくとも1 ether送金することを忘れないでください!この例では、_var1と_var2として3と4を渡します。
コードは以下の通りです:
前回の例と同様、slot2をロードする必要があります。ただし今回は、selectorAを取り出すためにselectorBをマスクします。
次に、selectorを0x80に格納します。
calldataからパラメータが必要なため、calldatacopy()を使用します。calldatacopy()に対して以下を指示します:
- calldataをメモリ位置0xa0に格納する
- 最初の36バイトをスキップする
- calldataのサイズから36バイトを引いたサイズを格納する
注: スキップする最初の4バイトはcallA()のfunction selectorで、続く32バイトはcallMeのアドレスです。
これでコントラクト呼び出しを行う準備が整いました!
前回と同様、gas()と_callMeを渡します。ただし今回は、0x9c(0x80メモリ系列の最後の4バイト)〜0xe0のcalldataを渡し、データをメモリ位置0x100〜0x120に格納します。
再び、呼び出しが成功したか確認し、出力をreturnします。コントラクトCallMeを確認すると、値が正常に3と4に更新されていることがわかります。
何が起きているかをより明確にするため、returnする直前のメモリレイアウトを以下に示します:
Yulでdelegatecallを使う方法
最後のセクションでは、delegatecall()を見ていきます。コードはcall()とほぼ同一で、変更点は1つだけです。
変更したのはcall()をdelegatecall()に変えたことと、callvalue()を削除したことだけです。
callvalue()が不要な理由は、delegatecallがCallMeのコードを自身の状態内で実行するためです。したがって、a()内のrequire()文は、私たちのCallerコントラクトにetherが送金されたかどうかをチェックしています。CallMe内のvar1とvar2を確認すると、変更がないことがわかります。一方、Callerコントラクト内のvar1とvar2は正常に更新されています。
これでコントラクト呼び出しに関するセクションは終わりです!次は、YulにおけるMemory、Storage、Smart Contract Callsの仕組みについて学びましょう。
関連する概要

ブロックチェーンで魔法を生み出す
Alchemyは、最も強力なweb3開発者向けプロダクトとツールを、豊富なリソース、コミュニティ、そして卓越したサポートと組み合わせて提供します。


