Developers Forum for XinFin XDC Network

Cover image for [Informative] Understanding XRC404 which is EVM Compatible (ERC404): Bridging Fungible Tokens and NFTs
Vinn
Vinn

Posted on • Updated on

[Informative] Understanding XRC404 which is EVM Compatible (ERC404): Bridging Fungible Tokens and NFTs

In the fast-evolving landscape of blockchain technology, the quest for interoperability and enhanced utility has led to the development of innovative standards like ERC404. This standard aims to bridge the worlds of fungible tokens and non-fungible tokens (NFTs), bringing together the benefits of both in a seamless manner. In this article, we delve into the intricacies of XRC404, exploring how it facilitates the convergence of fungibility and uniqueness within the realm of digital assets.

Image description

What is XRC404?

XRC404 is a EVM Compatible protocol standard built on the XinFin XDC Network, designed to enable the interoperability of fungible tokens and NFTs. It draws inspiration from the ERC-404 standard and introduces a set of guidelines and specifications for the creation and management of hybrid assets that possess both fungible and non-fungible characteristics.

Key Features and Functionality:

Hybrid Asset Creation: XRC404 allows developers to create hybrid assets that combine the properties of fungible tokens and NFTs. These assets can exhibit both divisibility and uniqueness, offering a wide range of use cases across various industries.

Interoperability: One of the primary objectives of XRC404 is to facilitate interoperability between fungible tokens(FTs) and NFTs. Through this standard, different types of digital assets can seamlessly interact within the XinFin ecosystem, unlocking new possibilities for decentralized applications (DApps) and smart contracts.

Enhanced Utility: By merging fungibility and uniqueness, XRC404 enhances the utility of digital assets. Users can leverage these hybrid tokens for applications such as gaming, digital collectibles, supply chain management, and more, enjoying the benefits of both liquidity and scarcity.

Benefits of XRC404:

Versatility: XRC404 opens up a world of possibilities by offering versatile digital assets that can cater to a wide range of use cases.

Efficiency: The standard streamlines asset management and transactions, improving overall efficiency within the blockchain ecosystem.

Innovation: XRC404 fosters innovation by encouraging the development of novel DApps and solutions that leverage the unique capabilities of hybrid tokens.

Use Cases:

Gaming: XRC404 tokens can be utilized within gaming ecosystems to represent both fungible in-game currency and rare collectible items, enhancing the gaming experience for players.

Art and Collectibles: Artists and creators can tokenize their works using XRC404, allowing for the creation of limited-edition digital art pieces that possess inherent value and scarcity.

Supply Chain Management: Hybrid tokens can be employed in supply chain management to track and verify the authenticity of high-value goods, ensuring transparency and trust throughout the supply chain.

How Does XRC-404 Work?

Currently, ERC-404 works by linking every issued token to an NFT, which acts as the source of truth for the token’s state and ownership. The NFT can have a base unit, which defines the minimum amount of tokens that can be transferred or exchanged. The XDC Network, being EVM compatible and deployed on the XDC Apothem Network, mirrors this functionality seamlessly.

For example, if the base unit is 100, then the token can be divided into 100 fractions, each representing 1% of the NFT. The token can also have a total supply, which defines the maximum number of tokens that can be issued for the NFT. For example, if the total supply is 1000, then the token can be minted up to 1000 times, each representing 0.1% of the NFT.

The standard defines the primary functions within the token contract:
Functions:

  • setWhitelist: Allows the contract owner to whitelist addresses, preventing them from minting or burning tokens while transferring tokens.
  • ownerOf: Returns the owner of a specified token ID.
  • tokenURI: (Abstract Function) Should be implemented to return the URI for a token's metadata.
  • approve: Allows a token owner to approve another address to spend a specific amount or token ID on their behalf.
  • setApprovalForAll: Enables or disables approval for an operator to manage all of the caller's tokens.
  • transferFrom: Facilitates the transfer of fungible tokens or a specific NFT from one address to another.
  • transfer: Allows for the transfer of fungible tokens from the caller's address to another.
  • safeTransferFrom (with and without data): Similar to transferFrom but includes checks to ensure the recipient can safely receive NFTs.
  • _transfer: Internal function that handles the logic of transferring fungible tokens, including potential minting or burning of NFTs based on the transferred amount.
  • _getUnit: Returns the unit used for fractional transfers, typically 10^decimals.
  • _mint: Mints a new token ID to a specified address.
  • _burn: Burns the specified token ID from a given address.
  • _setNameSymbol: Allows updating the token's name and symbol.

Let’s Deploy ERC404.sol contract on XDC Apothem Network

To construct our ERC-404 token contract, we'll develop two separate Solidity contracts:

  • ERC404.sol: This contract implements the ERC-404 standard, defining the core functionality that enables the mixed fungible and non-fungible token features.
  • My404.sol: This is our custom token contract that inherits from ERC404.sol. Here, we can define specific behaviors, tokenomics, and additional features that are unique to our token.

ERC404.sol

//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

abstract contract Ownable {
    event OwnershipTransferred(address indexed user, address indexed newOwner);

    error Unauthorized();
    error InvalidOwner();

    address public owner;

    modifier onlyOwner() virtual {
        if (msg.sender != owner) revert Unauthorized();

        _;
    }

    constructor(address _owner) {
        if (_owner == address(0)) revert InvalidOwner();

        owner = _owner;

        emit OwnershipTransferred(address(0), _owner);
    }

    function transferOwnership(address _owner) public virtual onlyOwner {
        if (_owner == address(0)) revert InvalidOwner();

        owner = _owner;

        emit OwnershipTransferred(msg.sender, _owner);
    }

    function revokeOwnership() public virtual onlyOwner {
        owner = address(0);

        emit OwnershipTransferred(msg.sender, address(0));
    }
}

abstract contract ERC721Receiver {
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external virtual returns (bytes4) {
        return ERC721Receiver.onERC721Received.selector;
    }
}

/// @notice ERC404
///         A gas-efficient, mixed ERC20 / ERC721 implementation
///         with native liquidity and fractionalization.
///
///         This is an experimental standard designed to integrate
///         with pre-existing ERC20 / ERC721 support as smoothly as
///         possible.
///
/// @dev    In order to support full functionality of ERC20 and ERC721
///         supply assumptions are made that slightly constraint usage.
///         Ensure decimals are sufficiently large (standard 18 recommended)
///         as ids are effectively encoded in the lowest range of amounts.
///
///         NFTs are spent on ERC20 functions in a FILO queue, this is by
///         design.
///
abstract contract ERC404 is Ownable {
    // Events
    event ERC20Transfer(
        address indexed from,
        address indexed to,
        uint256 amount
    );
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 amount
    );
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed id
    );
    event ERC721Approval(
        address indexed owner,
        address indexed spender,
        uint256 indexed id
    );
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    // Errors
    error NotFound();
    error AlreadyExists();
    error InvalidRecipient();
    error InvalidSender();
    error UnsafeRecipient();

    // Metadata
    /// @dev Token name
    string public name;

    /// @dev Token symbol
    string public symbol;

    /// @dev Decimals for fractional representation
    uint8 public immutable decimals;

    /// @dev Total supply in fractionalized representation
    uint256 public immutable totalSupply;

    /// @dev Current mint counter, monotonically increasing to ensure accurate ownership
    uint256 public minted;

    // Mappings
    /// @dev Balance of user in fractional representation
    mapping(address => uint256) public balanceOf;

    /// @dev Allowance of user in fractional representation
    mapping(address => mapping(address => uint256)) public allowance;

    /// @dev Approval in native representaion
    mapping(uint256 => address) public getApproved;

    /// @dev Approval for all in native representation
    mapping(address => mapping(address => bool)) public isApprovedForAll;

    /// @dev Owner of id in native representation
    mapping(uint256 => address) internal _ownerOf;

    /// @dev Array of owned ids in native representation
    mapping(address => uint256[]) internal _owned;

    /// @dev Tracks indices for the _owned mapping
    mapping(uint256 => uint256) internal _ownedIndex;

    /// @dev Addresses whitelisted from minting / burning for gas savings (pairs, routers, etc)
    mapping(address => bool) public whitelist;

    // Constructor
    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals,
        uint256 _totalNativeSupply,
        address _owner
    ) Ownable(_owner) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        totalSupply = _totalNativeSupply * (10 ** decimals);
    }

    /// @notice Initialization function to set pairs / etc
    ///         saving gas by avoiding mint / burn on unnecessary targets
    function setWhitelist(address target, bool state) public onlyOwner {
        whitelist[target] = state;
    }

    /// @notice Function to find owner of a given native token
    function ownerOf(uint256 id) public view virtual returns (address owner) {
        owner = _ownerOf[id];

        if (owner == address(0)) {
            revert NotFound();
        }
    }

    /// @notice tokenURI must be implemented by child contract
    function tokenURI(uint256 id) public view virtual returns (string memory);

    /// @notice Function for token approvals
    /// @dev This function assumes id / native if amount less than or equal to current max id
    function approve(
        address spender,
        uint256 amountOrId
    ) public virtual returns (bool) {
        if (amountOrId <= minted && amountOrId > 0) {
            address owner = _ownerOf[amountOrId];

            if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) {
                revert Unauthorized();
            }

            getApproved[amountOrId] = spender;

            emit Approval(owner, spender, amountOrId);
        } else {
            allowance[msg.sender][spender] = amountOrId;

            emit Approval(msg.sender, spender, amountOrId);
        }

        return true;
    }

    /// @notice Function native approvals
    function setApprovalForAll(address operator, bool approved) public virtual {
        isApprovedForAll[msg.sender][operator] = approved;

        emit ApprovalForAll(msg.sender, operator, approved);
    }

    /// @notice Function for mixed transfers
    /// @dev This function assumes id / native if amount less than or equal to current max id
    function transferFrom(
        address from,
        address to,
        uint256 amountOrId
    ) public virtual {
        if (amountOrId <= minted) {
            if (from != _ownerOf[amountOrId]) {
                revert InvalidSender();
            }

            if (to == address(0)) {
                revert InvalidRecipient();
            }

            if (
                msg.sender != from &&
                !isApprovedForAll[from][msg.sender] &&
                msg.sender != getApproved[amountOrId]
            ) {
                revert Unauthorized();
            }

            balanceOf[from] -= _getUnit();

            unchecked {
                balanceOf[to] += _getUnit();
            }

            _ownerOf[amountOrId] = to;
            delete getApproved[amountOrId];

            // update _owned for sender
            uint256 updatedId = _owned[from][_owned[from].length - 1];
            _owned[from][_ownedIndex[amountOrId]] = updatedId;
            // pop
            _owned[from].pop();
            // update index for the moved id
            _ownedIndex[updatedId] = _ownedIndex[amountOrId];
            // push token to to owned
            _owned[to].push(amountOrId);
            // update index for to owned
            _ownedIndex[amountOrId] = _owned[to].length - 1;

            emit Transfer(from, to, amountOrId);
            emit ERC20Transfer(from, to, _getUnit());
        } else {
            uint256 allowed = allowance[from][msg.sender];

            if (allowed != type(uint256).max)
                allowance[from][msg.sender] = allowed - amountOrId;

            _transfer(from, to, amountOrId);
        }
    }

    /// @notice Function for fractional transfers
    function transfer(
        address to,
        uint256 amount
    ) public virtual returns (bool) {
        return _transfer(msg.sender, to, amount);
    }

    /// @notice Function for native transfers with contract support
    function safeTransferFrom(
        address from,
        address to,
        uint256 id
    ) public virtual {
        transferFrom(from, to, id);

        if (
            to.code.length != 0 &&
            ERC721Receiver(to).onERC721Received(msg.sender, from, id, "") !=
            ERC721Receiver.onERC721Received.selector
        ) {
            revert UnsafeRecipient();
        }
    }

    /// @notice Function for native transfers with contract support and callback data
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        bytes calldata data
    ) public virtual {
        transferFrom(from, to, id);

        if (
            to.code.length != 0 &&
            ERC721Receiver(to).onERC721Received(msg.sender, from, id, data) !=
            ERC721Receiver.onERC721Received.selector
        ) {
            revert UnsafeRecipient();
        }
    }

    /// @notice Internal function for fractional transfers
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal returns (bool) {
        uint256 unit = _getUnit();
        uint256 balanceBeforeSender = balanceOf[from];
        uint256 balanceBeforeReceiver = balanceOf[to];

        balanceOf[from] -= amount;

        unchecked {
            balanceOf[to] += amount;
        }

        // Skip burn for certain addresses to save gas
        if (!whitelist[from]) {
            uint256 tokens_to_burn = (balanceBeforeSender / unit) -
                (balanceOf[from] / unit);
            for (uint256 i = 0; i < tokens_to_burn; i++) {
                _burn(from);
            }
        }

        // Skip minting for certain addresses to save gas
        if (!whitelist[to]) {
            uint256 tokens_to_mint = (balanceOf[to] / unit) -
                (balanceBeforeReceiver / unit);
            for (uint256 i = 0; i < tokens_to_mint; i++) {
                _mint(to);
            }
        }

        emit ERC20Transfer(from, to, amount);
        return true;
    }

    // Internal utility logic
    function _getUnit() internal view returns (uint256) {
        return 10 ** decimals;
    }

    function _mint(address to) internal virtual {
        if (to == address(0)) {
            revert InvalidRecipient();
        }

        unchecked {
            minted++;
        }

        uint256 id = minted;

        if (_ownerOf[id] != address(0)) {
            revert AlreadyExists();
        }

        _ownerOf[id] = to;
        _owned[to].push(id);
        _ownedIndex[id] = _owned[to].length - 1;

        emit Transfer(address(0), to, id);
    }

    function _burn(address from) internal virtual {
        if (from == address(0)) {
            revert InvalidSender();
        }

        uint256 id = _owned[from][_owned[from].length - 1];
        _owned[from].pop();
        delete _ownedIndex[id];
        delete _ownerOf[id];
        delete getApproved[id];

        emit Transfer(from, address(0), id);
    }

    function _setNameSymbol(
        string memory _name,
        string memory _symbol
    ) internal {
        name = _name;
        symbol = _symbol;
    }
}
Enter fullscreen mode Exit fullscreen mode

My404.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

// Importing the XRC404 contract as the base and OpenZeppelin's Strings library for string operations
import "./ERC404.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

// My404 contract inherits XRC404 to create a custom token with both ERC-20 and ERC-721 features
contract My404 is ERC404 {
    // Public variables to store URIs for token metadata
    string public dataURI;
    string public baseTokenURI;

    // Constructor to initialize the contract with token details and owner's initial balance
    constructor(address _owner) ERC404("XRC404", "XRC404", 18, 10000, _owner) {
        balanceOf[_owner] = 10000 * 10 ** 18; // Setting the initial balance of tokens for the owner
    }

    // Function to set the data URI, which can be used for additional metadata (change as needed)
    function setDataURI(string memory _dataURI) public onlyOwner {
        dataURI = _dataURI;
    }

    // Function to set the base URI for token metadata; this can be an IPFS link (changeable by the owner)
    function setTokenURI(string memory _tokenURI) public onlyOwner {
        baseTokenURI = _tokenURI;
    }

    // Allows the owner to update the token's name and symbol post-deployment (optional flexibility)
    function setNameSymbol(string memory _name, string memory _symbol) public onlyOwner {
        _setNameSymbol(_name, _symbol);
    }

    // Override of the tokenURI function to return the base URI for token metadata; users can implement logic to return unique URIs per token ID
    function tokenURI(uint256 id) public view override returns (string memory) {
        // Potential place to append the token ID to the base URI for unique metadata per token
        // For now, it simply returns the base URI for all tokens
        return baseTokenURI;
    }
}

Enter fullscreen mode Exit fullscreen mode

Adjusting Solidity Version:
Before compiling the flattened contract, ensure that the Solidity version is compatible. Change the version from 0.8.20 to 0.8.18 as follows:

pragma solidity ^0.8.20;
Enter fullscreen mode Exit fullscreen mode

Compiling the Contract:
Now, compile the modified contract to ensure it compiles successfully without any errors. This step verifies the integrity of the contract code and prepares it for deployment.

Deploying the Contract:
Once the contract is successfully compiled, it’s time to deploy it on the XDC Apothem Network. Make sure you have sufficient XDC tokens in your wallet to cover the deployment costs.

Image description

Conclusion:

ERC404 stands at the forefront of blockchain innovation, offering a unique solution for bridging the gap between fungible tokens and NFTs. By introducing hybrid assets that blend liquidity with scarcity, this standard paves the way for new possibilities in digital asset management, decentralized finance, and beyond. Leveraging the power of semi-fungible tokens, asset fractionalization, and smart contracts, XRC404 represents a transformative solution for trade finance on the XDC Network. It promises unparalleled efficiency, liquidity, and accessibility, revolutionizing how trade assets are tokenized, traded, and financed. As the blockchain ecosystem evolves, XRC404 is poised to play a pivotal role in shaping the future of digital finance and decentralized applications, unlocking new opportunities for growth, efficiency, and resilience within the global trade ecosystem.

Discussion (0)