EIP3525 - Semi-Fungible Token Standard
# Simple Summary
This is a standard for semi-fungible tokens. The set of smart contract interfaces described in this document defines an ERC-721 extension, by introducing an <ID, SLOT, AMOUNT> triple scalar model to represent the semi-fungible nature of a token. This standard also introduces an optional set of interfaces called 'UnderlyingContainer', for an ERC-3525 token to act as a representing layer of existing tokens, which is one of the key purpose of tokenization.
# Abstract
ERC-3525 is ERC-721 compatible, which means, as an ERC-721 token, each ERC-3525 token contains an ID property to identify itself as a universally unique entity. What empowers a ERC-3525 token is that it contains a 'units' property, representing the quantitative nature of the token. Thus, a ERC-3525 token can be split into several different ERC-3525 tokens, with certain properties maintained unchanged but the sum of the units of all split-out token equals that of the original one. Nevertheless, each ERC-3525 token has a 'SLOT' attribute, which labels its logical category. Several ERC-3525 tokens can be merged into one if their SLOT attributes indicate that they are of the same category.
# Motivation
Tokenization of assets is one of the most important applications in crypto. Normally there are two options when tokenizing assets: fungible and non-fungible. The first one generally uses the ERC-20 standard, in the case that every unit of assets is identical to each other, ERC-20 standard provides a flexible and efficient way to manipulate fungible tokens. The second one predominately uses the ERC-721 token standard, for that each asset needs to be described by one or more customized properties. For example, when a decentralized exchange that supports the Automatic Market-Making model allows its liquidity providers to specify their positions at different price ranges, an LP token can be implemented in ERC-721, since this token standard has the capability to identify each position as an entity, with different attributes for each entity.
Both options have significant drawbacks. In the fungible way, one needs to create a separate ERC-20 contract for each different value or combination of customizable properties, which can easily require an unacceptable number of ERC-20 contracts in practice. On the other hand, there is no quantitative feature in an ERC-721, hence significantly reducing the computability, liquidity, and manageability. For example, when we want to stake part of the position LP in some smart contract, the liquidity has to be withdrawn from the LP to create a new one, causes inconvenience and temporary decrease of liquidity.
An intuitive and direct way to solve the problem is to add a property to represent the quantitative nature directly to an ERC-721 token, making it best for both property customization and semi-fungibility. Furthermore, the ERC-721 compatibility would help the new standard easily utilize existing infrastructures and gain fast adoption.
For further design motivations, see papers and documents below:
Articles & Discussions
- How vnft can improve position management in uniswap-v3 (opens new window)
- What are digital assets (opens new window)
- Vnft tokens vs.erc-20 vs. erc-721 (opens new window)
# Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Related Standards
- ERC-721 Non-Fungible Token Standard
- ERC-165 Standard Interface Detection
- JSON Schema (opens new window)
- RFC 2119 Key words for use in RFCs to Indicate Requirement Levels (opens new window)
Every ERC-3525 compliant contract must implement the ERC3525, ERC721 and ERC165 interfaces
pragma solidity 0.7.6;
/**
* @title ERC-3525 Semi-Fungible Token Standard
* @dev See https://eips.ethereum.org/EIPS/eip-3525
* Note: the ERC-165 identifier for this interface is 0x1487d183.
*/
interface IERC3525 /* is ERC-721 */{
/**
* @dev This emits when partial units of a token are transferred to another.
* @param _from The address of the owner of `_tokenId`
* @param _to The address of the owner of `_targetTokenId`
* @param _tokenId The token to partially transfer
* @param _targetTokenId The token to receive the units transferred
@ @param _transferUnits The amount of units to transfer
*/
event TransferUnits(address indexed _from, address indexed _to, uint256 indexed _tokenId, uint256 _targetTokenId, uint256 _transferUnits);
/**
* @dev This emits when a token is split into two.
* @param _owner The address of the owner of both `_tokenId` and `_newTokenId`
* @param _tokenId The token to be split
* @param _newTokenId The new token created after split
@ @param _splitUnits The amount of units to be split from `_tokenId` to `_newTokenId`
*/
event Split(address indexed _owner, uint256 indexed _tokenId, uint256 _newTokenId, uint256 _splitUnits);
/**
* @dev This emits when a token is merged into another.
* @param _owner The address of the owner of both `_tokenId` and `_targetTokenId`
* @param _tokenId The token to be merged into `_targetTokenId`
* @param _targetTokenId The token to receive all units of `_tokenId`
@ @param _mergeUnits The amount of units to be merged from `_tokenId` to `_targetTokenId`
*/
event Merge(address indexed _owner, uint256 indexed _tokenId, uint256 indexed _targetTokenId, uint256 _mergeUnits);
/**
* @dev This emits when the approved units of the approved address for a token is set or changed.
* @param _owner The address of the owner of the token
* @param _approved The address of the approved operator
* @param _tokenId The token to approve
@ @param _approvalUnits The amount of approved units for the operator
*/
event ApprovalUnits(address indexed _owner, address indexed _approved, uint256 indexed _tokenId, uint256 _approvalUnits);
/**
* @dev Find the slot of a token.
* @param _tokenId The identifier for a token
* @return The slot of the token
*/
function slotOf(uint256 _tokenId) external view returns(uint256);
/**
* @dev Count all tokens holding the same slot.
* @param _slot The slot of which to count tokens
* @return The number of tokens of the specified slot
*/
function supplyOfSlot(uint256 _slot) external view returns (uint256);
/**
* @dev Find the number of decimals a token uses for units - e.g. 6, means the user representation of the units of a token can be calculated by dividing it by 1,000,000.
* @return The number of decimals for units of a token
*/
function unitDecimals() external view return (uint8);
/**
* @dev Enumerate all tokens of a slot.
* @param _slot The slot of which to enumerate tokens
* @param _index The index in the token list of the slot
* @return The id for the `_index`th token in the token list of the slot
*/
function tokenOfSlotByIndex(uint256 _slot, uint256 _index) external view returns (uint256);
/**
* @dev Find the amount of units of a token.
* @param _tokenId The token to query units
* @return The amount of units of `_tokenId`
*/
function unitsInToken(uint256 _tokenId) external view returns (uint256);
/**
* @dev Set or change the approved units of an operator for a token.
* @param _to The address of the operator to be approved
* @param _tokenId The token to approve
* @param _units The amount of approved units for the operator
*/
function approve(address _to, uint256 _tokenId, uint256 _units) external;
/**
* @dev Find the approved units of an operator for a token.
* @param _tokenId The token to find the operator for
* @param _spender The address of an operator
* @return The approved units of `_spender` for `_tokenId`
*/
function allowance(uint256 _tokenId, address _spender) external view returns (uint256);
/**
* @dev Split a token into several by separating its units and assigning each portion to a new created token.
* @param _tokenId The token to split
* @param _units The amounts to split, i.e., the units of the new tokens created after split
* @return The ids of the new tokens created after split
*/
function split(uint256 _tokenId, uint256[] calldata _units) external returns (uint256[] memory);
/**
* @dev Merge several tokens into one by merging their units into a target token before burning them.
* @param _tokenIds The tokens to merge
* @param _targetTokenId The token to receive all units of the merged tokens
*/
function merge(uint256[] calldata _tokenIds, uint256 _targetTokenId) external;
/**
* @dev Transfer units from a token to a newly created token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving ERC-3525 token units.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the newly created token
* @param _tokenId The token to partially transfer
* @param _units The amount of units to transfer
* @return The token created after transfer containing the transferred units
*/
function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _units) external returns (uint256);
/**
* @dev Transfer partial units of a token to a newly created token. If `_to` is a smart contract, this function MUST call `onERC3525Received` on `_to` after transferring and then verify the return value.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the newly created token
* @param _tokenId The token to partially transfer
* @param _units The amount of units to transfer
* @param _data
* @return The token created after transfer containing the transferred units
*/
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _units, bytes calldata _data) external returns (uint256);
/**
* @dev Transfer units from a token to another token. When transferring to a smart contract, the caller SHOULD check if the recipient is capable of receiving ERC-3525 token units.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the token to receive units
* @param _tokenId The token to transfer units from
* @param _targetTokenId The token to receive units
* @param _units The amount of units to transfer
*/
function transferFrom(address _from, address _to, uint256 _tokenId, uint256 _targetTokenId, uint256 _units) external;
/**
* @dev Transfer partial units of a token to an existing token. If `_to` is a smart contract, this function MUST call `onERC3525Received` on `_to` after transferring and then verify the return value.
* @param _from The address of the owner of the token to transfer
* @param _to The address of the owner the token to receive units
* @param _tokenId The token to partially transfer
* @param _targetTokenId The token to receive units
* @param _units The amount of units to transfer
* @param _data
*/
function safeTransferFrom(address _from, address _to, uint256 _tokenId, uint256 _targetTokenId, uint256 _units, bytes calldata _data) external;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# ERC-3525 Token Receiver
Smart contracts MUST implement all of the functions in the IERC3525Receiver interface to accept transfers. See “Safe Transfer Rules” for further detail.
/**
@notice Handle the receipt of a ERC-3525 token type.
@dev A ERC-3525 compliant smart contract MUST call this function on the token recipient contract, at the end of a `safeTransferFrom` after the balance has been updated.
This function MUST return `bytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xb382cdcd) if it accepts the transfer.
This function MUST revert if it rejects the transfer.
Return of any other value than the prescribed keccak256 generated value MUST result in the transaction being reverted by the caller.
@param operator The address which initiated the transfer (i.e. msg.sender)
@param from The address which previously owned the token
@param id The ID of the token being transferred
@param units The units of tokenId being transferred
@param data Additional data with no specified format
@return `bytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))`
Note: the ERC-165 identifier for this interface is 0xb382cdcd.
*/
interface IERC3525Receiver {
function onERC3525Received(address operator, address from, uint256 tokenId,
uint256 units, bytes calldata data) external returns (bytes4);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Token Manipulation
# Scenarios
Transfer:
An ERC-3525 token is ERC-721 compatible, it adds units transfer ability, that can transfer units from one token to another token.
The units level transfer has two types of interfaces, and both have safe and unsafe versions:
1. function transferFrom(address from, address to, uint256 tokenId, uint256 targetTokenId, uint256 units) external;
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 targetTokenId, uint256 units,
bytes calldata data) external;
2. function transferFrom(address from, address to, uint256 tokenId, uint256 units) external returns (uint256 newTokenId);
function safeTransferFrom(address from, address to, uint256 tokenId, uint256 units, bytes calldata data)
external returns (uint256 newTokenId);
2
3
4
5
6
7
8
9
The main difference between the two kinds of interface is whether the application or the contract is responsible for determining/generating the target token ID in the transfer.
Since transferring units of a token will possibly result in new token id creation, it's important to give the implementing contract the ability to do that. On the other hand, since part of a token can be transferred to a token with the same slot, we want to keep the flexibility for dapps to determine whether to use this ability, resulting in less contract complexity and less gas consumption.
Merge:
Several tokes with the same SLOT can be merged together using merge(uint256[] calldata tokenIds, uint256 targetTokenId);
. targetTokenId
should already exist, and cannot be one of tokenIds
. After merging, targetTokenId
owns all the units from the merged tokens, and the merged tokens will be burned.
Split:
One token can be split into several tokens, usingsplit(uint256 tokenId, uint256[] calldata units) returns (uint256[] memory newTokenIds);
. This will result in several newly generated tokens containing units equal to the parameter units
.
# Rules
approving rules:
- For being compatible with ERC721, there are three kinds of approving operations, which SHOULD be used to indicate different levels of approval.
setApprovalForAll
SHOULD indicate the top level of approval, the authorized operators are capable of handling all tokens, including their units, owned by the owner.- The ID level
approve
SHOULD indicate that the_tokenId
is approved to the operator, but not the units of that token. - The units level
approve
with the_units
parameter SHOULD indicate that only the specified amount of units are approved to the operator, but not the whole token. - Any
approve
MUST revert ifmsg.sender
is equal to_to
or_operator
. - The units level
approve
MUST revert ifmsg.sender
is not the owner of_tokenId
nor set approval for all tokens. - The units level
approve
MUST emit theApprovalUnits
event.
splitting rules:
- MUST revert if
_tokenId
is not a valid token. - MUST revert if
msg.sender
is neither the owner of_tokenId
nor set approval for all. - MUST revert if the sum of all
_units
exceeds the actual amount of units in_tokenId
. - MUST return an array containing the ids of the generated tokens after splitting.
- MUST emit the
Split
event.
merging rules:
- MUST revert if
_targetTokenId
or any of_tokenIds
is not a valid token. - MUST revert if the owner of
tokenId
is not the owner of_targetTokenId
. - MUST revert if
msg.sender
is neither the owner of all_tokenIds
andtargetTokenId
nor having been set approval for all. - MUST revert if any of
_tokenIds
is equal to_targetTokenId
. - Each of
_tokenIds
MUST be burnt after being merged. - MUST emit the
Merge
event.
transferFrom rules:
The
transferFrom
without the_targetTokenId
parameter SHOULD indicate transferring units to the recipient, whether the units are transferred into an existing token or a generated new token, should be decided by the implementation contract.- MUST revert unless
msg.sender
is the owner of_tokenId
, or having been set approval for all tokens, or having been approved for a certain number units of_tokenId
. - MUST revert if
_tokenId
is not a valid token. - MUST revert if
_from
is not the current owner of_tokenId
. - MUST revert if
_to
is the zero address. - MUST revert if the transfer amount exceeds the actual amount of units in
_tokenId
. - MUST revert if the transfer amount exceeds the approved units limit.
- MUST return the newly created token of the recipient containing the transferred units.
- MUST emit the
TransferUnits
event.
- MUST revert unless
The
transferFrom
with both the_units
and the_targetTokenId
parameters SHOULD indicate transferring units to an existing token of the recipient.- MUST revert unless
msg.sender
is the owner of_tokenId
, or having been set approval for all tokens, or the transfer amount is within the ERC-3525 approved units limit. - MUST revert if either
_tokenId
or_targetTokenId
is not a valid token. - MUST revert if
_from
is not the current owner of_tokenId
. - MUST revert if
_to
is not the current owner of_targetTokenId
. - MUST revert if
_to
is the zero address. - MUST revert if the transfer amount exceeds the actual amount of units in
_tokenId
. - MUST revert if the transfer amount exceeds the ERC-3525 approved units limit.
- MUST emit the ERC-3525 level
TransferUnits
event.
- MUST revert unless
safeTransferFrom rules:
safeTransferFrom
SHOULD be used to implement the same function astransferFrom
, with an extra step to check if the recipient is capable of receiving ERC-3525 token units by implementing theonERC3525Received
interface.- MUST obey the above rules set for
transferFrom
. - MUST check if
_to
is a smart contract (code size > 0). If so,safeTransferFrom
MUST callonERC3525Received
on_to
and MUST revert if the return value does not matchbytes4(keccak256("onERC3525Received(address,address,uint256,uint256,bytes)"))
# Metadata
# Metadata Extensions
ERC-3525 metadata extensions are compatible ERC-721 metadata extensions.
The optional ERC3525Metadata extension can be identified with the ERC-165 Standard Interface Detection.
pragma solidity ^0.7.6;
interface ERC3525Metadata /* is IERC721Metadata */ {
function contractURI() external view returns (string memory);
function slotURI(uint256 slot_) external view returns (string memory);
}
2
3
4
5
6
# ERC-3525 Metadata URI JSON Schema
This is the "ERC-3525 Metadata JSON Schema for contractURI()" referenced above.
{
"title": "Contract Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Contract Name"
},
"description": {
"type": "string",
"description": "Describes the contract "
},
"unitDecimals": {
"type": "integer",
"description": "The number of decimal places that the units should display - e.g. 18, means to divide the token units by 1000000000000000000 to get its user representation."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
This is the "ERC-3525 Metadata JSON Schema for slotURI(uint)" referenced above.
{
"title": "Slot Metadata",
"type": "object",
"properties": {
"tokensInSlot": {
"type": "integer",
"description": "Number of tokens in this slot, i.e., the number of tokens that have same business attributes represented by the slot."
},
"unitsInSlot": {
"type": "integer",
"description": "Total units in the slot, since tokens in same slot are fungible, this value equals to the sum of units of all tokens in this slot."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
This is the "ERC-3525 Metadata JSON Schema for tokenURI(uint)" referenced above.
{
"title": "Token Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents."
},
"units": {
"type": "BigNumber",
"description": ""
},
"slot": {
"type": "BigNumber",
"description": "The id of the slot that this token belongs to."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Approval
ERC-3525 adds a new approval model, that is, one can approve operators to transfer units from a token with certain ID, the new interface is:
function approve(address to, uint256 tokenId, uint256 units);
# Rationale
# Metadata generation
Since ERC-3525 is designed for representing underlying assets, rather than artifacts for gaming or arts, the implementation should give out the metadata directly from contract code, rather than give a URL of a server for returning metadata.
# Design decision: Keep unsafe transfer
There are mainly two reasons we keep the unsafe transfer interfaces:
- Since ERC-3525 token is ERC-721 compatible, we must keep compatibility for all wallets and contracts that are still calling unsafe transfer interfaces for ERC-721 tokens.
- We want to keep the ability that dapps can trigger business logic on contracts by simply transferring ERC-3525 tokens to them, that is, a contract can put business logic in
onERC3525Received
function so that it can be called whenever a token is transferred usingsafeTransferFrom
. However, in this situation, an approved contract with customized transfer functions like deposit etc. SHOULD never callsafeTransferFrom
since it will result in confusion that whetheronERC3525Received
is called by itself or other dapps that safe transfer a token to it.
# Approval
For maximum semantical compatibility with ERC-721, as well as simplifying the approval model, we decided to make the relationship between two levels of approval like that:
- Approval of an id does not result in the ability to partial transfer units from this id by the approved operator;
- Approval of all units in a token does not result in the ability to transfer the token entity by the approved operator;
setApprovalForAll
will result in the ability to transfer any tokens from the owner, as well as the ability to partial transfer units from any token.setApprovalForAll
will result in the ability to approve any tokens of the owner to third parties, as well as the ability to approve partial transfer units of any token to third parties.
# Backwards Compatibility
As mentioned at the very beginning, a ERC-3525 is an extension interface of ERC721, hence it is 100% compatible with ERC-721.
# Reference Implementation
# Copyright
Copyright and related rights waived via CC0 (opens new window).