コンテンツへスキップ
0%

Yulのコントラクト呼び出しの仕組み

Mark Jonathas headshot

執筆者 Mark Jonathas

2023年8月1日 公開読了時間 3 分

Yulは、スマートコントラクト内でアセンブリ言語の一種を記述するために使用できる中間プログラミング言語です。構文、ストレージ、Yulにおけるメモリの仕組みを理解したところで、次はコントラクトの呼び出し方を見ていきましょう。

Yulにおけるコントラクト呼び出しの仕組み

このシリーズの最終セクションでは、Yulにおけるコントラクト呼び出しの仕組みを見ていきます。いくつかの例に入る前に、いくつかのYulオペレーションについて学ぶ必要があります。見ていきましょう。‍

Instruction
Explanation

gas()

実行に使用可能な残りのガス量

gasPrice()

トランザクションのガス価格

address()

現在のコントラクトのアドレス

balance(a)

アドレスaが保有するether残高(wei単位)

selfbalance()

balance(address())を呼び出すのと同じだが、ややコストが低い

caller()

コントラクトを呼び出したアドレス(msg.sender)

origin()

トランザクションを発行したアドレス(tx.origin)

callvalue()

コントラクト呼び出しで送金されたether量(wei単位、msg.value)

calldataload(p)

位置pから始まるcalldata(32バイト分のみ)

calldatacopy(t, f, s)

calldataをメモリ位置tにコピーする。calldata内の位置fから開始し、sバイト分のデータをコピーする。

extcodesize(a)

アドレスaにあるコードのサイズ

returndatasize()

直近のreturndataのサイズ

returndatacopy(t, f, s)

return data内の位置fからsバイトをメモリの位置tにコピーする

timestamp()

現在のブロックのタイムスタンプ(秒単位)

number()

現在のブロック番号

call(g, a, v, in, insize, out, outsize)

アドレスaにあるコントラクトを、ガスgで呼び出し、v weiをmsg.valueとして渡し、メモリ位置in〜insizeからtx.dataを渡し、返り値をメモリ位置out〜outsizeに格納する。呼び出しが成功した場合は1を、そうでない場合は0を返す。

delegatecall(g, a, in, insize, out, outsize)

call()と似ているが、主な違いはdelegatecall()が呼び出し元コントラクトの状態変数を更新する点である。プロキシコントラクトでよく使用される。意図しないストレージ変数の上書きを避けるため、呼び出し先コントラクトとストレージレイアウトが一致している必要があることに注意。パラメータvが存在しない点にも注意。delegatecall()ではetherを送金できない。

staticcall(g, a, in, insize, out, outsize)

call()と似ているが、ブロックチェーンの状態を変更するコントラクト(つまりpureおよびview以外の関数)を呼び出すことはできない。パラメータvが存在しない点に注意。staticcall()ではetherを送金できない。

では、これらの例で使用する新しいコントラクトを見ていきましょう。まず、これから呼び出す対象のコントラクトを見てみます。

このコントラクトには、それぞれストレージスロット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する直前のメモリレイアウトを以下に示します:

Memory Location
Value Stored

0x00

Scratch Space (空)

0x20

Scratch Space (空)

0x40

0x80(Yulでは全ての操作を手動で行うため、Free Memory Pointerを更新していません)

0x60

0x80

0x0000000000000000000000000000000000000000000000000000000000773d45e0(Function Selector)

0xa0

0x0000000000000000000000000000000000000000000000000000000000000003(_var1)

0xc0

0x0000000000000000000000000000000000000000000000000000000000000004(_var2)

0xe0

0x100

0x0000000000000000000000000000000000000000000000000000000000000003(最初の返り値)

0x120

0x0000000000000000000000000000000000000000000000000000000000000004(2番目の返り値)

0x140

Free Memory Pointer(空)

Yulでdelegatecallを使う方法

最後のセクションでは、delegatecall()を見ていきます。コードはcall()とほぼ同一で、変更点は1つだけです。

変更したのはcall()をdelegatecall()に変えたことと、callvalue()を削除したことだけです。

callvalue()が不要な理由は、delegatecallがCallMeのコードを自身の状態内で実行するためです。したがって、a()内のrequire()文は、私たちのCallerコントラクトにetherが送金されたかどうかをチェックしています。CallMe内のvar1とvar2を確認すると、変更がないことがわかります。一方、Callerコントラクト内のvar1とvar2は正常に更新されています。

これでコントラクト呼び出しに関するセクションは終わりです!次は、YulにおけるMemoryStorageSmart Contract Callsの仕組みについて学びましょう。

Background gradient

ブロックチェーンで魔法を生み出す

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