Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP: Redeemable Tokens #6732

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 296 additions & 0 deletions EIPS/eip-6732.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
---
eip: 6732
title: Redeemable Tokens
description: A proposition for redeemable tokens
lucazffz marked this conversation as resolved.
Show resolved Hide resolved
author: Lucaz Lindgren (@LucazFFz)
discussions-to: https://ethereum-magicians.org/t/eip-6732-redeemable-tokens/13441
status: Draft
type: Standards Track
category: ERC
created: 2023-03-20
requires: 165
---

## Abstract

This ERC outlines a smart contract interface that can represent any number of fungible and non-fungible redeemable token types. The standard builds upon the [ERC-1155](./eip-1155.md) standard borrowing many of the ideas introduced by it including support for multiple tokens within the same contract and batch operations.

Contrary to the ERC-1155 standard, this ERC does not enforce transferability as it recognizes situations where implementers might not want to allow it. Additionally, it introduces several extensions used to expand the functionality like the **Expirable extension** which provides a simple way to add an expiry date to tokens.
lucazffz marked this conversation as resolved.
Show resolved Hide resolved

## Motivation

The core idea of this ERC is to standardize and simplify the development of redeemable tokens (tickets) on the Ethereum blockchain. The tokens can be redeemed in exchange for currency or access (the reward for redeeming is left to implementers). The tokens can be exchanged for real-world or digital rewards.

Example of use-cases for redeemable tokens:

- **General admission tickets** - Tickets allowing attendees entry to the event together with access to the regular event activities. This type of ticket could be fungible meaning they are interchangeable. The event can be held either physically or digitally.
- **VIP tickets** - All-access tickets that include everything the event offers with activities excluded from the general admission tickets to give the attendees a premium experience. These tickets can be non-fungible making every ticket unique and allowing each ticket to be connected to an individual artwork or likewise to incentivize purchasing VIP tickets instead.

Additionally, these tokens can be used to create, among others, vouchers, gift cards, and scratchcards.

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

**Every smart contract implementing this ERC MUST implement all of the functions in the `IRedeemableToken` interface. Additionally, they MUST implement the [ERC-165](./eip-165.md) `supportsInterface` function and MUST return the constant value `true` if the constant value `0x9d1da9d1` is passed through the `interfaceId` argument.**

```solidity
interface IRedeemableToken is IERC165 {
lucazffz marked this conversation as resolved.
Show resolved Hide resolved
/**
* @dev Emitted when `amount` tokens of token type `id` are minted to `to` by `minter`.
*/
event MintSingle(address indexed minter, address indexed to, uint256 indexed id, uint256 amount);

/**
* @dev Equivalent to multiple {MintSingle} events, where `minter` and `to` is the same for all token types.
*/
event MintBatch(address indexed minter, address indexed to, uint256[] ids, uint256[] amounts);

/**
* @dev Emitted when `amount` tokens of token type `id` owned by `owner` are burned by `burner`.
*/
event BurnSingle(address indexed burner, address indexed owner, uint256 indexed id, uint256 amount);

/**
* @dev Equivalent to multiple {BurnSingle} events, where `owner` and `burner` is the same for all token types.
*/
event BurnBatch(address indexed burner, address indexed owner, uint256[] ids, uint256[] amounts);

/**
* @dev Emitted when `amount` of tokens of token type `id` are redeemed by `account`.
*/
event Redeem(address indexed account, uint256 indexed id, uint256 amount);

/**
* @dev Returns the balance of tokens of token type `id` owned by `account`.
*
* Requirements:
* - `account` cannot be the zero address.
*/
function balanceOf(address account, uint256 id) external view returns (uint256);

/**
* @dev Returns the balance of `account` for a batch of token `ids`.
*/
function balanceOfBatch(address account, uint256[] calldata ids) external view returns (uint256[] memory);

/**
* @dev Returns the balance of multiple `accounts` for a batch of token `ids`.
* This is equivalent to calling {balanceOfBatch} for several accounts in just one call.
*
* Requirements:
* - `accounts` and `ids` must have the same length.
*/
function balanceOfBundle(address[] calldata accounts, uint256[][] calldata ids)
external
view
returns (uint256[][] memory);

/**
* @dev Returns the balance of tokens of token type `id` redeemed by `account`.
*
* Requirements:
* - `account` cannot be the zero address.
*/
function balanceOfRedeemed(address account, uint256 id) external view returns (uint256);

/**
* @dev Returns the balance of `account` for a batch of redeemed token `ids`.
*/
function balanceOfRedeemedBatch(address account, uint256[] calldata ids) external view returns (uint256[] memory);

/**
* @dev Returns the balance of multiple `accounts` for a batch of redeemed token `ids`.
* This is equivalent to calling {balanceOfRedeemedBatch} for several accounts in just one call.
*
* Requirements:
* - `accounts` and `ids` must have the same length.
*/
function balanceOfRedeemedBundle(address[] calldata accounts, uint256[][] calldata ids)
external
view
returns (uint256[][] memory);

/**
* Redeem `amount` of token type `id` owned by `account`.
*
* Requirements:
* - `account` cannot be the zero address.
* - `amount` together with `account` balance of redeemed token of token type `id`
* cannot exceed `account` balance of token type `id`.
*/
function redeem (address account, uint256 id, uint256 amount, bytes memory data) external;
}
```

In addition, in order for a contract to be compliant with this ERC, it MUST abide by the following:

- Implementers MAY enable token transfers to support functionality such as ticket reselling.
- Implementers MUST NOT allow tokens to be transferred between addresses after they have been redeemed.
- Implementers MUST allow token holders to redeem their tokens.
- Implementers MUST NOT allow token issuers to redeem the tokens they have issued.
- Implementers SHOULD allow token recipients to burn any token they receive.
- Implementers MAY enable token issuers to burn the tokens they issued.

**Smart contracts MUST implement all of the functions in the `IRedeemableTokenReceiver` interface to accept tokens being minted to them.**

The **URI Storage extension** is OPTIONAL for compliant smart contracts. This allows contracts to associate a unique URI for each token ID.

```solidity
interface IRedeemableTokenURIStorage is IRedeemableToken {
/**
* @dev Emitted when the URI for token type `id` changes to `value`, if it is a non-programmatic URI.
*/
event URI(uint256 indexed id, string value);

/**
* @dev Returns the Uniform Resource Identifier (URI) for `id` token.
*/
function tokenURI(uint256 id) external view returns (string memory);
}
```

The **Expirable extension** is OPTIONAL for compliant smart contracts. This allows contracts to associate a unique expiry date for each token ID. Smart contracts implementing the `RedeemableTokenExpirable` extension MUST abide by the following:

- Implementers MUST NOT allow tokens to be minted or redeemed if expired.

```solidity
interface IRedeemableTokenExpirable is IRedeemableToken {
/**
* @dev Sets the expiry date for the token of token type `id`.
*/
function setExpiryDate(uint256 id, uint256 date) external;

/**
* @dev [Batched] version of {setExpiryDate}.
*/
function setBatchExpiryDates(uint256[] memory ids, uint256[] memory dates) external;

/**
* @dev Returns the expiry date for the token of token type `id`.
*/
function expiryDate(uint256 id) external view returns (uint256);

/**
* @dev Returns `true` or `false` depending on if the token of token type `id` has expired
* by comparing the expiry date with `block.timestamp`.
*/
function isExpired(uint256 id) external view returns (bool);
}
```

The **Supply extension** is OPTIONAL for compliant smart contracts. This allows contracts to associate a unique max supply for each token ID. Smart contracts implementing the `RedeemableTokenSupply` extension MUST abide by the following:

- Implementers SHOULD NOT allow tokens to be minted if total supply exceeds max supply.
- Implementers SHOULD increment total supply upon minting and decrement upon burning.
- Implementers are RECOMMENDED to override the `_beforeMint` hook to increment total supply upon minting and decrement upon burning.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _beforeMint function is part of the reference implementation, not the specification itself. The Specification section should be understandable on its own without the other sections, while the reference implementation should help to clarify what's in the the Specification section.

Suggested change
- Implementers are RECOMMENDED to override the `_beforeMint` hook to increment total supply upon minting and decrement upon burning.


```solidity
interface IRedeemableTokenSupply is IRedeemableToken {
/**
* @dev Sets the max supply for the token of token type `id`.
*/
function setMaxSupply(uint256 id, uint256 amount) external;

/**
* @dev [Batched] version of {setMaxSupply}.
*/
function setBatchMaxSupplies(uint256[] memory ids, uint256[] memory amounts) external;

/**
* @dev Returns the total supply for token of token type `id`.
*/
function totalSupply(uint256 id) external view returns (uint256);

/**
* @dev Returns the max supply for token of token type `id`.
*/
function maxSupply(uint256 id) external view returns (uint256);

/**
* @dev Indicates whether any token of token type `id` exists, or not.
*/
function exists(uint256 id) external view returns (bool);
}
```

The **Payable extension** is OPTIONAL for compliant smart contracts. This allows contracts to associate a unique price for each token ID. Smart contracts implementing the `RedeemableTokenPayable` extension MUST abide by the following:

- Implementers SHOULD require recipients to provide funding equal to the token price.

```solidity
interface IRedeemableTokenPayable is IRedeemableToken {
/**
* @dev Sets the price `amount` for the token of token type `id`.
*/
function setPrice(uint256 id, uint256 amount) external;

/**
* @dev [Batched] version of {setPrice}.
*/
function setBatchPrices(uint256[] memory ids, uint256[] memory amounts) external;

/**
* @dev Returns the price for the token type `id`.
*/
function price(uint256 id) external view returns (uint256);
}
```

## Rationale

### Overview

The proposed interface and implementation draw heavy inspiration from ERC-1155 which paved the way for managing multiple token types in the same smart contract reducing gas costs. It draws from the lessons and prior discussions which emerged from the standard and therefore inherits from its design and structure.

ERC-1155 presents multiple interesting features also applicable to redeemable tokens:

- Because one contract is able to hold multiple different token types, there is no need to deploy multiple contracts for each collection as with previous standards. This saves on deployment gas costs as well as reduces redundant bytecode on the Ethereum blockchain.
- ERC-1155 is fungibility-agnostic which means the same contract can manage both fungible and non-fungible token types.
- Batch operations are supported making it possible to mint or query the balance for multiple token ids within the same call.
- Eliminates the possibility to lock tokens in smart contracts due to the requirement for smart contracts to implement the `ERC1155TokenReceiver` interface.
- Smart contracts implementing the `ERC1155TokenReceiver` interface may reject an increase in balance.

This standard does not assume who has the power to mint/burn tokens nor under what condition the actions can occur. However, it requires that the token holders have the ability to redeem their tokens.

### Fungibility

This ERC, likewise ERC-1155, chooses to stay agnostic regarding the fungibility of redeemable Tokens. It recognizes that it may be useful in some cases to have both fungible and non-fungible redeemable tokens managed in the same contract.
lucazffz marked this conversation as resolved.
Show resolved Hide resolved

One way to achieve non-fungible tokens is to utilize split ID bits reserving the top 128 bits of the 256 bits `id` parameter to represent the base token ID while using the bottom 128 bits to represent the index of the non-fungible to make it unique.

Alternatively, a more intuitive approach to represent non-fungible tokens is to allow a maximum value of 1 for each. This would reflect the real world, where unique items have a quantity of 1 and fungible items have a quantity greater than 1.

### Batch Operations

ERC-1155, likewise this ERC, conveniently supports batch operations, where a batch is represented by an array of token IDs and an amount for each token ID making it possible to for example mint multiple token IDs in one call.

However, a batch often time only concerns one address. While minting a batch of tokens to an address in one transaction is convenient, supporting the ability to mint to multiple addresses in one transaction is even more convenient. Therefore, this ERC supports bundle operations. A bundle is simply a collection of batches where each batch has a separate address.

### Transfer

This ERC does not enforce transferability between addresses except for minting and burning. However, it recognizes the possible need for it when for example implementing resell functionality and, therefore, does not prevent it.

### Redeem

The core addition to redeemable tokens is the possibility to redeem tokens in exchange for currency, access, etc. As tokens only can be redeemed by their holders a possible `redeemBatch` function has been excluded as it is not expected that a holder would want to redeem multiple token types simultaneously.

### Safe Mint Only

This ERC takes inspiration from the ERC-1155 standard and only supports safe-style transfers when minting tokens. This enables receiver contracts to depend on `RedeemableTokenMint` or `RedeemableTokenMintBatch` functions to be called at the end of minting. This allows receivers to reject an increase in balance.

## Backwards Compatibility

Because of the heavy inspiration from the ERC-1155 standard many concepts and methods remain identical, namely the concept of a batch, the `balanceOf` and `balanceOfBatch` functions, and to some extent the URI Storage and Supply extensions.

## Reference Implementation

- [Reference implementation](../assets/eip-6732/)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown renderer probably won't let you link to the whole directory. We normally recommend linking to the readme.

Copy link
Author

@lucazffz lucazffz Apr 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the reference implementation's readme? If I should remove the migrations and slim the implementation down I suppose I should also remove the reference implementation's readme since it's not necessary at that point. Correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, if you cram the whole reference implementation into a single file, just link to that. If not, you can make a README.md that's just a manual listing of files:

# ERC-XXXX Reference Implementation

 * [SomeContract.sol](./SomeContract.sol)
 * [MagicInterface.sol](./MagicInterface.sol)


## Security Considerations

Needs discussion.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
2 changes: 2 additions & 0 deletions assets/eip-6732/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# directories
build/*
95 changes: 95 additions & 0 deletions assets/eip-6732/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# [ERC4365] Redeemable Tokens (Tickets) Reference Implementation

## Core

{{IERC4365}}
{{ERC4365}}
{{IERC4365Receiver}}

## Extensions

{{ERC4365Expirable}}
{{ERC4365Payable}}
{{ERC4365Supply}}
{{ERC4365URIStorage}}
{{IERC4365Expirable}}
{{IERC4365Payable}}
{{IERC4365Supply}}

## Usage

### Prerequisites

To run this code you first need to install Truffle via npm (node package manager). If you do not already have npm installed,
you can install it [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm#using-a-node-version-manager-to-install-nodejs-and-npm).

To install Truffle, run:

```sh
npm install -g truffle
```

If you want to use Ganache to host a personal blockchain, download it [here](https://trufflesuite.com/ganache/).

### Compile

If you want to compile, simply run:

```sh
truffle compile
```

### Test

To run all tests, simply run:

```sh
truffle test
```

You can also run each test individually by calling:

```sh
truffle test {test-location}
```

### Deploy

To deploy the `Ticket.sol` example smart contract, you need to connect to a blockchain. Truffle has a built-in personal blockchain used for testing. Altertently, if you installed
Ganache, you can use that.

If you want to use Truffles built-in blockchain, run:

```sh
truffle develop
```

If you want to use Ganache, run:

```sh
truffle deploy
```

However, dont forget to link Ganache to the project (tutorial [here](https://trufflesuite.com/docs/ganache/how-to/link-a-truffle-project/)), alternatively create a Ganache quickstart workspace and match the server host and port in `truffle-config.js`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to remove external links from your reference implementation as well.


## Syntax Highlighting

If you use VSCode, you can enjoy syntax highlighting for your Solidity code ia the [vscode-solidity](https://github.com/juanfranblanco/vscode-solidity) extension. It is also
available via the VSCode extensions marketplace. The recommended approach to set the compiler version is to add the following fields to your VSCode user settings:

```json
{
"solidity.compileUsingRemoteVersion": "v0.8.4+commit.c7e474f2",
"solidity.defaultCompiler": "remote"
}
```

Where `v0.8.4+commit.c7e474f2` can be replaced by any other version.

## Todo

- [ ] Add tests

## Credits

- EIP inspirations: [EIP-1155: Multi Token Standard](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this file please.

Empty file.
Loading