2024.07.08

ERC-1363トークンの紹介

こんにちは。次世代システム研究室のL.W.です。

ERC-1363は、Ethereumトークンの標準規格の一つであり、支払いを簡素化し、トークンのユーティリティを拡張するための仕組みです。この標準は、ERC-20とERC-721トークンの機能を拡張して、特定のスマートコントラクトがトークンを受け取った際に自動的にコールバック関数(hook)を呼び出せるように設計されています。

一体、どのようなメリットがあるか。なんで最近注目されているかについて紹介します。

 

1.ERC-20トークンはどのような不足があるか

ERC-20トークンは、ERC-20スタンダードに従って、作ったコントラクトです。スマートコントラクト上でトークンを実装するための技術仕様を定めたものです。これにより、異なるプロジェクトが同じ基準でトークンを作成・管理でき、相互運用性が確保されます。ERC-20トークンはDEX、DAOを始め、既に各DAPPで活躍されています。

 

1.1. ERC-20 Token Standard

広義的には、以下のインタフェースを実現したコントラクトはERC20トークンと呼ばれます。もちろん、これ以外は拡張するのは可能です。例えば、ガバナンストークンのCOMP、UNIは投票の機能を拡張しています。セキュリティのために、我々のGYENはアカウント管理機能を拡張しています。

ERC20トークンの送付は、transferで実現しています。第三者に自分のトークンの送付を承認するのはapproveで実現しています。

 

interface ERC20 {
  function totalSupply() external view returns (uint256);
  function balanceOf(address account) external view returns (uint256);
  function transfer(address recipient, uint256 amount) external returns (bool);
  function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
  function allowance(address owner, address spender) external view returns (uint256);
  function approve(address spender, uint256 amount) external returns (bool);
  event Transfer(address indexed from, address indexed to, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);
}

 

1.2.ERC-20 Tokenの不足

Uniswap、AAVE、Bridge(EthereumとArbitrumyやOptimismの間のトークンの転送)などのDAPPの利用で、ユーザはまず自分の持つERC20トークンを該当コントラクトに預ける必要があります。預けてから該当DAPPコントラクトでどのアカウントからどのぐらいのトークンを預けたかをコントラクトに記録しないといけません。
ただ、ERC20のtransferだけではこのニーズに満たせません。

以下のopenzeppelinのtransferの実現を見てください。
toはDAPPのコントラクトアドレスに指定されても、DAPPのコントラクトで感知できないので、記帳できません。

function transfer(address to, uint256 value) public virtual returns (bool) {
    address owner = _msgSender();
    _transfer(owner, to, value);
    return true;
}
では、今までのDAPPはどのように実現してきましたか。ユーザはapproveとtransferFromの2回の呼び出しでDAPPコントラクトで記帳を完成さたと言うことです。
以下のはapproveの実現です。

function approve(address spender, uint256 value) public virtual returns (bool) {
    address owner = _msgSender();
    _approve(owner, spender, value);
    return true;
}

ユーザはapproveの呼び出しで、spenderをDAPPのコントラクトにしておきます。valueはDAPPコントラクトにどのぐらいのトークンの送付を承認するかの数値です。

transferFromの実現は以下の通りです。
fromアカウントは事前にspenderにvalueのトークンの送付を承認しておきましたので、spenderしかtransferFromを呼び出せないです。
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
    address spender = _msgSender();
    _spendAllowance(from, spender, value);
    _transfer(from, to, value);
    return true;
}

DAPPのコントラクトの実現はそれぞれですが、まとめてみると、記帳するためには、以下のように要約できるかと思います。

contract xxxDappContract {
    function deposit(uint256 amount) external {
        // will revert if this contract is not approved
        // or the user has an insufficient balance
        ERC20(token).transferFrom(msg.sender, address.this, amount); // 承認されたので、ユーザの代わりに、該当コントラクトにトークン送付する

        deposits[msg.sender] += amount;  // どのアカウントから、どのぐらいのトークンをデポジットしたか、記帳する。
    }
}
次は、ユーザはDAPPコントラクトのdepositを呼び出します。depositでtransferFromを呼び出しています。

ここまで、どのような問題がありますか。

まずは、コスト面で、2回の別々のトランザクションなので、GASを二回に支払う必要があります。
セキュリティー面では、トランザクションの順序によって古い許可と新しい許可の両方が使用される危険性があります。
例えば、approveでのvalueを不意に無限大にすると、DAPPの欠陥でユーザの持つ全てのトークンを失われることも可能となります。

では、ユーザは一回のDAPPコントラクトへのトークンの送付のトランザクションで、DAPPコントラクトに記帳を済ませるか。ERC-1363はまさにこのペインポイントを解決するための提案です。

2.ERC-1363スタンダードの紹介

トークンが転送された際に特定のコールバック関数を実行する能力までに拡張するのはこのERC-1363の標準規格です。この標準は、特定の条件が満たされた時に、自動的にアクションを起こすようにスマートコントラクトに指示することができるため、より洗練されたトークンの使い方が可能になります。

2.1. ERC-1363 Standard インタフェース

ERC1363の標準インタフェースをERC20のを継承しています。

pragma solidity ^0.8.0;

/**
 * @title ERC1363
 * @dev An extension interface for ERC-20 tokens that supports executing code on a recipient contract
 * after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction.
 */
interface ERC1363 is ERC20, ERC165 {
  /*
   * NOTE: the ERC-165 identifier for this interface is 0xb0202a11.
   * 0xb0202a11 ===
   *   bytes4(keccak256('transferAndCall(address,uint256)')) ^
   *   bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
   *   bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
   *   bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
   *   bytes4(keccak256('approveAndCall(address,uint256)')) ^
   *   bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
   */

  /**
   * @dev Moves a `value` amount of tokens from the caller's account to `to`
   * and then calls `ERC1363Receiver::onTransferReceived` on `to`.
   * @param to The address to which tokens are being transferred.
   * @param value The amount of tokens to be transferred.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function transferAndCall(address to, uint256 value) external returns (bool);

  /**
   * @dev Moves a `value` amount of tokens from the caller's account to `to`
   * and then calls `ERC1363Receiver::onTransferReceived` on `to`.
   * @param to The address to which tokens are being transferred.
   * @param value The amount of tokens to be transferred.
   * @param data Additional data with no specified format, sent in call to `to`.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool);

  /**
   * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
   * and then calls `ERC1363Receiver::onTransferReceived` on `to`.
   * @param from The address from which to send tokens.
   * @param to The address to which tokens are being transferred.
   * @param value The amount of tokens to be transferred.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function transferFromAndCall(address from, address to, uint256 value) external returns (bool);

  /**
   * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism
   * and then calls `ERC1363Receiver::onTransferReceived` on `to`.
   * @param from The address from which to send tokens.
   * @param to The address to which tokens are being transferred.
   * @param value The amount of tokens to be transferred.
   * @param data Additional data with no specified format, sent in call to `to`.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool);

  /**
   * @dev Sets a `value` amount of tokens as the allowance of `spender` over the caller's tokens
   * and then calls `ERC1363Spender::onApprovalReceived` on `spender`.
   * @param spender The address which will spend the funds.
   * @param value The amount of tokens to be spent.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function approveAndCall(address spender, uint256 value) external returns (bool);

  /**
   * @dev Sets a `value` amount of tokens as the allowance of `spender` over the caller's tokens
   * and then calls `ERC1363Spender::onApprovalReceived` on `spender`.
   * @param spender The address which will spend the funds.
   * @param value The amount of tokens to be spent.
   * @param data Additional data with no specified format, sent in call to `spender`.
   * @return A boolean value indicating the operation succeeded unless throwing.
   */
  function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool);
}

 

2.2. ERC1363Receiver(トークンの受入側のコントラクト)のStandard インタフェース

ユーザはDAPPコントラクトにトークンを預ける時、transferAndCallだけを呼び出すことで済ませるために、transferAndCallでのパラメーターのtoのDAPPコントラクトも拡張させる必要があります。つまり、受け取り側として、onTransferReceivedを実現しないといけないです。

/**
 * @title ERC1363Receiver
 * @dev Interface for any contract that wants to support `transferAndCall` or `transferFromAndCall` from ERC-1363 token contracts.
 */
interface ERC1363Receiver {
  /**
   * @dev Whenever ERC-1363 tokens are transferred to this contract via `ERC1363::transferAndCall` or `ERC1363::transferFromAndCall`
   * by `operator` from `from`, this function is called.
   *
   * NOTE: To accept the transfer, this must return
   * `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))`
   * (i.e. 0x88a7ca5c, or its own function selector).
   *
   * @param operator The address which called `transferAndCall` or `transferFromAndCall` function.
   * @param from The address which are tokens transferred from.
   * @param value The amount of tokens transferred.
   * @param data Additional data with no specified format.
   * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` if transfer is allowed unless throwing.
   */
  function onTransferReceived(address operator, address from, uint256 value, bytes calldata data) external returns (bytes4);
}

onTransferReceivedで記帳を実現する例です。

function onTransferReceived(
    address operator,
    address from,
    uint256 value,
    bytes calldata data
) external returns (bytes4) {
    
    require(msg.sender == erc1363Token, "not the expected token");
    balances[from] += value;
   // 正しく処理された場合、関数の署名を返却。
    return this.onTransferReceived.selector;
}

ERC-1363のtransferAndCallでtoのコントラクトのonTransferReceivedを呼び出します。onTransferReceivedが正しく処理された時だけ、トークンの送付が成功と言えます。

function transferAndCall(address to, uint256 value) public virtual returns (bool) {
    return transferAndCall(to, value, "");
}

/**
 * @inheritdoc IERC1363
 */
function transferAndCall(address to, uint256 value, bytes memory data) public virtual returns (bool) {
    if (!transfer(to, value)) {
        revert ERC1363TransferFailed(to, value);
    }
    _checkOnTransferReceived(_msgSender(), to, value, data);
    return true;
}

/**
 * @dev Performs a call to `IERC1363Receiver::onTransferReceived` on a target address.
 * This will revert if the target doesn't implement the `IERC1363Receiver` interface or
 * if the target doesn't accept the token transfer or
 * if the target address is not a contract.
 *
 * @param from Address representing the previous owner of the given token amount.
 * @param to Target address that will receive the tokens.
 * @param value The amount of tokens to be transferred.
 * @param data Optional data to send along with the call.
 */
function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private {
    if (to.code.length == 0) {
        revert ERC1363EOAReceiver(to);
    }

    try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
     // 関数の署名が返却され場合、onApprovalReceivedが正しく処理されたことと見なす。
        if (retval != IERC1363Receiver.onTransferReceived.selector) {
            revert ERC1363InvalidReceiver(to);
        }
    } catch (bytes memory reason) {
        if (reason.length == 0) {
            revert ERC1363InvalidReceiver(to);
        } else {
            assembly {
                revert(add(32, reason), mload(reason))
            }
        }
    }
}

 

同様に、transferFromをカスタマイズして、何か面白いことを実現できます。

transferFromAndCallの実現をあなたの想像にお任せします。

 

2.3. ERC1363Spender(承認先のコントラクト)のStandard インタフェース

このケースを考えてみてください。あるDAPPコントラクトをトークンのapprove(承認)をされました。

承認の同時に、トークンの送付も実現させます。このケースでは、承認されたコントラクトにはonApprovalReceivedを拡張しないとい行けないです。

/**
 * @title ERC1363Spender
 * @dev Interface for any contract that wants to support `approveAndCall` from ERC-1363 token contracts.
 */
interface IERC1363Spender {
    /**
     * @dev Whenever an ERC-1363 tokens `owner` approves this contract via `IERC1363::approveAndCall` to spend their tokens, this function is called.
     *
     * NOTE: To accept the approval, this must return
     * `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`
     * (i.e. 0x7b04a2d0, or its own function selector).
     *
     * @param owner The address which called `approveAndCall` function and previously owned the tokens.
     * @param value The amount of tokens to be spent.
     * @param data Additional data with no specified format.
     * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` if approval is allowed unless throwing.
     */
    function onApprovalReceived(address owner, uint256 value, bytes calldata data) external returns (bytes4);
}

 

onApprovalReceivedでトークンの送付を実現する例です。

function onApprovalReceived(            
    address owner,            
    uint256 value,            
    bytes calldata data
) external returns (bytes4) {                
    require(isApprovedToken[msg.sender], "not an approved token"); 
           
    // getTarget is not implemented here,                
    // see the next section for how to it work                
    address target = getTarget(data);                
    bool success = IERC1363(msg.sender).transferFrom(owner, target, value);                

    require(success, "transfer failed");                
    // 正しく処理された場合、関数の署名を返却。
    return this.onApprovalReceived.selector; 
   
}

 

ERC-1363のapproveAndCallでtoのコントラクトのonTransferReceivedを呼び出します。approveAndCallが正しく処理された時だけ、トークンの送付の承認が成功と言えます。

/**
 * @inheritdoc IERC1363
 */
function approveAndCall(address spender, uint256 value) public virtual returns (bool) {
    return approveAndCall(spender, value, "");
}

/**
 * @inheritdoc IERC1363
 */
function approveAndCall(address spender, uint256 value, bytes memory data) public virtual returns (bool) {
    if (!approve(spender, value)) {
        revert ERC1363ApproveFailed(spender, value);
    }
    _checkOnApprovalReceived(spender, value, data);
    return true;
}
/**
 * @dev Performs a call to `IERC1363Spender::onApprovalReceived` on a target address.
 * This will revert if the target doesn't implement the `IERC1363Spender` interface or
 * if the target doesn't accept the token approval or
 * if the target address is not a contract.
 *
 * @param spender The address which will spend the funds.
 * @param value The amount of tokens to be spent.
 * @param data Optional data to send along with the call.
 */
function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private {
    if (spender.code.length == 0) {
        revert ERC1363EOASpender(spender);
    }

    try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 retval) {
     // 関数の署名が返却され場合、onApprovalReceivedが正しく処理されたことと見なす。
        if (retval != IERC1363Spender.onApprovalReceived.selector) {
            revert ERC1363InvalidSpender(spender);
        }
    } catch (bytes memory reason) {
        if (reason.length == 0) {
            revert ERC1363InvalidSpender(spender);
        } else {
            assembly {
                revert(add(32, reason), mload(reason))
            }
        }
    }
}

 

3.まとめ

トークンをDappのコントラクトへの送付(transfer,transferFrom)や承認(approve)したときに、追加でトランザクションを走らせずにDappのコントラクトでの記帳処理などを実行できるの便利ですね。

ERC1363トークンの一般的な使い道としては、サービスやプロダクトの支払い系のシステム、DeFi(分散型金融)アプリケーション、トークン駆動型エコシステムの自動化が挙げられ流ことで、「Payable Token」も呼ばれてます。

ただ、ERC20トークンをERC1363までにアップグレードだけではなく、DappのコントラクトもERC1363SpenderとERC1363Receiverまでにアップグレードしないと行けないこととなりますので、イーサリアムの世界では容易ではないでしょう。

そもそもアップグレードの実現ができないERC20トークン、Dappのコントラクトがたくさんがあることですから。

ERC1363トークンとERC1363トークンの対応するDappのコントラクトをデプロイし直すのは一つの解決案ですが、ユーザに新規デプロイしたコントラクト へのマイグレーションをさせるのも容易ではないでしょう。

ただ、ERC1363スタンダードでトークンとDappのコントラクトの将来性が見えて始めますね。これからのトレンドを見守っていきましょう。

 

4.最後に

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事