OpenZeppelin, Defenderを使いMeta transactionを実装
OpenZeppelinのDefenderというサービスを使いMeta transactionを実装する方法を説明します。
OpenZeppelinの公式チュートリアルで実装方法が解説されています。
動画の説明もあり、かなり丁寧です。
今回こちらのGitHub repoをPolygon testnetで動かしてみたので、中身の解説をしたいと思います。もとのrepoはxDaiにdeployするものだったので、Polygon testnetにdeployするように修正したものもアップしました。テストネットで動かす場合、参考にしてください。
Meta transactionとは
Meta txとは、ユーザーがガス代を支払わずとも、トランザクションを実行できる仕組みです。ユーザーは実行するトランザクションに署名し、オフチェーンで署名をオペレータに送ります。オペレータは送られた署名を使ってトランザクションをオンチェーンで実行し、ガス代はオペレータによって肩代わりされます。
以下が概要図です:参考。ユーザー(Client)は、オフチェーンで、オペレータが運用するrelayerサーバーに署名されたリクエストを送ります。relayerはこのデータをForwarderコントラクトに送信し、署名が正しければ、リクエストはRecipientコントラクトにForward(転送)され、ユーザーの送ったリクエストがオンチェーンで実行されます。このときガスを払うのは、オペレータになるので、ユーザーはガスを支払うことなくリクエストを実行できます。
それでは、OpenZeppelinのチュートリアル用のサンプルコントラクトRegistry.solを見てみます。このRegistry.solが上の図のRecipientコントラクトに相当します。Meta txでは、register()をForwarderからcallして実行することで、ユーザーのregisterリクエストを間接的に実行することができます。register内部のaddress owner = _msgSender(); // Changed from msg.sender
という行がポイントで、この_msgSender()は、ERC2771Contextで定義された特殊な_msgSender()です。この_msgSender()は、register()がForwarderから実行された場合は、Forwarderのアドレスではなく、Forwarderにリクエストを投げたユーザーのアドレスを返します。つまり、この処理のおかけでForwarderがregister()を実行しているにも関わらず、register()内部ではユーザーがtxをsendしたとみなして処理を実行できます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
import "@openzeppelin/contracts/metatx/MinimalForwarder.sol";
contract Registry is ERC2771Context {
event Registered(address indexed who, string name);
mapping(address => string) public names;
mapping(string => address) public owners;
constructor(MinimalForwarder forwarder) // Initialize trusted forwarder
ERC2771Context(address(forwarder)) {
}
function register(string memory name) external {
require(owners[name] == address(0), "Name taken");
address owner = _msgSender(); // Changed from msg.sender
owners[name] = owner;
names[owner] = name;
emit Registered(owner, name);
}
}
OpenZeppelin, DefenderでMeta txを試してみる
それでは、実際にdeployして挙動を確認してみます。こちらのGitHubレポにPolygon testnetデプロイ用に修正したコードをおいておきました。
まずはdeployコマンドで、MinimalForwarder, RegistryコントラクトをPolygon testnetにデプロイしてください。デプロイにあたって、.envにウォレットの秘密鍵と、InfuraのAPI keyを書いてください。
yarn deploy
deployが終わったら、Relayerサーバーを立ち上げます。OpenZeppelinがDefenderというサービスを提供しており、簡単な操作でRelayerサーバーを構築できます。
https://defender.openzeppelin.com にSignUpして、Create a new Relayerしてください。NetworkはMumbaiを選択してください。CreateするとRelayerアドレスが生成されるので、こちらにいくらかのMATICを送付してください。このアドレスからMeta txのガスが支払われます。
RelayerができたらAutotaskを作り、ユーザーがオフチェーントランサクションを送信するためのエンドポイントを作成します。Create Autotaskを選び、Connect to a relayer (optional)の欄には、先程作成したrelayerを選択して、createします。Autotaskが作成されると、Webhook URIが作られます。このURIに対しclientからトランザクションをHTTP通信でPostすることで、Meta txを実行できるようになります。
ここまでが完了したら.envを更新して、以下の環境変数を設定してください。AUTOTASK_IDはURLに入っているidです(https://defender.openzeppelin.com/#/autotask/{autotask_id})。TEAMのKEYは右側の設定画面で確認できます。TEAM KEYはAutotaskにrelay用のスクリプトをアップロードするために使います。
RELAYER_API_KEY="Defender Relayer API key, used for sending txs with yarn relay"
RELAYER_API_SECRET="Defender Relayer API secret"
AUTOTASK_ID="Defender Autotask ID to update when running yarn upload"
TEAM_API_KEY="Defender Team API key, used for uploading autotask code"
TEAM_API_SECRET="Defender Team API secret"
次にAutotaskがオフチェーントランザクションを処理するためのスクリプトをアップロードします。autotasks/relay/index.js を、以下のコマンドでAutotaskにアップロードします。
yarn upload
アップロードしたindex.jsのhandler(event)の部分で、AutotaskのWebhook URIにclientからtxがpostされたときに、txをForwarderコントラクトにrelayして送信するようにしています。
dappの実行
appフォルダに移ってyarn
します。以下の.envを設定してください。
REACT_APP_WEBHOOK_URL=https://api.defender.openzeppelin.com/autotasks/*****
REACT_APP_QUICKNODE_URL=https://polygon-mumbai.infura.io/v3/{infura_api}
yarn start
でdappを起動します。起動すると名前を入力してRegisterボタンを押せるようになっています。MetamaskでMumbaiに接続してから、ボタンを押して署名してください。ガスレスであることを確認したいので、Maticを持っていないアドレスに切り替えてからやってください。署名してしばらくすると、Transaction sentと表示され画面中央に実行したユーザーのアドレスと登録した名前が表示されます。Maticを持っていないアカウントからでもtxが実行できることが確認できたと思います。
dappの処理
app/src/eth/register.js に、registerボタンを押したときの処理が書いてあります。
Forwarder, Registryコントラクト、register()メソッドを実行するためのcalldataを指定して署名し、requestデータを作ります。これをWebhoook URIにpostすると、Forwarderにrequestが送信され、間接的にregister()が実行されます。
async function sendMetaTx(registry, provider, signer, name) {
console.log(`Sending register meta-tx to set name=${name}`);
const url = process.env.REACT_APP_WEBHOOK_URL;
if (!url) throw new Error(`Missing relayer url`);
const forwarder = createInstance(provider);
const from = await signer.getAddress();
const data = registry.interface.encodeFunctionData('register', [name]);
const to = registry.address;
const request = await signMetaTxRequest(signer.provider, forwarder, { to, from, data });
return fetch(url, {
method: 'POST',
body: JSON.stringify(request),
headers: { 'Content-Type': 'application/json' },
});
}
まとめ
OpenZeppelin, Defenderを使うと簡単にMeta txが実現できる。
Discussion