0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに(Introduction)

USDCJPYCなどが採用している、ERC3009を実装してみます。

ERC3009

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

実装(Implementation)

OpenZeppelinのERC20に公式のImplementationを適用します。
※:一部不具合やOpenZeppelinと合致しない部分を修正します。

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

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)を実装しています。

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 に変更しています。

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); に変更しています。

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 を実行する実装とします。

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 が行います。

    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 関数を作成します。

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 <<<');
}

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

>>> 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などを経由して受け取る必要があります。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?