はじめに
初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
代表的なゲームはクリプトスペルズというブロックチェーンゲームです。
今回は、ガス代を他のアドレスに支払ってもらいコントラクトの処理を実行するメタトランザクションについて提案しているERC2771についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
概要
このEIPは、新しい方法でEthereumのコントラクト間で通信する仕組みを提案しています。
具体的には、メタトランザクションと呼ばれるもので、コントラクトが別のコントラクトに対して操作を実行できるようになります。
これまでの通信方法では、呼び出し元のアカウント(EOA)がトランザクションのガス費用を支払う必要がありました。
EOAアカウントとは、Metamaskなどのウォレットのことです。
しかし、この提案では、ガス費用を支払えない外部所有アカウントからの操作も可能になります。
これを実現するために、信頼できるForwarderコントラクトが介在します。
具体的には、外部所有アカウントが操作を行いたいとき、Forwarderコントラクトを通じてメタトランザクションを送信します。
Forwarderコントラクトは、ガス費用を支払えるためのETHを持っているため、操作を実行するためのガスを提供します。
そして、その操作が実際に行われるコントラクト(Recipientコントラクト)に対して、特別な情報を付加した形で通知を行います。
この特別な情報には、操作を行ったアカウント(msg.sender
)や操作の内容(msg.data
)が含まれます。
これによって、Recipientコントラクトは正しく操作を実行することができます。
重要なのは、既存のプロトコルを大きく変更せずに、新しい仕組みを追加するところです。
これによって、ガス費用を持たないアカウントでもコントラクトの機能を利用できるようになります。
メタトランザクションについては以下でも説明しています。
動機
Ethereumのコントラクトが、ガスの支払いに必要なETHを持たない外部所有アカウントからの呼び出しを受け入れる仕組みへの関心が高まっています。
ガスコストを支払うのに代わりに第三者が支払うことを可能にする解決策は、メタトランザクションと呼ばれます。
このEIPの観点では、メタトランザクションとは、トランザクション署名者によって承認され、ガス代を支払うための信頼できない第三者によって中継されたトランザクションを指します(ガスリレーとも呼ばれます)。
このプロセスにより、ETHを持っていないユーザーもコントラクトの機能を利用できるようになります。
以下のようにユーザーはガス代を考慮せずに処理の実行を仲介者にお願いできます。
仕様
用語の定義
トランザクション署名者(Transaction Signer)
トランザクション署名者は、トランザクションに署名を行い、その署名済みトランザクションをガスリレーに送信する役割を果たします。
ガスリレー(Gas Relay)
ガスリレーは、トランザクション署名者からオフチェーンで署名済みのリクエストを受け取り、ガス代を支払って有効なトランザクションに変換し、信頼できるForwarderを介して処理されるようにします。
信頼されたForwarder(Trusted Forwarder)
信頼されたForwarderは、Recipientによって信頼されており、トランザクション署名とnonce(トランザクションの一意の識別子)を正しく検証し、トランザクション署名者からのリクエストをRecipientに送付する前に検証するコントラクトです。
「信頼された」とは、以下のゲームの例えで言えばゲーム運営という存在を信頼して署名を預けたことを指します。
Forwarderがトランザクションの起点となります。
そして、渡された署名付きのトランザクションの実行者(meg.sender
)をユーザーに設定することができます。
ただ、あくまでトランザクションを起こすのはForwarderであるということを注意してください。
受信者(Recipient)
受信者(Recipient)は、信頼されたForwarderを介してメタトランザクションを受け入れるコントラクトです。
これによって、ガスリレーを経由して送信されたトランザクションが受信者(Recipient)のコントラクトで実行されます。
このプロセスは、トランザクション署名者が署名したリクエストをガスリレーが受け取り、ガス代を支払ってトランザクションに変換し、信頼されたForwarderを介して受信者(Recipient)に送付されることで、メタトランザクションを実現します。
受信者(Recipient)は信頼されたForwarder通じてメタトランザクションを受け入れることで、ガス代を支払えないユーザーでもコントラクトの機能を利用できるようになります。
以下のゲームで例えると、ユーザーがトランザクション署名者になります。
そして、ゲームを通じて裏側で署名を行い運営に渡している署名情報がガスリレーの説明部分になります。
署名情報を受け取った運営(この場合はEOAアカウントではなく、コントラクトアカウントになります)が、信頼されたForwarderとなり、署名を検証してガス代を負担して処理を実行してくれます。
最後に運営が受信者(Recipient) となり、コントラクトを呼び出して具体的な処理を実行してくれます。
上記の説明の場合、信頼されたForwarderと**受信者(Recipient)**が同じコントラクトに見えますが、実際は運営が2つのコントラクトを所有していると認識してもらえるとわかりやすいです。
サンプルフロー
引用: https://eips.ethereum.org/EIPS/eip-2771
トランザクション署名者のアドレス抽出
信頼されたForwarderによるアドレス追加
信頼されたForwarderは、受信者(Recipient)コントラクトを呼び出す責任を持ち、呼び出しデータの末尾にトランザクション署名者のアドレス(20バイトのデータ)を追加する必要があります。
(bool success, bytes memory returnData) = to.call.value(value)(abi.encodePacked(data, from));
トランザクション署名者アドレス抽出
受信者(Recipient)コントラクト、次の3つの操作を実行することでトランザクション署名者のアドレスを抽出できます。
- 信頼されたForwarderかどうかをチェックします。
- これがどのように実装されるかは、提案の範囲外です。
- 呼び出しデータの最後の
20
バイトからトランザクション署名者のアドレスを抽出し、それをトランザクションの元の送信者として使用します(msg.sender
の代わりに)。 -
msg.sender
が信頼されたForwarderでない場合(またはmsg.data
が20
バイト未満の場合)、元のmsg.sender
をそのまま返します。
信頼されたForwarderの確認
受信者(Recipient)は、アドレスデータが信頼されていないコントラクトから追加されないように、信頼されたForwarderをチェックする必要があります。
これによって、偽造されたアドレスが生成されるリスクを回避します。
プロトコルサポートの発見メカニズム
特定のフロントエンドで受信者(Recipient)コントラクトがネイティブメタトランザクションをサポートしていることを知っている場合を除いて、ユーザーにメタトランザクションを選択してコントラクトとやり取りする選択肢を提供することはできません。
したがって、受信者(Recipient)がメタトランザクションをサポートしていることを世界に伝えるためのメカニズムが必要です。
特に、Web3ウォレットレベルでメタトランザクションをサポートする場合、ウォレットはユーザーが対話したい受信者(Recipient)コントラクトについて必ずしも知る必要がありません。
異なるインターフェースと機能(トランザクションバッチング、異なるメッセージ署名フォーマットなど)を持つ信頼されたForwarderが存在する可能性があるため、ウォレットが信頼されたForwarderを発見できるようにする必要があります。
このために、受信者(Recipient)コントラクトは以下の関数を実装する必要があります。
function isTrustedForwarder(address forwarder) external view returns(bool);
isTrustedForwarder
関数は、Forwarderが受信者(Recipient)によって信頼されている場合にtrue
を返し、それ以外の場合はfalse
を返す必要があります。
isTrustedForwarder
関数はrevert
してはなりません。
内部的には、受信者(Recipient)はForwarderからのリクエストを受け入れる必要があります。
isTrustedForwarder
関数はオンチェーンで呼び出される可能性があり、そのためガス制限が必要です。
50,000
ガスを超えることは避けるべきです。
背景としての論理を詳しく説明します。
補足
この提案の目的は、最も簡単なコントラクトインターフェースを標準化することによって、コントラクト開発者がメタトランザクションのサポートを簡単に追加できるようにすることです。
受信者(Recipient)コントラクトがメタトランザクションをサポートしていない場合、外部所有アカウントは受信者(Recipient)コントラクトとメタトランザクションを使用して対話することはできません。
標準契約インターフェースの重要性
標準のコントラクトインターフェースがない場合、クライアントが受信者(Recipient)がメタトランザクションをサポートしているかどうかを判別する標準的な方法は存在しません。
また、標準のコントラクトインターフェースがない場合、受信者(Recipient)にメタトランザクションを送信する標準的な方法も存在しません。
信頼されたForwarderの利用
信頼されたForwarderを利用できない場合、各受信者(Recipient)コントラクトはメタトランザクションを安全に受け入れるために必要なロジックを内部的に実装しなければなりません。
発見プロトコルの重要性
発見プロトコルがない場合、クライアントが特定のフォワーダーをサポートする受信者(Recipient)を発見するメカニズムがありません。
コントラクトインターフェースの標準化の利点
コントラクトインターフェースを信頼されたForwarderの内部実装の詳細から切り離すことで、受信者(Recipient)コントラクトがコードの変更なしに複数のForwarderをサポートすることが可能になります。
msg.senderの問題とセキュリティ
msg.sender
は、トランザクションに署名したユーザーを特定するためにコントラクトによって検査できるトランザクションのパラメータです。
しかし、メタトランザクションを保護するには、msg.sender
のセキュリティだけでは不十分です。
問題は、メタトランザクションに対応していないコントラクトの場合、トランザクションのmsg.sender
がガスリレーからのものに見え、トランザクション署名者からのものには見えないということです。
メタトランザクションを安全に受け入れるためのセキュアなプロトコルは、ガスリレーがトランザクション署名者によるリクエストを偽造、変更、複製することを防止する必要があります。
参考実装
contract RecipientExample {
function purchaseItem(uint256 itemId) external {
address sender = _msgSender();
// ... perform the purchase for sender
}
address immutable _trustedForwarder;
constructor(address trustedForwarder) internal {
_trustedForwarder = trustedForwarder;
}
function isTrustedForwarder(address forwarder) public returns(bool) {
return forwarder == _trustedForwarder;
}
function _msgSender() internal view returns (address payable signer) {
signer = msg.sender;
if (msg.data.length>=20 && isTrustedForwarder(signer)) {
assembly {
signer := shr(96,calldataload(sub(calldatasize(),20)))
}
}
}
}
このコントラクトは、メタトランザクションをサポートするための受信者(Recipient)コントラクトの例です。
受信者(Recipient)コントラクトは、外部所有アカウントがメタトランザクションを使用して商品の購入を行うことを可能にします。
purchaseItem
外部から呼び出される関数で、指定された商品IDに基づいて商品の購入処理を行います。
_trustedForwarder
信頼されたForwarderのアドレスを保持する不変の変数。
constructor
コントラクトのデプロイ時に信頼されたForwarderのアドレスを受け取り、_trustedForwarder
変数に設定します。
isTrustedForwarder
渡されたアドレスが信頼されたForwarderかどうかをチェックする関数。
_msgSender
メタトランザクションからの呼び出しの場合、正しいmsg.sender
アドレスを返す関数。
信頼されたForwarderのアドレスが検出された場合、アセンブリコードを使用して正しい署名者アドレスを取得します。
セキュリティ上の考慮事項
悪意のあるForwarderは、_msgSender()
の値を偽装して、実際には任意のアドレスからトランザクションを送信することができます。
そのため、受信者(Recipient)コントラクトはForwarderを信頼する際に非常に注意が必要です。
もしForwarderがアップグレード可能である場合、そのコントラクトが悪意のあるアップグレードを行わないことも信頼する必要があります。
さらに、信頼されるForwarderを変更することを制限する必要があります。
攻撃者が自分のアドレスを「信頼」することで、トランザクションを偽造できる可能性があるためです。
信頼されるForwarderのリストを不変(変更不可能)にすることをお勧めします。
これが実現不可能な場合は、信頼されたコントラクト所有者のみがそれを変更できるようにすることが望ましいです。
要するに、信頼されたForwarderを選ぶ際には慎重さが必要であり、不正な操作や偽造を防ぐために信頼性の確認と制限を行うことが重要です。
引用
Ronan Sandford (@wighawag), Liraz Siri (@lirazsiri), Dror Tirosh (@drortirosh), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Hadrien Croubois (@Amxx), Sachin Tomar (@tomarsachin2271), Patrick McCorry (@stonecoldpat), Nicolas Venturo (@nventuro), Fabian Vogelsteller (@frozeman), Gavin John (@Pandapip1), "ERC-2771: Secure Protocol for Native Meta Transactions," Ethereum Improvement Proposals, no. 2771, July 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-2771.
最後に
今回は「ガス代を他のアドレスに支払ってもらいコントラクトの処理を実行するメタトランザクションについて提案しているERC2771」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
採用強化中!
CryptoGamesでは一緒に働く仲間を大募集中です。
この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!