credit: https://imagesvc.meredithcorp.io/

ERC20 with an expiration feature

sirawt

--

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

ERC20UTXO interface

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

sorry if my code sucks

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.”

Testing result

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.

--

--

sirawt
sirawt

Written by sirawt

0x91d9 Blockchain & Distributed Technology enthusiast

No responses yet