A Novel Approach of Time-limited ERC20 Token.
This article was archived by the owner on November 23, 2024, and is no longer maintained.
Introduction
The ERC20 standard has become the most widely used standard for implementing tokens on the Ethereum blockchain. However, it has a number of limitations that have become apparent as the use of ERC20 tokens has grown. One of the main limitations of the ERC20 standard is that it does not support complex use cases like the UTXO model, which can pack data into each UTXO set. In the UTXO model, each UTXO can be used to store additional data alongside the token amount, allowing for more complex and feature-rich applications to be built on top of the token standard.
For example, consider an application that requires users to attach a message or memo to each token transfer. With the ERC20 standard, this would not be possible without resorting to an external storage or adding additional functionality to the token contract. However, with the UTXO model, the message or memo can be stored directly alongside the token amount in each UTXO, allowing for seamless and efficient communication between users.
Another example of how the UTXO model can be used to pack additional data is in the implementation of non-fungible tokens (NFTs). NFTs are a type of token that represent unique and indivisible assets such as digital artwork or collectibles. With the ERC20 standard, each NFT would require a separate contract and token ID. However, with the UTXO model, each NFT can be represented as a single UTXO, with the unique asset data stored alongside the token amount.
Overall, the ability to pack additional data into each UTXO set is a key advantage of the UTXO model over the ERC20 standard. By incorporating this functionality into the ERC20 standard, it may be possible to create more versatile and powerful token-based applications that can support a wide range of use cases.
Approach
ERC20 with an expiration date using the UTXO (Unspent Transaction Output) approach is a novel method of implementing tokens on the Ethereum blockchain. This approach utilizes the same concept as UTXOs in Bitcoin, where the ownership of a token is represented as an unspent output of a transaction. which can be useful for tracking token movement and implementing more complex use cases. Where a unique UTXO represents each token with an expiration date. When a transfer occurs, the token holder must select an unexpired UTXO, and for the best user experience, the transfer function should automatically select or suggest spending the nearest unexpired UTXO for the transfer.
This can be confusing and cumbersome for users, especially those who are not familiar with the technical aspects of blockchain and cryptocurrency. Additionally, because each UTXO represents a specific amount of tokens, it can be difficult for users to accurately calculate the amounts of tokens needed to make a payment or transfer.
Furthermore, the use of UTXOs can make it more difficult to implement certain features such as token burning or the creation of new tokens. In the ERC20 standard, these actions can be easily performed by adjusting the total supply of the token. However, in the UTXO model, these actions require the creation or destruction of specific UTXOs, which can be more complex and time-consuming.
Despite these challenges, the UTXO model remains a powerful tool for creating more complex and feature-rich token-based applications. As blockchain technology continues to evolve and become more user-friendly, it is possible that the UTXO model may become more widely adopted and accessible to a broader audience.
Walkthrough
ERC20 with an expiration date using the UTXO (Unspent Transaction Output) approach is a novel method of implementing tokens on the Ethereum blockchain. This approach utilizes the same concept as UTXOs in Bitcoin, where the ownership of a token is represented as an unspent output of a transaction. which can be useful for tracking token set
From the code above, the UTXO struct can contain an extra data field so you can able to put the data into each UTXO for example you can spend and call a function at the same time.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "./IERC20UTXO.sol";
contract ERC20UTXO is Context, IERC20UTXO {
using ECDSA for bytes32;
UTXO[] private _utxos;
mapping(address => uint256) private _balances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() public view virtual override returns (string memory){
return _name;
}
function symbol() public view virtual override returns (string memory){
return _symbol;
}
function decimals() public view virtual override returns (uint8){
return 18;
}
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
function utxoLength() public view returns (uint256) {
return _utxos.length;
}
function utxo(uint256 id) public override view returns (UTXO memory) {
require(id < _utxos.length, "ERC20UTXO: id out of bound");
return _utxos[id];
}
function transfer(uint256 amount, TxInput memory input, TxOutput memory output) public virtual {
require(output.amount <= amount, "ERC20UTXO: transfer amount exceeds utxo amount");
address creator = _msgSender();
bytes storage data = _utxos[input.id].data;
if (output.amount < amount) {
uint256 value = amount - output.amount;
_spend(input, creator);
unchecked {
_balances[creator] -= value;
_balances[output.owner] += amount;
}
_create(output, creator, data);
_create(TxOutput(value, creator), creator, data);
} else {
_spend(input,creator);
unchecked {
_balances[creator] -= amount;
_balances[output.owner] += amount;
}
_create(output, creator, data);
}
}
function _mint(uint256 amount, TxOutput memory output, bytes memory data) internal virtual {
require(output.amount == amount, "ERC20UTXO: invalid amounts");
_totalSupply += amount;
unchecked {
_balances[output.owner] += amount;
}
_create(output, address(0), data);
}
function _create(TxOutput memory output, address creator, bytes memory data) internal virtual {
require(output.owner != address(0),"ERC20UTXO: create utxo output to zero address");
uint256 id = utxoLength()+1;
UTXO memory utxo = UTXO(output.amount, output.owner, data, false);
_beforeCreate(output.owner,utxo);
_utxos.push(utxo);
emit UTXOCreated(id, creator);
_afterCreate(output.owner,utxo);
}
function _spend(TxInput memory inputs, address spender) internal virtual {
require(inputs.id < _utxos.length, "ERC20UTXO: utxo id out of bound");
UTXO memory utxo = _utxos[inputs.id];
require(!utxo.spent, "ERC20UTXO: utxo has been spent");
_beforeSpend(utxo.owner,utxo);
require(
utxo.owner == keccak256(abi.encodePacked(inputs.id))
.toEthSignedMessageHash()
.recover(inputs.signature),
"ERC20UTXO: invalid signature");
_utxos[inputs.id].spent = true;
emit UTXOSpent(inputs.id, spender);
_afterSpend(utxo.owner,utxo);
}
function _beforeCreate(address creator, UTXO memory utxo) internal virtual {}
function _afterCreate(address creator, UTXO memory utxo) internal virtual {}
function _beforeSpend(address spender, UTXO memory utxo) internal virtual {}
function _afterSpend(address spender, UTXO memory utxo) internal virtual {}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.14;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./ERC20UTXO.sol";
abstract contract ERC20UTXOExpirable is ERC20UTXO, Ownable {
uint64 private immutable _period;
constructor(uint64 period_) {
_period = period_ ;
}
function mint(uint256 amount, TxOutput memory outputs) public onlyOwner {
_mint(amount, outputs, abi.encode(block.timestamp + _period));
}
function _beforeSpend(address spender, UTXO memory utxo) internal override {
uint256 expireDate = abi.decode(utxo.data, (uint256));
require(block.timestamp < expireDate,"UTXO has been expired");
}
}
The ERC20UTXOExpirable
contract inherits from both the ERC20UTXO
and the Ownable
contracts provided by OpenZeppelin. ERC20UTXO
implements the basic ERC20 token standard for UTXOs, while Ownable
provides a modifier for checking that a transaction is executed by the owner of the contract.
The contract introduces a new state variable called _period
which is an immutable variable that is set during contract creation. The _period
variable defines the duration in seconds that the UTXO will be valid for.
The mint
function allows the owner of the contract to create new UTXOs with an expiration date. It takes an amount
parameter, which specifies the number of tokens to create, and a TxOutput
parameter, which specifies the output of the transaction. The output is encoded with the expiration date by adding _period
to the current block timestamp. The _mint
function from the ERC20UTXO
contract is then called with the encoded output.
The _beforeSpend
hook function is overridden to check whether the UTXO has expired or not. It decodes the data from the UTXO to retrieve the expiration date, compares it with the current block timestamp, and throws an error if the expiration date has passed.
Overall, the ERC20UTXOExpirable
contract adds a valuable feature to the ERC20UTXO
contract by allowing UTXOs to expire after a certain period, providing additional security for users of the token.
Testing
The first test, “Spent UTXO”, transfers 10,000 tokens from the first user to the second user.
The second test, “Spend expire utxo”, mints 10,000 tokens to the account of the first user, creates a UTXO with that amount and the current time, and attempts to transfer 10,000 tokens from the UTXO to the second user’s account. However, before making the transfer, the test increases the time to when the UTXO is expired, decodes the expiration time from the UTXO data, and uses the decoded expiration time to check whether the UTXO is expired or not. If the UTXO has expired, the transfer should revert with the message “UTXO has been expired.”
Disclaimer: This code is a prototype and not intended for use in production environments. Use at your own risk.
Conclusion
Pros:
- Provides a more efficient and flexible approach to handling token transfers, as each token is represented by a separate UTXO with its expiration date.
- Allows for easy tracking of token ownership and expiration dates, improving transparency and security for token holders.
- Enables the creation more complex token-based applications, such as time-sensitive reward systems or lending platforms with expiration-based loan terms.
Cons:
- UTXO is not simply require heavy logic operations that directly affect the performance, and it increases gas used when execution.
(Hopefully, that different kind of EVM that’s layer2 provided today can reduce the execution cost) - Require more effort and understanding because of the complexity, it has to potentially introduce new bugs or vulnerabilities.
This a useful resource that inspired me to write prompt to ChatGPT and generate this article excluding smart contract code.