Deferred Token Transfer
Abstract
This standard specifies that allows users to deposit ERC-20 tokens for a beneficiary. The beneficiary can withdraw the tokens only after a specified future timestamp. Each deposit transaction is assigned a unique ID and includes details such as the token address, sender, recipient, amount, unlock time, and withdrawal status.
Motivation
In various scenarios, such as vesting schedules, escrow services, or timed rewards, there is a need for deferred payments. This contract provides a secure and reliable mechanism for time-locked token transfers, ensuring that tokens can only be transferred after a specified timestamp is reached. By facilitating structured and delayed payments, it adds an extra layer of security and predictability to token transfers. This is particularly useful for scenarios where payments are contingent upon the passage of time.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Implementers of this standard MUST have all of the following functions:
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
);
}
Rationale
The design of the Deferred Token Transfer contract aims to provide a straightforward and secure method for handling time-locked token transfers. The following considerations were made during its development:
Unlock Time Precision with uint40
: We chose a full uint40
for _unlockTime
because it provides a sufficiently large range to cover all practical time-lock scenarios. This ensures that the contract can handle deferred payments that require precise timing over long periods, such as vesting schedules or long-term escrows.
Returning txnId
from transferFrom
: The transferFrom
function returns a unique txnId
for each transaction. This design choice was made to facilitate easy and independent tracking of each transaction. By having a unique ID, users can manage and reference specific transactions, ensuring clarity and preventing confusion. This approach allows each transaction’s state to be managed independently, simplifying the withdrawal process.
Compatibility with Existing ERC-20 Tokens: The standard is designed as a separate interface rather than an extension of ERC-20 to ensure flexibility and broad compatibility. By not modifying the ERC-20 standard directly, this proposal can be used with any existing ERC-20 token without requiring changes to their contracts. This flexibility makes the standard applicable to a wide range of tokens already in circulation, enhancing its utility and adoption potential.
Reference Implementation
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
);
}
}
Security Considerations
Ownerless Contract Design: To prevent the risk of token loss after deposit, the contract should not have an owner. This ensures that the contract’s token balance cannot be transferred to any address other than the designated beneficiary.
Strict Beneficiary Control: During withdrawal, the contract must strictly ensure that tokens are transferred only to the beneficiary specified at the time of deposit. This prevents unauthorized access and ensures that only the intended recipient can withdraw the tokens.
Copyright
Copyright and related rights waived via CC0.