Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7720: 延期代币转移

允许用户安排 [ERC-20](./eip-20.md) 代币转移给受益人,以便在指定的未来时间提取,从而实现延期支付。

Authors Chen Liaoyuan (@chenly) <cly@kip.pro>
Created 2024-06-09
Discussion Link https://ethereum-magicians.org/t/erc-7720-deferred-token-transfer/20245
Requires EIP-20

摘要

本标准规定允许用户为受益人存入 ERC-20 代币。受益人只能在指定的未来时间戳之后提取代币。每个存款交易都被分配一个唯一的 ID,并包括诸如代币地址、发送者、接收者、金额、解锁时间和提取状态等详细信息。

动机

在各种场景中,例如归属计划、托管服务或定时奖励,都需要延期支付。该合约为时间锁定的代币转移提供了一种安全可靠的机制,确保代币只能在达到指定时间戳后才能转移。通过促进结构化和延迟支付,它为代币转移增加了一层额外的安全性和可预测性。这对于支付取决于时间流逝的场景尤其有用。

规范

本文档中的关键词 “MUST”、”MUST NOT”、”REQUIRED”、”SHALL”、”SHALL NOT”、”SHOULD”、”SHOULD NOT”、”RECOMMENDED”、”NOT RECOMMENDED”、”MAY” 和 “OPTIONAL” 按照 RFC 2119 和 RFC 8174 中的描述进行解释。

该标准的实现者必须具有以下所有功能:

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.
    //   _token:ERC20 代币合约的地址。
    // - _from: Address of the sender.
    //   _from:发送者的地址。
    // - _to: Address of the recipient.
    //   _to:接收者的地址。
    // - _amount: Amount of tokens to be transferred.
    //   _amount:要转移的代币数量。
    // - _unlockTime: Time after which the tokens can be withdrawn.
    //   _unlockTime:在此时间之后可以提取代币的时间。
    // - _reference: Reference ID for the transaction.
    //   _reference:交易的参考 ID。
    // Returns the transaction ID.
    // 返回交易 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.
    //   _txnId:要从中提取的交易的 ID。
    function withdraw(uint256 _txnId) external;

    // Function to get transaction details.
    // 用于获取交易详细信息的函数。
    // Parameters:
    // 参数:
    // - _txnId: ID of the transaction.
    //   _txnId:交易的 ID。
    // 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
        );
}

理由

延期代币转移合约的设计旨在提供一种直接且安全的方法来处理时间锁定的代币转移。在开发过程中考虑了以下因素:

使用 uint40 实现解锁时间精度:我们为 _unlockTime 选择了一个完整的 uint40,因为它提供了足够大的范围来覆盖所有实际的时间锁定场景。这确保了合约可以处理需要在很长一段时间内进行精确计时的延期支付,例如归属计划或长期托管。

transferFrom 返回 txnIdtransferFrom 函数为每个交易返回一个唯一的 txnId。做出此设计选择是为了方便轻松地独立跟踪每个交易。通过拥有唯一的 ID,用户可以管理和引用特定交易,从而确保清晰度并防止混淆。这种方法允许独立管理每个交易的状态,从而简化了提款过程。

与现有 ERC-20 代币的兼容性:该标准被设计为一个单独的接口,而不是 ERC-20 的扩展,以确保灵活性和广泛的兼容性。通过不直接修改 ERC-20 标准,该提案可以与任何现有的 ERC-20 代币一起使用,而无需更改其合约。这种灵活性使该标准适用于已在流通的各种代币,从而增强了其实用性和采用潜力。

参考实现

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.
        // ERC20 代币合约的地址。
        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.
        // 交易的参考 ID。
        bool withdrawn;     // Flag indicating if the tokens have been withdrawn.
        // 标志,指示代币是否已被提取。
    }

    // Mapping from transaction ID to Transaction structure.
    // 从交易 ID 到 Transaction 结构的映射。
    mapping(uint256 => Transaction) public transactions;

    // Variable to keep track of the next transaction ID.
    // 用于跟踪下一个交易 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.
    //   _token:ERC20 代币合约的地址。
    // - _from: Address of the sender.
    //   _from:发送者的地址。
    // - _to: Address of the recipient.
    //   _to:接收者的地址。
    // - _amount: Amount of tokens to be transferred.
    //   _amount:要转移的代币数量。
    // - _unlockTime: Time after which the tokens can be withdrawn.
    //   _unlockTime:在此时间之后可以提取代币的时间。
    // - _reference: Reference ID for the transaction.
    //   _reference:交易的参考 ID。
    // Returns the transaction ID.
    // 返回交易 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");
        // 需要(_amount > 0,“无效的转移金额”);

        // 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.
    //   _txnId:要从中提取的交易的 ID。
    function withdraw(uint256 _txnId) external {
        Transaction storage transaction = transactions[_txnId];
        require(transaction.amount > 0, "Invalid transaction ID");
        // 需要(transaction.amount > 0,“无效的交易 ID”);
        require(block.timestamp >= transaction.unlockTime, "Current time is before unlock time");
        // 需要(block.timestamp >= transaction.unlockTime,“当前时间在解锁时间之前”);
        // require(transaction.to == msg.sender, "Only the recipient can withdraw the tokens");
        require(!transaction.withdrawn, "Tokens already withdrawn");
        // 需要(!transaction.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.
    //   _txnId:交易的 ID。
    // 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");
        // 需要(transaction.amount > 0,“无效的交易 ID”);

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

安全考虑

无所有者合约设计:为防止存款后代币丢失的风险,合约不应有所有者。这确保了合约的代币余额不能转移到指定受益人以外的任何地址。

严格的受益人控制:在提款期间,合约必须严格确保代币仅转移到存款时指定的受益人。这可以防止未经授权的访问,并确保只有指定的接收者才能提取代币。

版权

版权及相关权利已通过 CC0 放弃。

Citation

Please cite this document as:

Chen Liaoyuan (@chenly) <cly@kip.pro>, "ERC-7720: 延期代币转移 [DRAFT]," Ethereum Improvement Proposals, no. 7720, June 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7720.