いまさら「ERC-3009: Transfer With Authorization」

# はじめに（Introduction）

[USDC](https://www.usdc.com/)、[JPYC](https://jpyc.jp/)などが採用している、ERC3009を実装してみます。

# ERC3009

モチベーションとしては、「ガスの支払いを他のユーザーに委任」できることです。
ネイティブトークンをもたないユーザーがERC20（USDC、JPYCなど）を移転したい場合に使用されます。

https://eips.ethereum.org/EIPS/eip-3009

# 実装（Implementation）

[OpenZeppelinのERC20](https://docs.openzeppelin.com/contracts/5.x/api/token/erc20)に公式の[Implementation](https://eips.ethereum.org/EIPS/eip-3009#implementation)を適用します。
**<font color="red">※：一部不具合やOpenZeppelinと合致しない部分を修正します。</font>**

`EIP712.sol` は EIP712のドメインセパレータの作成とリカバーを行うライブラリです。
公式のコードには不具合があるので注意してください。
`bytes32(chainId)`、`address(this)` の順が正しいです。

```javascript:EIP712.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

library EIP712 {
    // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
    bytes32 public constant EIP712_DOMAIN_TYPEHASH =
        0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f;

    function makeDomainSeparator(
        string memory name,
        string memory version
    ) internal view returns (bytes32) {
        uint256 chainId;
        assembly {
            chainId := chainid()
        }

        return
            keccak256(
                abi.encode(
                    EIP712_DOMAIN_TYPEHASH,
                    keccak256(bytes(name)),
                    keccak256(bytes(version)),
                    // address(this),
                    // bytes32(chainId)
                    bytes32(chainId),
                    address(this)
                )
            );
    }

    function recover(
        bytes32 domainSeparator,
        uint8 v,
        bytes32 r,
        bytes32 s,
        bytes memory typeHashAndData
    ) internal pure returns (address) {
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                domainSeparator,
                keccak256(typeHashAndData)
            )
        );
        address recovered = ecrecover(digest, v, r, s);
        require(recovered != address(0), "EIP712: invalid signature");
        return recovered;
    }
}
```

`EIP712Domain.sol` はEIP12のドメインセパレータを格納する変数（`DOMAIN_SEPARATOR`）を実装しています。

```javascript:EIP712Domain.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

abstract contract EIP712Domain {
    bytes32 public DOMAIN_SEPARATOR;
}
```

`IERC20Transfer.sol` は　ERC20の `_transfer` を上書きするためのインターフェースです。
OpenzeppelinのERC20が `_transfer` を実装していえて上書き禁止のため `__transfer` に変更しています。

```javascript:IERC20Transfer.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

abstract contract IERC20Transfer {
    // function _transfer(
    function __transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal virtual;
}
```

`EIP3009.sol` はEIP3009の実装です。
インターフェースを変更しているので、`_transfer(from, to, value);` を `__transfer(from, to, value);` に変更しています。

```javascript:EIP3009.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "./IERC20Transfer.sol";
import "./EIP712Domain.sol";
import "./EIP712.sol";

abstract contract EIP3009 is IERC20Transfer, EIP712Domain {
    // keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
        0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;

    // keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
    bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
        0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;

    mapping(address => mapping(bytes32 => bool)) internal _authorizationStates;

    event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);

    string internal constant _INVALID_SIGNATURE_ERROR =
        "EIP3009: invalid signature";

    function authorizationState(
        address authorizer,
        bytes32 nonce
    ) external view returns (bool) {
        return _authorizationStates[authorizer][nonce];
    }

    function transferWithAuthorization(
        address from,
        address to,
        uint256 value,
        uint256 validAfter,
        uint256 validBefore,
        bytes32 nonce,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        // require(now > validAfter, "EIP3009: authorization is not yet valid");
        require(
            block.timestamp > validAfter,
            "EIP3009: authorization is not yet valid"
        );
        // require(now < validBefore, "EIP3009: authorization is expired");
        require(
            block.timestamp < validBefore,
            "EIP3009: authorization is expired"
        );
        require(
            !_authorizationStates[from][nonce],
            "EIP3009: authorization is used"
        );

        bytes memory data = abi.encode(
            TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
            from,
            to,
            value,
            validAfter,
            validBefore,
            nonce
        );
        require(
            EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from,
            "EIP3009: invalid signature"
        );

        _authorizationStates[from][nonce] = true;
        emit AuthorizationUsed(from, nonce);

        // _transfer(from, to, value);
        __transfer(from, to, value);
    }
}
```

# ERC20

Openzeppelin の ERC20 を利用して、ERC20（DemoToken）を作成します。

コンストラクタ で ドメインセパレータ を作成し、コントラクト作成者に1000000トークンを付与します。
EIP712のドメイン情報を返す、`eip712Domain` という関数を提供します。
また、EIP3009の `__transfer` の実装は ERC2 0の `_transfer` を実行する実装とします。


```javascript:DemoToken.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import "./EIP3009.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "./EIP712.sol";

contract DemoToken is ERC20, EIP3009 {
    string private _name = "DemoToken";
    string private _symbol = "DTC";
    string private _version = "1";

    constructor() ERC20(_name, _symbol) {
        DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(_name, _version);
        _mint(msg.sender, 1000000 * 10 ** 18);
    }

    function eip712Domain()
        public
        view
        returns (
            string memory name,
            string memory version,
            uint256 chainId,
            address verifyingContract
        )
    {
        name = _name;
        version = _version;
        chainId = block.chainid;
        verifyingContract = address(this);
    }

    function __transfer(
        address sender,
        address recipient,
        uint256 amount
    ) internal virtual override {
        return super._transfer(sender, recipient, amount);
    }
}
```

# クライアント（Client）

実際に `transferWithAuthorization` を利用して、トークンを移転してみます。
`deployer` から `user1` へ 100トークン移転を行いますが、トランザクションは `user2` が送信します。
署名は `deployer` が行います。

```javascript
    const CONTRACT_ADDRESS = ...; // コントラクトアドレス
    const PROVIDER = new ethers.JsonRpcProvider('http://localhost:8545/'); // プロバイダー
    const CONTRACT_ABI = [
        // ERC-20
        "function name() public view returns (string)",
        "function symbol() public view returns (string)",
        "function decimals() public view returns (uint8)",
        "function totalSupply() public view returns (uint256)",
        "function balanceOf(address _owner) public view returns (uint256 balance)",
        "function transfer(address _to, uint256 _value) public returns (bool success)",
        "function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)",
        "function approve(address _spender, uint256 _value) public returns (bool success)",
        "function allowance(address _owner, address _spender) public view returns (uint256 remaining)",
        "event Transfer(address indexed _from, address indexed _to, uint256 _value)",
        "event Approval(address indexed _owner, address indexed _spender, uint256 _value)",
        // ERC-3009
        "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)",
    ];
    // コントラクト取得
    const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, PROVIDER);
    // 小数点以下の桁数を取得
    const decimals = await contract.decimals();

    // Deployer、User1、User2の残高を取得
    await balance(PROVIDER, contract, decimals, [deployer.address, user1.address, user2.address]);

    console.log('>>> transferWithAuthorization >>>');
    // ERC-3009のTransferWithAuthorizationを実行
    // EIP712のドメインを取得
    const eip712Domain = await token.eip712Domain();
    // EIP712のドメインを設定
    const domain = {
        name: eip712Domain.name,                           // ドメイン名
        version: eip712Domain.version,                     // ドメインバージョン
        verifyingContract: eip712Domain.verifyingContract, // コントラクトアドレス
        chainId: eip712Domain.chainId,                     // チェーンID
    };
    // TransferWithAuthorizationのタイプを定義
    const TYPES = {
        TransferWithAuthorization: [
            { name: "from", type: "address" },        // 支払アドレス (承認者)
            { name: "to", type: "address" },          // 受取アドレス
            { name: "value", type: "uint256" },       // 送金金額
            { name: "validAfter", type: "uint256" },  // この時間の後で有効 (UNIX 時間)
            { name: "validBefore", type: "uint256" }, // この時間の前で有効 (UNIX 時間)
            { name: "nonce", type: "bytes32" }        // 一意の nonce (32 バイト)
        ]
    };
    // TransferWithAuthorizationの値を定義
    const value = {
        from: deployer.address,                                     // 支払アドレス (承認者)
        to: user1.address,                                          // 受取アドレス
        value: ethers.parseUnits("100", decimals),                  // 送金金額
        validAfter: 0n,                                             // この時間の後で有効 (UNIX 時間)
        validBefore: BigInt(Math.floor(Date.now() / 1000)) + 3600n, // この時間の前で有効 (UNIX 時間)
        nonce: ethers.randomBytes(32),                              // 一意の nonce (32 バイト)
    };
    // EIP712の署名を取得
    const sign = ethers.Signature.from(await deployer.signTypedData(domain, TYPES, value));
    // TransferWithAuthorizationを実行
    const tx = await token.connect(user2).transferWithAuthorization(
        value.from,        // 支払アドレス (承認者)
        value.to,          // 受取アドレス
        value.value,       // 送金金額
        value.validAfter,  // この時間の後で有効 (UNIX 時間)
        value.validBefore, // この時間の前で有効 (UNIX 時間)
        value.nonce,       // 一意の nonce (32 バイト)
        sign.v,            // 署名の v 値
        sign.r,            // 署名の r 値
        sign.s);           // 署名の s 値
    // トランザクションの待機
    const receipt = await tx.wait();
    // トランザクションの結果を表示
    if (receipt) {
        console.log('receipt#status', receipt.status, ethers.formatEther(receipt.gasUsed * receipt.gasPrice));
    }
    console.log('<<< transferWithAuthorization <<<');

    await sleep(100); // 100ミリ秒待機

    // Deployer、User1、User2の残高を取得
    await balance(PROVIDER, contract, decimals, [deployer.address, user1.address, user2.address]);
```

以下に `sleep` 関数とトークンとネイティブトークンを表示する `balance` 関数を作成します。

```javascript
function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
};

async function balance(provider: Provider, contract: Contract, decimals: Numeric, addressList: string[]) {
    console.log('>>> balanceOf >>>');
    console.log('BlockNumber', await provider.getBlockNumber());
    for (const address of addressList) {
        const token = ethers.formatUnits(await contract.balanceOf(address), decimals);
        const native = ethers.formatEther(await provider.getBalance(address));
        console.log(address,
            token.padStart(10 - token.indexOf('.') + token.length),
            native.padStart(10 - native.indexOf('.') + native.length));
    }
    console.log('<<< balanceOf <<<');
}
```

実行結果は以下となります。
トークンの移転は、１番目のアドレス（`deployer`）から２番目のアドレス（`user1`）へ移転されていますが、ネイティブトークンが使用されていません。（`9999.998047450228515625`のまま）
トランザクションを送信した３番目のアドレス（`user2`）からネイティブトークンが減っています。
（`10000.0` ⇒ `9999.99991382376283858`）
差分はトランザクションのガス代（ガス量×ガス価格：`0.00008617623716142`）と一致します。

```sh
>>> balanceOf >>>
BlockNumber 1
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266    1000000.0       9999.998047450228515625
0x70997970C51812dc3A010C7d01b50e0d17dc79C8          0.0      10000.0
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC          0.0      10000.0
<<< balanceOf <<<
>>> transferWithAuthorization >>>
receipt#status 1 0.00008617623716142
<<< transferWithAuthorization <<<
>>> balanceOf >>>
BlockNumber 2
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266     999900.0       9999.998047450228515625
0x70997970C51812dc3A010C7d01b50e0d17dc79C8        100.0      10000.0
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC          0.0       9999.99991382376283858
<<< balanceOf <<<
```

# まとめ（Conclusion）

「ガスの支払いを他のユーザーに委任」することに成功しました。
実際はクライアントのコードのように一連の中で署名値を扱うことはなく、署名値はWalletConnectなどを経由して受け取る必要があります。