Meta Transaction Demo

# はじめに（Introduction）

前回、[ERC-2771: Secure Protocol for Native Meta Transactions](https://qiita.com/tnakagawa/items/82438304625fdd9da742)を翻訳したのですが、実装に関しては詳しく書かれていなかったので [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts)を利用して実装してみようと思います。

# バージョン（Version）

主なバージョンです。

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

# コントラクト（Contract）

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

## Solidity

受信者（Recipient）は転送者（Forwarder）からの要求を受け付ける為、[ERC2771Context.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol)を継承しています。
**重要なポイントは転送者（Forwarder）のコントラクトアドレス（`address trustedForwarder`）を設定しているところです。**
したがって、転送者（Forwarder）コントラクト先にデプロイしておく必要があります。

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

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

```solidity: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](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Forwarder.sol)を継承しているだけの単純なものとなっています。

```solidity: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をコンパイルします。
以下のコマンドで実行します。

```sh
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](https://github.com/tnakagawa/metatxdemo)に置いてあります。
重要な箇所だけ解説します。

## インポート（Import）

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

```javascript
"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`でプロバイダーを作成します。
接続確認の為、ブロック数を取得しています。

```javascript
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）

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

```javascript
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`コントラクトからデプロイします。

```javascript
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](https://eips.ethereum.org/EIPS/eip-712)の署名が必要なので、ドメインを取得します。
ドメインは`chainId`、`name`、`verifyingContract`、`version`が必要になります。

ForwardRequestは以下のとおりです。
- `from`: 代理のアドレス、 リクエストの署名者と同じである必要があります。
- `to`: Forwarderが呼び出すアドレスです。（ここでは、Recipientコントラクトのアドレス）
- `value`: リクエストに添付するネイティブトークンの量です。（ここでは、0です。）
- `gas`: リクエストで転送されるガス制限の量です。（ここでは、5000としています。）
- `nonce`: 再実行可能性を回避するため、無効化を要求するための一意のトランザクション順序識別子です。（Forwarderコントラクトから取得します。）
- `deadline`: このタイムスタンプを過ぎるとリクエストは実行できなくなります。（ここでは、現在時刻から1時間までとしてます。）
- `data`: リクエストで送信するエンコードされた `msg.data`です。（ここでは、ERC20の`transfer`のデータです。）

```javascript
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`）しています。

```javascript
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のノードを起動します。

```sh
npx hardhat node
```

[Meta Transaction Demo](https://tnakagawa.github.io/metatxdemo/frontend/index.html)ページを開きます。

## デモ実施

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

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

## 結果（Result）

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29914/74267bfc-8594-f58c-398a-220669223dee.png)

# まとめ（Conclusion）

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

※※※追記（2023/11/19）※※※

[デモ画面](https://tnakagawa.github.io/metatxdemo/frontend/index.html)ではURLにAlchemyなどから発行されるHTTPSのAPIのURLなどを入れればテストネットでもデモ可能でした。
PolygonのテストネットMumbaiとEthereumのテストネットSepoliaで確認済みです。