ERC-3525 Semi-fungible token

Final · NFTs & multi-token · created 2020-12-01

A semi-fungible token standard that adds a slot and a value to ERC-721-compatible tokens, so that tokens sharing the same slot can have their values split and exchanged.

Why it is needed

Ethereum tokens have so far been split into two camps. ERC-20 is a fungible token in which every unit is identical, and ERC-721 is a non-fungible token (NFT) in which each token is distinct. However, ERC-20 requires a separate new contract for each asset with a different data structure or combination of attributes, so in practice a huge number of ERC-20 contracts get created. Conversely, ERC-721 has no concept of quantity, which is a major disadvantage for calculation, liquidity and management. For example, if financial products such as bonds, insurance policies or vesting plans are built as ERC-721, there is no standard interface for handling the value inside the token, so it is impossible to transfer just part of the stake the token represents. This calls for a semi-fungible token that combines the quantity feature of ERC-20 with the individual attributes of ERC-721. Making it backward compatible with ERC-721 lets it reuse infrastructure that is already in use, which also speeds up adoption.

What to implement

In depth

The ID, SLOT, VALUE three-element model

Each token has an ID equivalent to ERC-721, so it is identified as a globally unique entity and can be transferred and approved in an ERC-721 compatible way. In addition, each token has a value property that represents a quantity, and its meaning is very similar to the balance of an ERC-20 token. Each token also has a slot property, and the values of two tokens with the same slot are treated as fungible with each other. The query functions follow this structure. balanceOf(uint256 _tokenId) returns the value of a token, slotOf(uint256 _tokenId) returns the slot of a token, and valueDecimals() returns the number of decimals of value. For wallet compatibility, the name valueDecimals() was chosen so that it does not overlap with decimals() in ERC-20. When a token's slot changes, a SlotChanged event must be emitted. Every compliant contract must implement the ERC-721 and ERC-165 interfaces together with ERC-3525, and the ERC-165 identifier of the base interface is 0xd5358140.

Two value transfer models

Two kinds of transfer are added on top of ERC-721 compatible token transfers. First, transferFrom(_fromTokenId, _toTokenId, _value) moves value between tokens in the same slot. The caller must be the owner of _fromTokenId, an operator with full authority, or an operator approved for the whole token or at least _value. It must revert if either token is 0 or does not exist, if the two tokens have different slots, or if _value exceeds the balance or approval limit, and it must emit a TransferValue event. Second, transferFrom(_fromTokenId, _to, _value) sends value from a token to an address. In practice, it finds a token in the same slot owned by _to, or creates one, has that token receive the value, and returns the ID of the receiving token. It must revert if _to is the zero address, and it must emit both the Transfer and TransferValue events. In both cases, if the recipient is a contract, the implementation checks whether onERC3525Received exists, calls it if it does, and must revert if the result is not 0x009ce20b.

Four-level approval model

Approval is divided into four levels. setApprovalForAll is ERC-721 compatible and is a full approval that can manage all of the owner's tokens and their value. setApprovalForSlot (optional) is a slot approval that manages all tokens in a specified slot. The ERC-721 compatible approve is a token ID approval that manages one specific token ID and its value, and approve(_tokenId, _operator, _value) is a value approval that manages only up to a specified maximum value on a specific token. For any approval function, the caller must be the owner or someone approved at a higher level of authority. The relationships between levels are also defined. An operator approved for a token ID can partially transfer value from that token, but conversely, being approved for the full value of a token does not mean the operator can transfer the token entity itself. With setApprovalForAll, an operator can partially transfer value from any token and can also approve a third party to partially transfer value.

Check, notify and respond instead of safe transfer

With the safe transfer of the ERC-721 family, the sender has to choose between a safe and an unsafe transfer method, but senders often do not know which to use, and calling safeTransferFrom can make a transfer fail merely because the receiving contract has not implemented the callback. This standard instead uses a Check, Notify and Response approach. There is no separate safeTransferFrom, so only one kind of transfer function needs to be called, and every ERC-3525 contract must check whether the receiving contract has onERC3525Received and must call it if it exists. The receiving contract returns 0x009ce20b if it accepts the transfer, and returns a different value or reverts if it rejects it. Even when value is sent to the contract itself, onERC3525Received is called, so a contract can apply different acceptance rules to self-transfers and external receipts.

Metadata and the on-chain JSON recommendation

The metadata extension is compatible with the ERC-721 metadata extension and adds contractURI() for the whole contract and slotURI(uint256) for each slot. Both functions are recommended to return a JSON-format URI that starts with a data:application/json; header. Because data stored in a smart contract is more trustworthy than data stored in a centralized system, the standard recommends that contractURI, slotURI and tokenURI all return JSON directly instead of an external URL. The contract metadata schema has name, description, image, external_link and valueDecimals, and the properties entry of slot metadata has an is_intrinsic field. Best practice is to build the slot value as a keccak256 hash of several attributes; if an attribute was used in the computation, is_intrinsic is marked TRUE and order records the order in which it appeared in the computation. Because this standard is designed for financial products rather than collectibles or game items, it strongly recommends generating metadata directly in contract code for transparency and safety.

Optional extensions: slot enumeration and slot approval

There are two optional extensions. The slot enumeration extension (IERC3525SlotEnumerable, ERC-165 identifier 0x3b741b9e) exposes the full list of slots a contract holds so they can be explored. slotCount() queries the total number of slots, slotByIndex(_index) the slot at each index, tokenSupplyInSlot(_slot) the total number of tokens in that slot, and tokenInSlotByIndex(_slot, _index) the token ID at each index within that slot. The slot approval extension (IERC3525SlotApprovable, identifier 0xb688be58) lets an operator manage all of an owner's tokens in the same slot. The caller of setApprovalForSlot must be the owner or an operator authorized through setApprovalForAll, and an ApprovalForSlot event must be emitted when the approval changes. isApprovedForSlot queries whether an approval exists.

Things to watch out for

Original ERC · ← ERCs