Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-4675: Multi-Fractional Non-Fungible Tokens

Fractionalize multiple NFTs using a single contract

Authors David Kim (@powerstream3604)
Created 2022-01-13
Discussion Link https://ethereum-magicians.org/t/eip-4675-multi-fractional-non-fungible-token-standard/8008
Requires EIP-165, EIP-721

Abstract

This standard outlines a smart contract interface eligible to represent any number of fractionalized non-fungible tokens. Existing projects utilizing standards like EIP-1633 conventionally deploy separate EIP-20 compatible token contracts to fractionalize the non-fungible token into EIP-20 tokens. In contrast, this ERC allows each token ID to represent a token type representing(fractionalizing) the non-fungible token.

This standard is approximate in terms of using _id for distinguishing token types. However, this ERC has a clear difference with EIP-1155 as each _id represents a distinct NFT.

Motivation

The conventional fractionalization process of fractionalizing a NFT to FT requires deployment of a FT token contract representing the ownership of NFT. This leads to inefficient bytecode usage on Ethereum Blockchain and limits functionalities since each token contract is separated into its own permissioned address. With the rise of multiple NFT projects needing to fractionalize NFT to FT, new type of token standard is needed to back up them.

Specification

/**
    @title Multi-Fractional Non-Fungible Token Standard
    @dev Note : The ERC-165 identifier for this interface is 0x83f5d35f.
*/
interface IMFNFT {
    /**
        @dev This emits when ownership of any token changes by any mechanism.
        The `_from` argument MUST be the address of an account/contract sending the token.
        The `_to` argument MUST be the address of an account/contract receiving the token.
        The `_id` argument MUST be the token type being transferred. (represents NFT)
        The `_value` argument MUST be the number of tokens the holder balance is decrease by and match the recipient balance is increased by.
    */
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _id, uint256 _value);

    /**
        @dev This emits when the approved address for token is changed or reaffirmed.
        The `_owner` argument MUST be the address of account/contract approving to withdraw.
        The `_spender` argument MUST be the address of account/contract approved to withdraw from the `_owner` balance.
        The `_id` argument MUST be the token type being transferred. (represents NFT)
        The `_value` argument MUST be the number of tokens the `_approved` is able to withdraw from `_owner` balance.
    */
    event Approval(address indexed _owner, address indexed _spender, uint256 indexed _id, uint256 _value);

    /**
        @dev This emits when new token type is added which represents the share of the Non-Fungible Token.
        The `_parentToken` argument MUST be the address of the Non-Fungible Token contract.
        The `_parentTokenId` argument MUST be the token ID of the Non-Fungible Token.
        The `_id` argument MUST be the token type being added. (represents NFT)
        The `_totalSupply` argument MUST be the number of total token supply of the token type.
    */
    event TokenAddition(address indexed _parentToken, uint256 indexed _parentTokenId, uint256 _id, uint256 _totalSupply);

    /**
        @notice Transfers `_value` amount of an `_id` from the msg.sender address to the `_to` address specified
        @dev msg.sender must have sufficient balance to handle the tokens being transferred out of the account.
        MUST revert if `_to` is the zero address.
        MUST revert if balance of msg.sender for token `_id` is lower than the `_value` being transferred.
        MUST revert on any other error.
        MUST emit the `Transfer` event to reflect the balance change.
        @param _to      Source address
        @param _id      ID of the token type
        @param _value   Transfer amount
        @return         True if transfer was successful, false if not
    */
    function transfer(address _to, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Approves `_value` amount of an `_id` from the msg.sender to the `_spender` address specified.
        @dev msg.sender must have sufficient balance to handle the tokens when the `_spender` wants to transfer the token on behalf.
        MUST revert if `_spender` is the zero address.
        MUST revert on any other error.
        MUST emit the `Approval` event.
        @param _spender Spender address(account/contract which can withdraw token on behalf of msg.sender)
        @param _id      ID of the token type
        @param _value   Approval amount
        @return         True if approval was successful, false if not
    */
    function approve(address _spender, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Transfers `_value` amount of an `_id` from the `_from` address to the `_to` address specified.
        @dev Caller must be approved to manage the tokens being transferred out of the `_from` account.
        MUST revert if `_to` is the zero address.
        MUST revert if balance of holder for token `_id` is lower than the `_value` sent.
        MUST revert on any other error.
        MUST emit `Transfer` event to reflect the balance change.
        @param _from    Source address
        @param _to      Target Address
        @param _id      ID of the token type
        @param _value   Transfer amount
        @return         True if transfer was successful, false if not

    */
    function transferFrom(address _from, address _to, uint256 _id, uint256 _value) external returns (bool);

    /**
        @notice Sets the NFT as a new type token
        @dev The contract itself should verify if the ownership of NFT is belongs to this contract itself with the `_parentNFTContractAddress` & `_parentNFTTokenId` before adding the token.
        MUST revert if the same NFT is already registered.
        MUST revert if `_parentNFTContractAddress` is address zero.
        MUST revert if `_parentNFTContractAddress` is not ERC-721 compatible.
        MUST revert if this contract itself is not the owner of the NFT.
        MUST revert on any other error.
        MUST emit `TokenAddition` event to reflect the token type addition.
        @param _parentNFTContractAddress    NFT contract address
        @param _parentNFTTokenId            NFT tokenID
        @param _totalSupply                 Total token supply
    */
    function setParentNFT(address _parentNFTContractAddress, uint256 _parentNFTTokenId, uint256 _totalSupply) external;

    /**
        @notice Get the token ID's total token supply.
        @param _id      ID of the token
        @return         The total token supply of the specified token type
    */
    function totalSupply(uint256 _id) external view returns (uint256);

    /**
        @notice Get the balance of an account's tokens.
        @param _owner  The address of the token holder
        @param _id     ID of the token
        @return        The _owner's balance of the token type requested
    */
    function balanceOf(address _owner, uint256 _id) external view returns (uint256);

    /**
        @notice Get the amount which `_spender` is still allowed to withdraw from `_owner`
        @param _owner   The address of the token holder
        @param _spender The address approved to withdraw token on behalf of `_owner`
        @param _id      ID of the token
        @return         The amount which `_spender` is still allowed to withdraw from `_owner`
    */
    function allowance(address _owner, address _spender, uint256 _id) external view returns (uint256);

    /**
        @notice Get the bool value which represents whether the NFT is already registered and fractionalized by this contract.
        @param _parentNFTContractAddress    NFT contract address
        @param _parentNFTTokenId            NFT tokenID
        @return                             The bool value representing the whether the NFT is already registered.
    */
    function isRegistered(address _parentNFTContractAddress, uint256 _parentNFTTokenId) external view returns (bool);
}

interface ERC165 {
    /**
        @notice Query if a contract implements an interface
        @param interfaceID The interface identifier, as specified in ERC-165
        @dev Interface identification is specified in ERC-165. This function
        uses less than 30,000 gas.
        @return `true` if the contract implements `interfaceID` and
        `interfaceID` is not 0xffffffff, `false` otherwise
    */
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

To receive Non-Fungible Token on safe Transfer the contract should include onERC721Received(). Including onERC721Received() is needed to be compatible with Safe Transfer Rules.

/**
    @notice Handle the receipt of an NFT
    @param _operator The address which called `safeTransferFrom` function
    @param _from The address which previously owned the token
    @param _tokenId The NFT identifier which is being transferred
    @param _data Additional data with no specified format
    @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
*/
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) external pure returns (bytes4);

Rationale

Metadata

The symbol() & name() functions were not included since the majority of users can just fetch it from the originating NFT contract. Also, copying the name & symbol every time when token gets added might place a lot of redundant bytecode on the Ethereum blockchain. However, according to the need and design of the project it could also be added to each token type by fetching the metadata from the NFT contract.

Design

Most of the decisions made around the design of this ERC were done to keep it as flexible for diverse token design & architecture. These minimum requirement for this standard allows for each project to determine their own system for minting, governing, burning their MFNFT tokens depending on their programmable architecture.

Backwards Compatibility

To make this standard compatible with existing standards, this standard event & function names are identical with ERC-20 token standard with some more events & functions to add token type dynamically.

Also, the sequence of parameter in use of _id for distinguishing token types in functions and events are very much similar to ERC-1155 Multi-Token Standard.

Since this standard is intended to interact with the EIP-721 Non-Fungible Token Standard, it is kept purposefully agnostic to extensions beyond the standard in order to allow specific projects to design their own token usage and scenario.

Test Cases

Reference Implementation of MFNFT Token includes test cases written using hardhat. (Test coverage : 100%)

Reference Implementation

MFNFT - Implementation

Security Considerations

To fractionalize an already minted NFT, it is evident that ownership of NFT should be given to token contracts before fractionalization. In the case of fractionalizing NFT, the token contract should thoroughly verify the ownership of NFT before fractionalizing it to prevent tokens from being a separate tokens with the NFT.

If an arbitrary account has the right to call setParentNFT() there might be a front-running issue. The caller of setParentNFT() might be different from the real NFT sender. To prevent this issue, implementors should just allow admin to call, or fractionalize and receive NFT in an atomic transaction similar to flash loan(swap).

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

David Kim (@powerstream3604), "ERC-4675: Multi-Fractional Non-Fungible Tokens [DRAFT]," Ethereum Improvement Proposals, no. 4675, January 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4675.