-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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-1238: Non-transferable Token Standard #5617
Changes from all commits
21604ad
e19df61
cfea1fd
144dede
31d37d3
6932730
f550072
2c6b810
a7f94be
a17e69d
8904cbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,231 @@ | ||||||
--- | ||||||
eip: 1238 | ||||||
title: Non-transferable Tokens | ||||||
description: An interface for fungible and non-fungible, non-transferable tokens. | ||||||
author: Raphael Roullet (@ra-phael), Chris Chung (@0xpApaSmURf), Nicola Greco (@nicola) | ||||||
discussions-to: https://ethereum-magicians.org/t/eip-1238-non-transferable-tokens/9044 | ||||||
status: Draft | ||||||
type: Standards Track | ||||||
category: ERC | ||||||
created: 2022-09-07 | ||||||
requires: 165, 712 | ||||||
--- | ||||||
|
||||||
## Abstract | ||||||
|
||||||
A _badge_ or non-transferable token (_NTT_) is a token that cannot be transferred once assigned. Badges can be accumulated through time and put at stake. Simply speaking, badges are statements about a public key: they can be quantitative (e.g. reputation, experience) or qualitative (badges, titles). | ||||||
|
||||||
The Non-Transferable Token standard defines a set of standard APIs allowing the identification of statements (called badges) attributed to a public key, such that different dapps and smart contract can use to filter users or to provide user with different badges different experiences. More importantly, this standard defines a way for which users can put their badges at stake. Badges cannot be transferred but can be lost (after staking) or can expire. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couple grammar and stylistic changes:
Suggested change
|
||||||
|
||||||
## Motivation | ||||||
|
||||||
The idea is to have tokens that once assigned cannot be transferred (like reputation) and that can be used by websites, or contracts to make users perform some actions. For example, if a user accumulates paper submissions at conferences, then they can use their paper badges to request grants. It's important that they can never share these badges. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think a badge can force a user to do something :P
Suggested change
|
||||||
|
||||||
This is the equivalent of a variety of other use cases | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
- Accumulating badges: degrees from academic institutions, paper publications for conferences, badges that allow for access in a building or in a special chat. | ||||||
- Experience points: points accumulated in a game, points accumulated by being honest in a decentralized system for some time or a DAO. | ||||||
- Statements: more broadly, any statement that is issued or signed by a contract, a dao, a single user that requires to be on-chain. | ||||||
- Subscription: badges can represent the validity of a paid subscription. | ||||||
|
||||||
Additionally, this standard could be used as a primitive for Soulbound tokens, whereby the recovery of a Soul would be done by obtaining signatures from co-owners of tokens with the same id. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there has been at least 4 attempts to propose a soul-bound / non-transferable NFT, I'd also suggest you add to your motivation and rationale on what this ERC has better merit than other competing ERCs e.g. https://eips.ethereum.org/EIPS/eip-5114 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
Multiple proposals for non-transferable / Soulbound tokens have been put forth, however this one presents a unique combination of features which includes having multiple token types in one contract (similar to [EIP-1155](./eip-1155.md)), recipient's consent and, optionally, token staking and expiry. | ||||||
|
||||||
## Specification | ||||||
|
||||||
Every contract compliant with this EIP must implement the following interface: | ||||||
|
||||||
```solidity | ||||||
interface IERC1238 is IERC165 { | ||||||
/** | ||||||
* @dev Emitted when some `amounts` of tokens with their respective `ids` are minted to `to` by `minter`. | ||||||
*/ | ||||||
event Minted(address indexed minter, address indexed to, uint256[] ids, uint256[] amounts); | ||||||
|
||||||
/** | ||||||
* @dev Emitted when some `amounts` of tokens with their respective `ids` are burned by `burner`. | ||||||
*/ | ||||||
event Burned(address indexed burner, address indexed owner, uint256[] ids, uint256[] amounts); | ||||||
|
||||||
/** | ||||||
* @dev Returns the amount of tokens of token type `id` owned by `account`. | ||||||
* | ||||||
* Requirements: | ||||||
* | ||||||
* - `account` cannot be the zero address. | ||||||
SamWilsn marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coming back to this, I think you should be more specific about what behaviour you expect. Perhaps:
Suggested change
|
||||||
*/ | ||||||
function balanceOf(address account, uint256 id) external view returns (uint256); | ||||||
|
||||||
/** | ||||||
* @dev Returns the balance of `account` for a batch of token `ids`. | ||||||
* | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might want to repeat the non-zero requirement here. |
||||||
*/ | ||||||
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); | ||||||
} | ||||||
``` | ||||||
|
||||||
In addition, in order for a contract to be compliant with this EIP, it MUST also abide by the following: | ||||||
|
||||||
- Implementers MUST NOT enable token transfers, i.e any update of the balances of an address must either come from burning the tokens they hold or being minted new tokens. | ||||||
- Implementers MUST NOT allow tokens to be transferred between addresses after they have been minted. | ||||||
- Implementers MUST allow token recipients to burn any token they receive. | ||||||
- Implementers MUST only allow tokens to be minted with the consent of the recipient. | ||||||
- Implementers MAY enable token issuers to burn the tokens they issued. | ||||||
|
||||||
**Smart contracts MUST implement all of the functions in the `ERC1238TokenReceiver` interface to accept tokens being minted to them.** | ||||||
|
||||||
The **URI Storage extension** is OPTIONAL for smart contracts that implement this EIP. This allows your contract to associate a unique URI for each token id. | ||||||
|
||||||
```solidity | ||||||
interface IERC1238URIStorage is IERC1238 { | ||||||
/** | ||||||
* @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 smart contracts that implement this EIP. This allows your contract to associate a expiry date for each token id. | ||||||
|
||||||
```solidity | ||||||
interface IERC1238Expirable is IERC1238 { | ||||||
/** | ||||||
* @dev Returns the expiry date for tokens with a given `id`. | ||||||
*/ | ||||||
function expiryDate(uint256 id) external view returns (uint256); | ||||||
|
||||||
/** | ||||||
* @dev Returns whether tokens are expired by comparing their expiry date with `block.timestamp`. | ||||||
*/ | ||||||
function isExpired(uint256 id) external view returns (bool); | ||||||
|
||||||
/** | ||||||
* @dev Sets the expiry date for the tokens with id `id`. | ||||||
*/ | ||||||
function setExpiryDate(uint256 id, uint256 date) external; | ||||||
|
||||||
/** | ||||||
* @dev [Batched] version of {setExpiryDate}. | ||||||
*/ | ||||||
function setBatchExpiryDates(uint256[] memory ids, uint256[] memory dates) external; | ||||||
} | ||||||
``` | ||||||
|
||||||
The **Holdable extension** is OPTIONAL for smart contracts that implement this EIP. This extension enables token owners to let their tokens be held by another address, including "staking" them in a smart contract. | ||||||
Since NTTs are non-transferable, the usual way of staking tokens by transferring ownership over them is not possible. In order to make staking possible, this extension introduces the notion of token holders, in addition to token owners: | ||||||
|
||||||
- When minting tokens to Alice's address A, address A is the token owner but also the initial token holder. Alice can then call the `entrust` function to entrust address B with her tokens. Address B becomes the token holder while Alice remains the token owner. | ||||||
- Only one address can hold specific tokens at a time. | ||||||
- A token holder can transfer tokens to another holder. Still, the important point is that this does not change ownership of the tokens. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be:
Suggested change
? |
||||||
- Because token holders can “transfer” tokens they hold (via the `entrust` function) to any address they choose, they can also send them to the zero address and have it hold these tokens forever. So token owners are effectively putting their tokens at stake with this mechanism. | ||||||
|
||||||
```solidity | ||||||
interface IERC1238Holdable is IERC1238 { | ||||||
/** | ||||||
* @dev Event emitted when `from` entrusts `to` with `amount` of tokens with token `id`. | ||||||
*/ | ||||||
event Entrust(address from, address to, uint256 indexed id, uint256 amount); | ||||||
|
||||||
/** | ||||||
* @dev Event emitted when tokens are burnt and the holder fails to acknowledge the burn. | ||||||
*/ | ||||||
event BurnAcknowledgmentFailed(address holder, address burner, address from, uint256 indexed id, uint256 amount); | ||||||
|
||||||
/** | ||||||
* @dev Returns the balance of a token holder for a given `id`. | ||||||
*/ | ||||||
function heldBalance(address holder, uint256 id) external view returns (uint256); | ||||||
|
||||||
/** | ||||||
* @dev Lets sender entrusts `to` with `amount` | ||||||
* of tokens, which gets transferred between their respective balances | ||||||
* of tokens held. | ||||||
*/ | ||||||
function entrust( | ||||||
address to, | ||||||
uint256 id, | ||||||
uint256 amount | ||||||
) external; | ||||||
} | ||||||
``` | ||||||
|
||||||
Smart contracts holding tokens MUST implement the following interface: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This standard really can't place requirements on smart contracts that don't claim to implement it. I'd reword this to something like:
|
||||||
|
||||||
```solidity | ||||||
/** | ||||||
* @dev Interface proposal for contracts that need to hold ERC1238 tokens. | ||||||
*/ | ||||||
interface IERC1238Holder is IERC1238Receiver { | ||||||
/** | ||||||
* @dev This function is called when tokens with id `id` are burnt. | ||||||
* Returns `true` as a sign that the burn was acknowledged and processed. | ||||||
*/ | ||||||
function onBurn(uint256 id, uint256 amount) external returns (bool); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an argument for making this |
||||||
} | ||||||
``` | ||||||
|
||||||
When burning tokens, implementers MUST try to call the `onBurn` function if the holder is a smart contract. This allows smart contract token holders to get notified when tokens that they hold are being burnt and gives them a chance to react and handle the situation as they see fit. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest expanding on what "try to call" means in this context. If |
||||||
|
||||||
## Rationale | ||||||
|
||||||
### Fungibility | ||||||
|
||||||
The proposed interface and implementation is heavily inspired from [EIP-1155](./eip-1155.md) which paved the way for managing multiple token types in just one smart contract. It draws from the lessons and prior discussions that emerged with this Multi Token standard and therefore also inherits from the design decisions that were made for it. For instance, `name` and `symbol` were left out from the smart contract interface in favour of more expressive definitions in a token metadata. | ||||||
|
||||||
This proposed interface and implementation chooses to stay agnostic regarding the fungibility of non-transferable tokens. What’s more, it recognises that in some cases it would make sense to have both fungible and non-fungible non-transferable tokens managed in the same contract. Let’s consider an online game issuing tokens. That game would issue both a non-transferable NFT when completing a level and non-transferable fungible tokens as experience points at the same time, in one transaction. | ||||||
|
||||||
EIP-1155 presents several interesting features that are also applicable to these tokens: | ||||||
|
||||||
1. There is no need to deploy multiple contracts for each token collection as with previous standards, this saves deployment gas cost | ||||||
2. EIP-1155 is fungibility-agnostic: the same smart contract can track both fungible tokens and NFTs | ||||||
3. Batch operations are possible such as minting or querying the balance for multiple token ids in just one call | ||||||
4. No more tokens stuck in contracts because they were sent by mistake. For transfers and batch transfers, if the recipient is a contract, the transaction will revert if it does not implement the `ERC1155TokenReceiver` interface | ||||||
5. Smart contracts which implement the `ERC1155TokenReceiver` interface may reject an increase in balance | ||||||
|
||||||
EIP-1155 conveniently supports **batch** operations, where a batch is represented by an array of token ids and an amount for each token id. | ||||||
However a batch often times only concerns one address. While minting a batch of tokens to an address in one transaction is convenient, we felt the need to support minting to multiple addresses in one transaction. | ||||||
As a result and keeping the strict definition that a batch is only for one address, this standard introduces the notion of a **bundle***.* A bundle is simply a collection of batches for multiple addresses. | ||||||
|
||||||
### Consent | ||||||
|
||||||
Discussions within the community have highlighted the risk of having unwanted, non-transferable tokens being issued to recipients without their consent, which could damage their reputation. | ||||||
|
||||||
In order to remedy to this, implementations of this standard must not let tokens being minted without approval from the recipient. In the case of a smart contract recipient, this is achieved via the `IERC1238Receiver` interface and returning a specific value to approve a minting. For recipients that are Externally Owned Accounts (EOAs), they must provide a [EIP-712](./eip-712.md) signature to approve token minting which gets passed to the smart contract when calling a mint function. | ||||||
|
||||||
## Backwards Compatibility | ||||||
|
||||||
Because of the inspiration drawn from EIP-1155, many concepts and methods remain identical, namely the concept of minting and burning tokens, a batch, the `ERC1238TokenReceiver`, the `balanceOf` and `balanceOfBatch` functions and to some extent the base extension for URI storage. Similarly, tokens whose owner is the zero address are considered burnt and invalid, therefore the zero address is not a valid input to call `balanceOf` with. | ||||||
|
||||||
Transfers events have been replaced by the `Minted` and `Burned` events whose names are less misleading in the context of this standard. | ||||||
|
||||||
|
||||||
## Security Considerations | ||||||
|
||||||
Given that the tokens are non-transferable, it becomes impossible to transfer them to another address in case of a suspected compromised account. Issuers should remain open to perform necessary checks on their end and allow tokens to be burnt and re-issued to someone. | ||||||
|
||||||
Additionally, the use of a smart contract wallet where key rotation for ownership is possible is highly recommended. | ||||||
|
||||||
Regarding privacy, even though this standard can be used for identity claims, issuers should refrain from using tokens to store any sensitive or personally identifiable information (given and family name, date of birth etc...) on-chain as this action cannot be undone. Issuers should warn users about the immutable and public nature of receiving a token, what it represents and must obtain the informed consent from recipients before minting a badge to them. | ||||||
|
||||||
## Copyright | ||||||
|
||||||
Copyright and related rights waived via [CC0](../LICENSE.md). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what "put at stake" means here. Is this some kind of staking mechanism, or are you talking about the badge issuer's reputation, or something else entirely?