2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Meta Transaction Demo

Last updated at Posted at 2023-11-18

はじめに(Introduction)

前回、ERC-2771: Secure Protocol for Native Meta Transactionsを翻訳したのですが、実装に関しては詳しく書かれていなかったので OpenZeppelinを利用して実装してみようと思います。

バージョン(Version)

主なバージョンです。

  • Node.js 20.9.0
  • hardhat@2.19.0
  • @openzeppelin/contracts@5.0.0

コントラクト(Contract)

ERC-2771でコントラクトは受信者(Recipient)と転送者(Forwarder)の2つ必要となります。

Solidity

受信者(Recipient)は転送者(Forwarder)からの要求を受け付ける為、ERC2771Context.solを継承しています。
重要なポイントは転送者(Forwarder)のコントラクトアドレス(address trustedForwarder)を設定しているところです。
したがって、転送者(Forwarder)コントラクト先にデプロイしておく必要があります。

メインの機能についてはなんでもいいので、今回はERC20を選択しています。

_msgSender()_msgDataについては、ERC2771ContextContextに実装されているので、ERC2771Contextを優先するようにoverrideします。

SampleRecipient.sol
// SPDX-License-Identifier: MiT
pragma solidity <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";

contract SampleRecipient is ERC20, Ownable, ERC2771Context {
    constructor(
        string memory name,
        string memory symbol,
        address trustedForwarder
    )
        ERC20(name, symbol)
        Ownable(msg.sender)
        ERC2771Context(trustedForwarder)
    {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    function _msgSender()
        internal
        view
        override(Context, ERC2771Context)
        returns (address)
    {
        return super._msgSender();
    }

    function _msgData()
        internal
        view
        override(Context, ERC2771Context)
        returns (bytes calldata)
    {
        return super._msgData();
    }
}

転送者(Forwarder)はERC2771Forwarderを継承しているだけの単純なものとなっています。

SampleForwarder.sol
// SPDX-License-Identifier: MiT
pragma solidity <0.9.0;

import "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";

contract SampleForwarder is ERC2771Forwarder {
    constructor(string memory name) ERC2771Forwarder(name) {}
}

コンパイル(Compile)

Solidityをコンパイルします。
以下のコマンドで実行します。

npx hardhat compile

成功するとartifactsフォルダが生成されます。

artifacts/contracts/SampleForwarder.solフォルダとartifacts/contracts/SampleRecipient.solフォルダ内にあるSampleForwarder.jsonファイルとSampleRecipient.jsonファイルを使います。

artifacts
├─@openzeppelin
├─build-info
└─contracts
    ├─SampleForwarder.sol
    │      SampleForwarder.dbg.json
    │      SampleForwarder.json
    │
    └─SampleRecipient.sol
            SampleRecipient.dbg.json
            SampleRecipient.json

実装(Implementation)

ソースコードは、Githubに置いてあります。
重要な箇所だけ解説します。

インポート(Import)

ES Modulesで実装します。
インポートは以下のとおりとなります。
ethersは6系を使用します。
また、コンパイルで作成されたSampleForwarder.jsonファイルとSampleRecipient.jsonファイルをjsonとしてインポートします。

"use strict";

import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
import SampleRecipientJson from "./SampleRecipient.json" assert {type: "json"};
import SampleForwarderJson from "./SampleForwarder.json" assert {type: "json"};

プロバイダー取得(Provider)

JsonRpcProviderでプロバイダーを作成します。
接続確認の為、ブロック数を取得しています。

var provider = null;

async function connectProvider(providerUrl) {
    if (provider) {
        try {
            provider.destroy();
        } catch (e) {
            console.log("provider.destroy err", e);
        }
    }
    provider = null;
    try {
        provider = new ethers.JsonRpcProvider(providerUrl);
        let blockNumber = await provider.getBlockNumber();
        console.log("getBlockNumber", blockNumber);
    } catch (err) {
        console.log("connectProvider err", err);
        if (provider) {
            try {
                provider.destroy();
            } catch (e) { }
        }
        provider = null;
        throw err;
    }
}

署名者(Signers)

署名者を作成します。
サンプル実装なのでmnemoniclocalStorageに保存していますが、おすすめできません。

var signers = [];

/**
 * ※:サンプル実装なので真似しないでください。
 */
function setSiners(provider, password) {
    signers.length = 0;
    let mnemonic = localStorage.getItem("mnemonic");
    if (!mnemonic) {
        mnemonic = ethers.Wallet.createRandom().mnemonic.phrase;
        // mnemonicをローカルストレージに入れるのはセキュリティ的にNGです。
        localStorage.setItem("mnemonic", mnemonic);
    }
    let parent = ethers.HDNodeWallet.fromPhrase(mnemonic, password, "m/44'/60'/0'/0");
    for (let i = 0; i < 4; i++) {
        let wallet = parent.deriveChild(i);
        let signer = wallet.connect(provider);
        signers.push(signer);
    }
}

デプロイ(Deploy)

コントラクトをデプロイします。
RecipientコントラクトにはForwarderコントラクトのアドレスが必要なので、Forwarderコントラクトからデプロイします。

async function deploy(signer) {
    let sampleForwarderFactory = new ethers.ContractFactory(SampleForwarderJson.abi, SampleForwarderJson.bytecode, signer);
    let sampleForwarderContract = await sampleForwarderFactory.deploy("SampleForwarder");
    await sampleForwarderContract.waitForDeployment();
    let sampleRecipientFactory = new ethers.ContractFactory(SampleRecipientJson.abi, SampleRecipientJson.bytecode, signer);
    let sampleRecipientContract = await sampleRecipientFactory.deploy("SampleRecipient", "SRC", sampleForwarderContract.target);
    await sampleRecipientContract.waitForDeployment();
    return [sampleForwarderContract.target, sampleRecipientContract.target];
}

リクエスト生成(Request of Meta Transaction)

ERC20のtransferを呼び出すリクエストを作成します。
EIP-712の署名が必要なので、ドメインを取得します。
ドメインはchainIdnameverifyingContractversionが必要になります。

ForwardRequestは以下のとおりです。

  • from: 代理のアドレス、 リクエストの署名者と同じである必要があります。
  • to: Forwarderが呼び出すアドレスです。(ここでは、Recipientコントラクトのアドレス)
  • value: リクエストに添付するネイティブトークンの量です。(ここでは、0です。)
  • gas: リクエストで転送されるガス制限の量です。(ここでは、5000としています。)
  • nonce: 再実行可能性を回避するため、無効化を要求するための一意のトランザクション順序識別子です。(Forwarderコントラクトから取得します。)
  • deadline: このタイムスタンプを過ぎるとリクエストは実行できなくなります。(ここでは、現在時刻から1時間までとしてます。)
  • data: リクエストで送信するエンコードされた msg.dataです。(ここでは、ERC20のtransferのデータです。)
async function signMetaTransaction(forwarderAddress, recipientAddress, signer, toAddress, amount) {
    let sampleForwarder = new ethers.Contract(forwarderAddress, SampleForwarderJson.abi, signer);
    let sampleRecipient = new ethers.Contract(recipientAddress, SampleRecipientJson.abi, signer);
    let eip712domain = await sampleForwarder.eip712Domain();
    let domain = {
        chainId: eip712domain.chainId,
        name: eip712domain.name,
        verifyingContract: eip712domain.verifyingContract,
        version: eip712domain.version,
    };
    let types = {
        ForwardRequest: [
            { type: "address", name: "from" },
            { type: "address", name: "to" },
            { type: "uint256", name: "value" },
            { type: "uint256", name: "gas" },
            { type: "uint256", name: "nonce" },
            { type: "uint48", name: "deadline" },
            { type: "bytes", name: "data" },
        ],
    };
    // ERC20 transfer
    let iface = new ethers.Interface(SampleRecipientJson.abi);
    let data = iface.encodeFunctionData("transfer", [toAddress, amount]);
    let value = {
        from: signer.address,
        to: sampleRecipient.target,
        value: 0n,
        gas: 50000n,
        nonce: await sampleForwarder.nonces(signer.address),
        deadline: (Math.floor(Date.now() / 1000) + 3600),
        data: data,
    };
    let sign = await signer.signTypedData(domain, types, value);
    let request = {
        from: value.from,
        to: value.to,
        value: value.value,
        gas: value.gas,
        deadline: value.deadline,
        data: value.data,
        signature: sign,
    };
    return request;
}

送信(Send Meta Transaction)

Gas代を肩代わりする転送者(Relayer)がリクエストを送信します。
送信する前に検証(verify)しています。

async function sendMetaTransaction(forwarderAddress, signer, request) {
    let sampleForwarder = new ethers.Contract(forwarderAddress, SampleForwarderJson.abi, signer);
    let result = await sampleForwarder.verify(request);
    if (result) {
        let tx = await sampleForwarder.execute(request);
        await tx.wait(1);
    } else {
        throw new Error("Failed to verify");
    }
}

デモ(Demo)

実際に動かしてみます。

準備

Hardhatのノードを起動します。

npx hardhat node

Meta Transaction Demoページを開きます。

デモ実施

デモの実施手順は以下となります。

  1. 「Connect」ボタンを押下して、ノードに接続します。
  2.  RelayerにCoinが無い場合は「Faucet」ボタンを押下してRelayerにCoinを付与します。
  3. 「Deploy」ボタンを押下してコントラクトをデプロイします。
  4. 「Mint Token」ボタンを押下して各Signerにトークンを付与します。
  5. 「Sign Meta Transaction」ボタンを押下してリクエストを生成します。
  6. 「Send Meta Transaction」ボタンを押下してRelayerが送信します。

結果(Result)

RelayerがGas代を肩代わりしているので、RelayerのCoinが減っていますが、ERC20のTokenはUser1からUser2へ移転していることがわかると思います。

image.png

まとめ(Conclusion)

OpenZeppelinを利用すると簡単にデモが作成できました。
基本的に一般ユーザーは暗号資産を持っていないので、Gas代を肩代わりしてくれるのは便利だと思います。
ただ、コントラクト側に「ERC2771Context」を含めておく必要があるので設計をあるていど考えておく必要がありそうです。

※※※追記(2023/11/19)※※※

デモ画面ではURLにAlchemyなどから発行されるHTTPSのAPIのURLなどを入れればテストネットでもデモ可能でした。
PolygonのテストネットMumbaiとEthereumのテストネットSepoliaで確認済みです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?