はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、ERC20トークンを指定した将来のタイムスタンプ以降に引き出せるようにする仕組みを提案しているERC7720についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

この規格では、特定のアドレスが引き出せるようにERC20トークンを預けて、指定した将来のタイムスタンプ以降にのみトークンを引き出すことができます。 預け入れる時に以下の情報が保存されます。

  • token address
    • 預けられたERC20トークンのアドレス。
  • sender
    • トークンを預けたユーザーのアドレス。
  • recipient
    • トークンを受け取る予定のアドレス。
  • amount
    • 預けられたトークンの数量。
  • unlock time
    • トークンを引き出すことができるようになる指定された未来のタイムスタンプ。
  • withdrawal status
    • トークンが引き出されたかどうかを示すステータス。

ERC20については以下の記事を参考にしてください。

動機

べスティングスケジュール、エスクローサービス、タイムドリワードなど、支払いを遅延させるシナリオは様々あります。 このコントラクトは、指定されたタイムスタンプに達するまでトークン送付されないようにして、時間ロックされたトークン送付を安全かつ信頼性のあるメカニズムで実装できます。 これにより以下のメリットがあります。

  • 構造化された支払い
    • トークンの転送が時間的に構造化され設定された通りに行われる。
  • セキュリティ
    • トークンが指定された日時まで引き出せないため、受取アドレスが不正にトークンを取得するリスクが軽減される。
  • 予測可能性
    • トークンの転送が特定の時間に行われるため設計がしやすくなる。

仕様

この規格に準拠するコントラクトは以下の機能を実装する必要があります。

js
pragma solidity ^0.8.0;

interface ITokenTransfer {
    // Event emitted when a transfer is initiated.
    event Transfer(
        uint256 txnId,
        address indexed token,
        address indexed from,
        address indexed to,
        uint256 amount,
        uint40 unlockTime,
        bytes32 referenceNo
    );

    // Event emitted when tokens are withdrawn.
    event Withdraw(
        uint256 txnId,
        address indexed token,
        address indexed from,
        address indexed to,
        uint256 amount
    );

    // Function to initiate a token transfer.
    // Parameters:
    // - _token: Address of the ERC20 token contract.
    // - _from: Address of the sender.
    // - _to: Address of the recipient.
    // - _amount: Amount of tokens to be transferred.
    // - _unlockTime: Time after which the tokens can be withdrawn.
    // - _reference: Reference ID for the transaction.
    // Returns the transaction ID.
    function transferFrom(
        address _token,
        address _from,
        address _to,
        uint256 _amount,
        uint40 _unlockTime,
        bytes32 _reference
    ) external returns (uint256 txnId);

    // Function to withdraw tokens from a transaction.
    // Parameters:
    // - _txnId: ID of the transaction to withdraw from.
    function withdraw(uint256 _txnId) external;

    // Function to get transaction details.
    // Parameters:
    // - _txnId: ID of the transaction.
    // Returns the transaction details.
    function getTransaction(uint256 _txnId)
        external
        view
        returns (
            address token,
            address from,
            address to,
            uint256 amount,
            uint40 unlockTime,
            bytes32 referenceNo,
            bool withdrawn
    );
}

Transfer

js
event Transfer(
    uint256 txnId,
    address indexed token,
    address indexed from,
    address indexed to,
    uint256 amount,
    uint40 unlockTime,
    bytes32 referenceNo
);

トークンがtrasnferされた時に発行されるイベント。

  • txnId
    • トランザクションのID。
  • token
    • ERC20トークンのコントラクトアドレス。
  • from
    • 送信者のアドレス。
  • to
    • 受け取りアドレス。
  • amount
    • 転送されるトークンの量。
  • unlockTime
    • トークンが引き出せるようになる時刻(タイムスタンプ)。
  • referenceNo
    • トランザクションの参照ID。

Withdraw

トークンが引き出される時に発行されるイベント。

js
event Withdraw(
    uint256 txnId,
    address indexed token,
    address indexed from,
    address indexed to,
    uint256 amount
);

transferFrom

js
function transferFrom(
    address _token,
    address _from,
    address _to,
    uint256 _amount,
    uint40 _unlockTime,
    bytes32 _reference
) external returns (uint256 txnId);

トークンtrasnferを行う関数。

  • _token
    • ERC20トークンのコントラクトアドレス。
  • _from
    • 送信者のアドレス。
  • _to
    • 受け取りアドレス。
  • _amount
    • 転送されるトークンの量。
  • _unlockTime
    • トークンが引き出せるようになる時刻(タイムスタンプ)。
  • _reference
    • トランザクションの参照ID。

withdraw

トランザクションIDからトークンを引き出す関数。

js
function withdraw(uint256 _txnId) external;
  • _txnId
    • き出すトランザクションのID。

getTransaction

トランザクションの詳細を取得する関数。

js

function getTransaction(uint256 _txnId)
    external
    view
    returns (
        address token,
        address from,
        address to,
        uint256 amount,
        uint40 unlockTime,
        bytes32 referenceNo,
        bool withdrawn
);
  • _txnId
    • トランザクションのID。

補足

uint40によるUnlock Timeの精度

_unlockTimeにuint40を使用することで、十分なロック時間をカバーできます。 これにより、ベスティングスケジュールや長期のエスクローなど、長期間にわたる遅延支払いにも対応できます。

約35,000年までカバーできます。

transferFromからのtxnIdの返却

transferFrom関数が各トランザクションに対して固有のtxnIdを返すように設計されています。 固有のIDがあることで、ユーザーは特定のトランザクションを管理および参照できます。

既存のERC20トークンとの互換性

この標準はERC20の拡張ではなく、別のインターフェースとして設計されています。 これにより、既存のERC20トークンに変更を加えることなく使用でき、既に存在しているさまざまなトークンに適用できるようになります。

参考実装

js
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract TokenTransfer {
    using SafeERC20 for IERC20;

    struct Transaction {
        address token;      // Address of the ERC20 token contract.
        address from;       // Address of the sender.
        address to;         // Address of the recipient.
        uint256 amount;     // Amount of tokens to be transferred.
        uint40 unlockTime; // Time after which the tokens can be withdrawn.
        bytes32 referenceNo;  // Reference ID for the transaction.
        bool withdrawn;     // Flag indicating if the tokens have been withdrawn.
    }

    // Mapping from transaction ID to Transaction structure.
    mapping(uint256 => Transaction) public transactions;

    // Variable to keep track of the next transaction ID.
    uint256 public lastTxnId = 0;

    // Event emitted when a transfer is initiated.
    event Transfer(
        uint256 txnId,
        address indexed token,
        address indexed from,
        address indexed to,
        uint256 amount,
        uint40 unlockTime,
        bytes32 referenceNo
    );

    // Event emitted when tokens are withdrawn.
    event Withdraw(
        uint256 txnId,
        address indexed token,
        address indexed from,
        address indexed to,
        uint256 amount
    );

    constructor() {}

    // Function to initiate a token transfer.
    // Parameters:
    // - _token: Address of the ERC20 token contract.
    // - _from: Address of the sender.
    // - _to: Address of the recipient.
    // - _amount: Amount of tokens to be transferred.
    // - _unlockTime: Time after which the tokens can be withdrawn.
    // - _reference: Reference ID for the transaction.
    // Returns the transaction ID.
    function transferFrom(
        address _token,
        address _from,
        address _to,
        uint256 _amount,
        uint40 _unlockTime,
        bytes32 _reference
    ) external returns (uint256 txnId) {
        require(_amount > 0, "Invalid transfer amount");

        // Transfer tokens from sender to this contract.
        IERC20(_token).safeTransferFrom(_from, address(this), _amount);

        lastTxnId++;

        // Store the transaction details.
        transactions[lastTxnId] = Transaction({
            token: _token,
            from: _from,
            to: _to,
            amount: _amount,
            unlockTime: _unlockTime,
            referenceNo: _reference,
            withdrawn: false
        });

        // Emit an event for the transaction creation.
        emit Transfer(lastTxnId, _token, _from, _to, _amount, _unlockTime, _reference);
        return lastTxnId;
    }

    // Function to withdraw tokens from a transaction.
    // Parameters:
    // - _txnId: ID of the transaction to withdraw from.
    function withdraw(uint256 _txnId) external {
        Transaction storage transaction = transactions[_txnId];
        require(transaction.amount > 0, "Invalid transaction ID");
        require(block.timestamp >= transaction.unlockTime, "Current time is before unlock time");
        // require(transaction.to == msg.sender, "Only the recipient can withdraw the tokens");
        require(!transaction.withdrawn, "Tokens already withdrawn");

        IERC20(transaction.token).safeTransfer(transaction.to, transaction.amount);

        transaction.withdrawn = true;

        // Emit an event for the token withdrawal.
        emit Withdraw(_txnId, transaction.token, transaction.from, transaction.to, transaction.amount);
    }

    // Function to get transaction details.
    // Parameters:
    // - _txnId: ID of the transaction.
    // Returns the transaction details.
    function getTransaction(uint256 _txnId)
        external
        view
        returns (
            address token,
            address from,
            address to,
            uint256 amount,
            uint40 unlockTime,
            bytes32 referenceNo,
            bool withdrawn
        )
    {
        Transaction storage transaction = transactions[_txnId];
        require(transaction.amount > 0, "Invalid transaction ID");

        return (
            transaction.token,
            transaction.from,
            transaction.to,
            transaction.amount,
            transaction.unlockTime,
            transaction.referenceNo,
            transaction.withdrawn
        );
    }
}

セキュリティ

オーナーレスコントラクト設計

コントラクトにオーナーを設定しないことで、コントラクトのトークン残高が指定された受け取りアドレス以外にtrasnferされることを防ぎます。 この設計により、管理者がトークンを不正に移動させるリスクが排除されます。

厳格な受け取りアドレスの管理

引き出し時に、預け入れ時に指定された受け取りアドレスにのみトークンがtrasnferされるようにすることで、不正アクセスを防止して意図された受け取りアドレスのみがトークンを引き出せるようになります。

引用

Chen Liaoyuan (@chenly) cly@kip.pro, "ERC-7720: Deferred Token Transfer [DRAFT]," Ethereum Improvement Proposals, no. 7720, June 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7720.

最後に

今回は「ERC20トークンを指定した将来のタイムスタンプ以降に引き出せるようにする仕組みを提案しているERC7720」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!