Solidity イベントは、イーサリアムの「print」または「console.log」ステートメントに最も近いものです。それらがどのように機能し、いつ使用するかを説明します。また、他のリソースでは省略されがちな多くの技術的な詳細についても説明します。
以下は、Solidity イベントを発行する最小限の例です。
contract ExampleContract { // We will explain the significance of the indexed parameter later. event ExampleEvent(address indexed sender, uint256 someValue); function exampleFunction(uint256 someValue) public { emit ExampleEvent(sender, someValue); } }
おそらく最もよく知られているイベントは、 ERC20トークンの転送時に発行されるイベントです。送信者、受信者、および金額がイベントに記録されます。
emit Transfer(from, to, amount);
これは冗長ではないでしょうか?すでに過去のトランザクションを調べて転送を確認できます。その後、トランザクションの呼び出しデータを調べて同じ情報を確認できます。
これは正しいです。イベントを削除しても、スマート コントラクトのビジネス ロジックには影響を及ぼさない可能性があります。しかし、これは歴史を考察する効率的な方法ではありません。
Ethereum クライアントには、「ERC20 のすべての送金トランザクション」など、トランザクションを「タイプ」別にリストするための API がありません。トランザクションをクエリする場合のオプションは次のとおりです。
getTransaction
getTransactionFromBlock
getTransactionFromBlock
API は、特定のブロックでどのようなトランザクションが発生したかを通知することしかできず、スマート コントラクト アドレスの複数のブロックをターゲットにすることはできません。
getTransaction
トランザクション ハッシュがわかっているトランザクションのみを検査できます。
一方、イベントははるかに簡単に取得できます。
Ethereum クライアントのオプションは次のとおりです。
イベント
events.allEvents
過去のイベントの取得
これらのそれぞれでは、クエリアが調べたいスマート コントラクト アドレスを指定する必要があり、指定されたクエリ パラメーターに従ってスマート コントラクトが発行したイベントのサブセット (またはすべて) を返します。
トランザクションそのものではなく、イベントを使用してトランザクションを追跡する理由についての重要な洞察は次のとおりです。イーサリアムは、スマート コントラクトのすべてのトランザクションを取得するメカニズムを提供しませんが、スマート コントラクトからすべてのイベントを取得するメカニズムは提供します。
どうしてこれなの?イベントを迅速に取得できるようにするには、追加のストレージ オーバーヘッドが必要です。イーサリアムがすべてのトランザクションに対してこれを行うと、チェーンがかなり大きくなるでしょう。イベントを使用すると、Solidity プログラマーは、追加のストレージ オーバーヘッドを支払う価値がある情報の種類を選択して、オフチェーンでの迅速な取得を可能にすることができます。
上記の API の使用例を次に示します。このコードでは、クライアントはスマート コントラクトからのイベントをサブスクライブします。これらの例はすべてJavascript で記述されています。
このコードは、ERC20 トークンが転送イベントを発行するたびにコールバックをトリガーします。
const { ethers } = require("ethers"); // const provider = your provider const abi = [ "event Transfer(address indexed from, address indexed to, uint256 value)" ]; const tokenAddress = "0x..."; const contract = new ethers.Contract(tokenAddress, abi, provider); contract.on("Transfer", (from, to, value, event) => { console.log(`Transfer event detected: from=${from}, to=${to}, value=${value}`); });
イベントを遡って確認したい場合は、次のコードを使用できます。この例では、ERC20 トークンの承認トランザクションの過去に注目します。
const ethers = require('ethers'); const tokenAddress = '0x...'; const filterAddress = '0x...'; const tokenAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const tokenContract = new ethers.Contract(tokenAddress, tokenAbi, provider); // this line filters for Approvals for a particular address. const filter = tokenContract.filters.Approval(filterAddress, null, null); tokenContract.queryFilter(filter).then((events) => { console.log(events); });
2 つの特定の既知のアドレス間の取引を探したい場合 (そのような取引が存在する場合)、ethers.js.コードは次のようになります。
tokenContract.filters.Transfer(address1, address2, null);
以下は、ethers.js ではなくweb3.jsでの同様の例です。 fromBlock
およびtoBlock
クエリ パラメーターが追加されていることに注意してください (これらのブロック間のイベントのみを考慮することを示すため)。送信者である複数のアドレスをリッスンする機能を示します。アドレスは「OR」条件で結合されます。
const Web3 = require('web3'); const web3 = new Web3('https://rpc-endpoint'); const contractAddress = '0x...'; // The address of the ERC20 contract const contractAbi = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "to", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" } ]; const contract = new web3.eth.Contract(contractAbi, contractAddress); const senderAddressesToWatch = ['0x...', '0x...', '0x...']; // The addresses to watch for transfers from const filter = { fromBlock: 0, toBlock: 'latest', topics: [ web3.utils.sha3('Transfer(address,address,uint256)'), null, senderAddressesToWatch, ] }; contract.getPastEvents('Transfer', { filter: filter, fromBlock: 0, toBlock: 'latest', }, (error, events) => { if (!error) { console.log(events); } });
上記の例は、ERC20 の承認 (および転送) イベントによって送信者がインデックス付けされるように設定されるため、機能します。以下は、Solidity での ERC20 承認イベント宣言です。
event Approval(address indexed owner, address indexed spender, uint256 value);
「owner」引数にインデックスが作成されていない場合、前述の JavaScript コードは通知なく失敗します。ここでの意味は、転送に特定のvalue
(トークン量) を持つ ERC20 イベントはインデックス付けされていないため、フィルタリングできないということです。すべてのイベントを取得し、JavaScript 側でフィルタリングする必要があります。 Ethereum クライアントでは実行できません。
イベント宣言のインデックス付き引数は、トピックと呼ばれます。
一般に受け入れられているイベントのベスト プラクティスは、重大な状態変化が発生する可能性がある場合は常にイベントをログに記録することです。例としては次のようなものがあります。
契約者の変更
動くエーテル
取引を行う
すべての状態変化にイベントが必要なわけではありません。 Solidity 開発者が自問すべき質問は、「このトランザクションをすぐに取得または発見することに興味のある人はいるだろうか?」ということです。
この遺言にはある程度の主観的な判断が必要です。インデックスのないパラメータを直接検索することはできませんが、インデックス付きパラメータがあれば有用なデータになる可能性があることに注意してください。これを直感的に理解する良い方法は、確立されたコードベースがイベントをどのように設計しているかを調べることです。
一般的な経験則として、暗号通貨の金額にはインデックスを付けるべきではなく、アドレスにはインデックスを付ける必要がありますが、このルールを盲目的に適用すべきではありません。インデックスを使用すると、値の範囲ではなく、正確な値のみを迅速に取得できます。
この例としては、基盤となるライブラリがすでにこのイベントを発行しているため、トークンが作成されるときにイベントを追加することが挙げられます。
イベントは状態を変化させます。ログを保存することでブロックチェーンの状態を変更します。したがって、ビュー (または純粋) 関数では使用できません。
イベントは、他の言語の console.log や print のようにデバッグには役に立ちません。イベント自体は状態変化するため、トランザクションが元に戻ってもイベントは発行されません。
インデックスのない引数の場合、引数の数に本質的な制限はありませんが、もちろん、適用されるコントラクト サイズとガス制限はあります。次の無意味な例は、有効な堅牢性です。
contract ExampleContract { event Numbers(uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256); }
同様に、ログに保存される文字列または配列の長さに本質的な制限はありません。
ただし、イベント内に 3 つを超えるインデックス付き引数 (トピック) を含めることはできません。匿名イベントは 4 つのインデックス付き引数を持つことができます (この区別については後で説明します)。
イベントがゼロの引数も有効です。
次のイベントは同じように動作します
event NewOwner(address newOwner); event NewOwner(address);
一般に、次の例の背後にあるセマンティクスは非常に曖昧であるため、変数名を含めることが理想的です (これはイベントを宣言する方法ではありません)。
event Trade(address,address,address,uint256,uint256);
アドレスが送信者とトークン アドレスに対応し、uint256es が金額に対応していると推測できますが、これを解読するのは困難です。
イベント名は大文字で表記するのが一般的ですが、コンパイラではその必要はありません。これはもっと良い宣言になります:
event Trade(address trader, address token1, address token2, uint256 token1Amount, uint256 token2Amount);
イベントが親コントラクトで宣言されると、子コントラクトによってイベントを発行できます。イベントは内部的なものであり、プライベートまたはパブリックに変更することはできません。
以下に例を示します。
contract ParentContract { event NewNumber(uint256 number); function doSomething(uint256 number) public { emit NewNumber(number); } } contract ChildContract is ParentContract { function doSomethingElse(uint256 number) public { emit NewNumber(number); // valid } }
同様に、次の例のように、イベントをインターフェイスで宣言し、子で使用することができます。
interface IExampleInterface { event Deposit(address indexed sender, uint256 amount); } contract ExampleContract is IExampleInterface { function deposit() external payable { emit Deposit(msg.sender, msg.value); // also valid } }
EVM (イーサリアム仮想マシン) は、署名の keccak256 を使用してイベントを識別します。
Solidity バージョン 0.8.15 以降の場合は、.selector メンバーを使用してセレクターを取得することもできます。
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 indexed timestamp); function selector() external pure returns (bool) { // true return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
イベント セレクターは実際にはそれ自体がトピックです (これについては後のセクションで詳しく説明します)。
変数にインデックス付きかどうかのマークを付けても、セレクターは変更されません。
イベントは匿名としてマークできます。その場合、イベントにはセレクターがありません。これは、クライアント側のコードが、前の例のように、それらをサブセットとして明確に分離できないことを意味します。クライアント側のコードが匿名イベントを確認する唯一の方法は、スマート コントラクトのすべてのイベントを取得することです。
pragma solidity ^0.8.15; contract ExampleContract { event SomeEvent(uint256 blocknum, uint256 timestamp) anonymous; function selector() public pure returns (bool) { // ERROR: does not compile, anonymous events don't have selectors return SomeEvent.selector == keccak256("SomeEvent(uint256,uint256)"); } }
イベント シグネチャはインデックスの 1 つとして使用され、関数シグネチャがトピックの 1 つとして「解放」されるため、匿名関数は 4 つのインデックス付きトピックを持つことができます。
匿名イベントには、最大 4 つのインデックス付きトピックを含めることができます。非匿名イベントには最大 3 つを含めることができます。
contract ExampleContract { // valid event SomeEvent(uint256 indexed, uint256 indexed, address indexed, address indexed) anonymous; }
匿名イベントが実際に使用されることはほとんどありません。
このセクションでは、EVM のアセンブリ レベルのイベントについて説明します。ブロックチェーン開発が初めてのプログラマーは、このセクションをスキップしてください。
スマート コントラクトで発生したすべてのトランザクションを取得するには、イーサリアム クライアントはすべてのブロックをスキャンする必要があり、これは非常に重い I/O 操作になります。しかし、イーサリアムは重要な最適化を使用します。
イベントは、ブロックごとにブルーム フィルターデータ構造に保存されます。ブルーム フィルターは、メンバーがセットに含まれているかどうかを効率的に回答する確率的なセットです。ブロック全体をスキャンする代わりに、クライアントはブロック内でイベントが発行されたかどうかをブルーム フィルターに問い合わせることができます。これにより、クライアントはブロックチェーンをより速くスキャンしてイベントを見つけることができます。
ブルーム フィルターは確率的です。アイテムがセットのメンバーではない場合でも、アイテムがセットのメンバーであると誤って返すことがあります。ブルーム フィルターに保存されるメンバーが増えるほど、エラーが発生する可能性が高くなり、これを補うためにブルーム フィルターを (ストレージ的に) 大きくする必要があります。このため、イーサリアムはすべてのトランザクションをブルーム フィルターに保存しません。イベントの数はトランザクションの数よりもはるかに少ないです。これにより、ブロックチェーン上のストレージ サイズが管理可能に保たれます。
クライアントがブルーム フィルターから肯定的なメンバーシップ応答を受け取ると、ブロックをスキャンしてイベントが発生したことを確認する必要があります。ただし、これはブロックのごく一部のサブセットでのみ発生するため、平均して、イーサリアム クライアントは最初にブルーム フィルターでイベントの存在をチェックすることで、多くの計算を節約します。
Yul 中間表現では、インデックス付き引数 (トピック) とインデックスなし引数の区別が明確になります。
次の yul 関数は、イベントの発行に使用できます (EVM オペコードは同じ名前です)。この表は、 yul ドキュメントから一部簡略化してコピーしたものです。
オペコード | 使用法 |
---|---|
log0(p,s) | トピックとデータメモリを含まないログ[p…(p+s)) |
log1(p, s, t1) | トピック t1 とデータ mem[p…(p+s)) を含むログ |
log2(p, s, t1, t2) | トピック t1、t2 およびデータ mem[p…(p+s)) を含むログ |
log3(p、s、t1、t2、t3) | トピック t1、t2、t3 およびデータ mem[p…(p+s)) を含むログ |
log4(p、s、t1、t2、t3、t4) | トピック t1、t2、t3、t4 およびデータ mem[p…(p+s)) を含むログ |
ログには最大 4 つのトピックを含めることができますが、非匿名の Solidity イベントには最大 3 つのインデックス付き引数を含めることができます。これは、最初のトピックがイベント署名の保存に使用されるためです。 4 つを超えるトピックを発行するためのオペコードや Yul 関数はありません。
インデックスのないパラメータは、メモリ領域 [p…(p+s)) で単に abi エンコードされ、1 つの長いバイト シーケンスとして出力されます。
前述したように、Solidity のイベントが持つことができるインデックスのない引数の数には原則として制限がありませんでした。根本的な理由は、log 演算コードの最初の 2 つのパラメーターで示されるメモリ領域にかかる時間に明示的な制限がないことです。もちろん、契約サイズとメモリ拡張ガスのコストによる制限があります。
イベントは、ストレージ変数に書き込むよりも大幅にコストがかかります。イベントはスマート コントラクトによってアクセスできるように意図されていないため、オーバーヘッドが相対的に少ないため、ガス コストの削減が正当化されます。
イベントにかかるガスの量の計算式は次のとおりです (ソース):
375 + 375 * num_topics + 8 * data_size + mem_expansion cost
各イベントには少なくとも 375 ガスがかかります。インデックス付きパラメータごとに追加の 375 が支払われます。非匿名イベントにはインデックス付きパラメーターとしてイベント セレクターがあるため、ほとんどの場合、そのコストが含まれます。次に、チェーンに書き込まれる 32 バイトのワード数の 8 倍を支払います。この領域は放出される前にメモリに格納されるため、メモリ拡張コストも考慮する必要があります。
一般に、ログを記録すればするほどガソリン代の支払いが増えるという直感は正確です。
イベントは、クライアントが関心のある可能性のあるトランザクションを迅速に取得するためのものです。スマート コントラクトの機能は変更されませんが、プログラマはどのトランザクションを迅速に取得できるようにするかを指定できます。これにより、dapp が重要な情報を迅速に要約することが容易になります。
イベントはストレージに比べてガス的には比較的安価ですが、コーダーが法外な量のメモリを使用しないと仮定すると、イベントのコストで最も重要な要素はインデックス付きパラメーターの数です。
ここにあるものは好きですか?詳細については、高度なSolidity ブートキャンプをご覧ください。
この記事のリード画像は、HackerNoon の AI Image Generator によって「Events in Solidity」というプロンプトを介して生成されました。