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
event TransferValue(uint256 indexed _fromTokenId, uint256 indexed _toTokenId, uint256 _value);: This event is emitted when value moves between tokens in the same slot. It must also be emitted for zero-value transfers and for token creation (_fromTokenId is 0) and destruction (_toTokenId is 0).event ApprovalValue(uint256 indexed _tokenId, address indexed _operator, uint256 _value);: This event is emitted when the value approval limit of a token is set or changed.event SlotChanged(uint256 indexed _tokenId, uint256 indexed _oldSlot, uint256 indexed _newSlot);: This event is emitted when the slot of a token is set or changed.function valueDecimals() external view returns (uint8);: Returns the number of decimals used to display value. The name valueDecimals() was chosen so that it does not conflict with decimals() in ERC-20.function balanceOf(uint256 _tokenId) external view returns (uint256);: Queries the value held by a token ID.function slotOf(uint256 _tokenId) external view returns (uint256);: Queries the slot a token belongs to.function allowance(uint256 _tokenId, address _operator) external view returns (uint256);: Queries the maximum value an operator can manage on the given token.function transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value) external payable;: Moves value between two tokens in the same slot. It must revert if the slots differ or if the amount exceeds the balance or approval limit.function slotCount() external view returns (uint256);: (Optional extension) Returns the total number of slots stored in the contract.function slotByIndex(uint256 _index) external view returns (uint256);: (Optional extension) Returns the slot at the given index in the list of all slots.function tokenSupplyInSlot(uint256 _slot) external view returns (uint256);: (Optional extension) Returns the total number of tokens that have the same slot.function tokenInSlotByIndex(uint256 _slot, uint256 _index) external view returns (uint256);: (Optional extension) Returns the token ID at the given index in the token list of that slot.event ApprovalForSlot(address indexed _owner, uint256 indexed _slot, address indexed _operator, bool _approved);: (Optional extension) This event is emitted when operator approval is turned on or off for all of an owner's tokens in a particular slot.function onERC3525Received(address _operator, uint256 _fromTokenId, uint256 _toTokenId, uint256 _value, bytes calldata _data) external returns (bytes4);: This is the receive notification function implemented by a contract that receives value. It returns 0x009ce20b if it accepts the transfer, and reverts or returns a different value if it rejects it.function contractURI() external view returns (string memory);: (Optional extension) Returns the metadata URI of the whole contract in JSON format.function slotURI(uint256 _slot) external view returns (string memory);: (Optional extension) Returns the metadata URI of a specific slot in JSON format.
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
- Value and slot approvals are separate from ERC-721 approval: Value-level approval and slot-level approval (optional) are isolated from the ERC-721 approval model, so approving value must not affect ERC-721 level approval. Implementations must follow this principle.
- ERC-721 compatibility means no risk of asset loss: Because this standard is compatible with ERC-721, any wallet or smart contract that can hold and handle standard ERC-721 tokens has no risk of losing ERC-3525 tokens due to a standard-incompatible implementation.