How to Implement Semi-Fungible Token in XDC Network (XRC1155)?

Dolly Bhati
Yodaplus
Published in
9 min readOct 27, 2021

In our previous article, we have discussed in detail how NFTs are implemented using XRC721. As NFTs are being used specifically in gaming applications, there was a need for NFTs as semi-fungible tokens.

Fungibility has been a consistent talk of the town of 2021, following the massive rise of NFTs. But what are semi-fungible tokens? How do they work? Let’s explore.

What are Semi-Fungible Tokens?

SFTs are a relatively new group of tokens that can be traded as both fungible and non-fungible during their life cycle. It can easily hold a fungible value that can be traded with some other SFT that has the same value. The token, once redeemed or traded, loses its value becoming non-fungible.

Semi-fungibility

XRC1155 introduces “semi-fungibility”, which can be considered an extension/superset of XRC721 functionality. Unlike XRC721 where a unique ID represents a single asset, the unique ID of an XRC1155 token represents a class of assets, and there is an additional quantity field to represent the amount of the class that a particular wallet has. The assets under the same class are interchangeable, and the user can transfer any amount of assets to others.

So, going back to our XRC721 example, Alice has created 7 original paintings in the “blue period” (token ID 1 to 7) and 5 original paintings in the “pink period” (Token Id 8 to 12). Now seeing the originals in demand Alice wants to bring the reprints of the original in the marketplace and wants to create 100 reprints of painting 1, 30 reprints of painting 2, and 50 reprints of painting 10 to bring into the marketplace. XRC1155 facilitates the same.

So, suppose Bob buys 20 reprints of painting 1 and Charlie buys 10 reprints of painting 2 and 20 reprints of painting 10. And Bob can trade 10 reprints of painting 1 for Charlie's 5 reprints of painting 2 and 10 reprints of painting 10.

XRC1155 Smart contract provides the capability of this kind of “minting” and transfer functionality.

As mentioned XRC1155 being a superset of XRC721, we would discuss the implementation of the extended functionality in this document.

IXRC1155 — Specifications

interface IXRC1155 {/**@dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).The `_operator` argument MUST be msg.sender.The `_from` argument MUST be the address of the holder whose balance is decreased.The `_to` argument MUST be the address of the recipient whose balance is increased.The `_id` argument MUST be the token type being transferred.The `_value` argument MUST be the number of tokens the holder balance is decreased by and match what the recipient balance is increased by.When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).*/event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value);/**@dev Either `TransferSingle` or `TransferBatch` MUST emit when tokens are transferred, including zero value transfers as well as minting or burning (see "Safe Transfer Rules" section of the standard).The `_operator` argument MUST be msg.sender.The `_from` argument MUST be the address of the holder whose balance is decreased.The `_to` argument MUST be the address of the recipient whose balance is increased.The `_ids` argument MUST be the list of tokens being transferred.The `_values` argument MUST be the list of number of tokens (matching the list and order of tokens specified in _ids) the holder balance is decreased by and match what the recipient balance is increased by.When minting/creating tokens, the `_from` argument MUST be set to `0x0` (i.e. zero address).When burning/destroying tokens, the `_to` argument MUST be set to `0x0` (i.e. zero address).*/event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);/**@dev MUST emit when approval for a second party/operator address to manage all tokens for an owner address is enabled or disabled (absense of an event assumes disabled).*/event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);/**@dev MUST emit when the URI is updated for a token ID.URIs are defined in RFC 3986.The URI MUST point a JSON file.*/event URI(string _value, uint256 indexed _id);/**@notice Transfers `_value` amount of an `_id` from the `_from` address to the `_to` address specified (with safety call).@dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).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 the `TransferSingle` event to reflect the balance change (see "Safe Transfer Rules" section of the standard).After the above conditions are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call `onERC1155Received` on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).@param _from    Source address@param _to      Target address@param _id      ID of the token type@param _value   Transfer amount@param _data    Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to`*/function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;/**@notice Transfers `_values` amount(s) of `_ids` from the `_from` address to the `_to` address specified (with safety call).@dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).MUST revert if `_to` is the zero address.MUST revert if length of `_ids` is not the same as length of `_values`.MUST revert if any of the balance(s) of the holder(s) for token(s) in `_ids` is lower than the respective amount(s) in `_values` sent to the recipient.MUST revert on any other error.MUST emit `TransferSingle` or `TransferBatch` event(s) such that all the balance changes are reflected (see "Safe Transfer Rules" section of the standard).Balance changes and events MUST follow the ordering of the arrays (_ids[0]/_values[0] before _ids[1]/_values[1], etc).After the above conditions for the transfer(s) in the batch are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call the relevant `XRC1155TokenReceiver` hook(s) on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).@param _from    Source address@param _to      Target address@param _ids     IDs of each token type (order and length must match _values array)@param _values  Transfer amounts per token type (order and length must match _ids array)@param _data    Additional data with no specified format, MUST be sent unaltered in call to the `XRC1155TokenReceiver` hook(s) on `_to`*/function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;/**@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 balance of multiple account/token pairs@param _owners The addresses of the token holders@param _ids    ID of the Tokens@return        The _owner's balance of the Token types requested (i.e. balance for each (owner, id) pair)*/function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory);/**@notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens.@dev MUST emit the ApprovalForAll event on success.@param _operator  Address to add to the set of authorized operators@param _approved  True if the operator is approved, false to revoke approval*/function setApprovalForAll(address _operator, bool _approved) external;/**@notice Queries the approval status of an operator for a given owner.@param _owner     The owner of the Tokens@param _operator  Address of authorized operator@return           True if the operator is approved, false if not*/function isApprovedForAll(address _owner, address _operator) external view returns (bool);}

Transfers

function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;

The difference from XRC721 safeTransferFrom is you have an additional _value parameter, which indicates the number of the token represented by _id is transferred from.

Additionally as mentioned in the example above, XRC1155 tokens can be transferred in a batch, which is done using safeBatchTransferFrom as below -

function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external {// MUST Throw on errorsrequire(_to != address(0x0), "destination address must be non-zero.");require(_ids.length == _values.length, "_ids and _values array length must match.");require(_from == msg.sender || operatorApproval[_from][msg.sender] == true, "Need operator approval for 3rd party transfers.");for (uint256 i = 0; i < _ids.length; ++i) {uint256 id = _ids[i];uint256 value = _values[i];// SafeMath will throw with insuficient funds _from// or if _id is not valid (balance will be 0)balances[id][_from] = balances[id][_from].sub(value);balances[id][_to]   = value.add(balances[id][_to]);}// Note: instead of the below batch versions of event and acceptance check you MAY have emitted a TransferSingle// event and a subsequent call to _doSafeTransferAcceptanceCheck in above loop for each balance change instead.// Or emitted a TransferSingle event for each in the loop and then the single _doSafeBatchTransferAcceptanceCheck below.// However it is implemented the balance changes and events MUST match when a check (i.e. calling an external contract) is done.// MUST emit eventemit TransferBatch(msg.sender, _from, _to, _ids, _values);// Now that the balances are updated and the events are emitted,// call onERC1155BatchReceived if the destination is a contract.if (_to.isContract()) {_doSafeBatchTransferAcceptanceCheck(msg.sender, _from, _to, _ids, _values, _data);}}

Balances

As XRC1155 supports batch, a balanceOfBatch is introduced to get balance os multiple owners and corresponding token ids.

function balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids) external view returns (uint256[] memory) {require(_owners.length == _ids.length);uint256[] memory balances_ = new uint256[](_owners.length);for (uint256 i = 0; i < _owners.length; ++i) {balances_[i] = balances[_ids[i]][_owners[i]];}return balances_;}

Callbacks when transferring to another smart contract

Similar to XRC721 onERC721Received, the token receiver contract needs to implement 2 functions, onERC1155Received and onERC1155BatchReceived.

if (_to.isContract()) {
_doSafeBatchTransferAcceptanceCheck(msg.sender, _from, _to, _ids, _values, _data);

Implementation of _doSafeBatchTransferAcceptanceCheck is as follows -
function _doSafeTransferAcceptanceCheck(address _operator, address _from, address _to, uint256 _id, uint256 _value, bytes memory _data) internal {require(XRC1155TokenReceiver(_to).onERC1155Received(_operator, _from, _id, _value, _data) == XRC1155_ACCEPTED (=0xf23a6e61), "contract returned an unknown value from onERC1155Received");}function _doSafeBatchTransferAcceptanceCheck(address _operator, address _from, address _to, uint256[] memory _ids, uint256[] memory _values, bytes memory _data) internal {require(XRC1155TokenReceiver(_to).onERC1155BatchReceived(_operator, _from, _ids, _values, _data) == XRC1155_BATCH_ACCEPTED (=0xbc197c81), "contract returned an unknown value from onERC1155BatchReceived");}

The typical implementation of the above two functions in token receiver contract is -

function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4){// bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))return 0xf23a6e61;}function onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external returns(bytes4) {// bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))return 0xbc197c81;}

Note:: The callbacks are onERC1155Received and onERC1155BatchReceived, for Ethereum wallet compatibility.

Mint and Burn

Similar to XRC721, XRC1155 can be extended to include mint and burn functionalities, including batch facilities.

As an example mint function can be implemented as -

// Batch mint tokens. Assign directly to _to[].function mint(uint256 _id, address[] calldata _to, uint256[] calldata _quantities) external creatorOnly(_id) {for (uint256 i = 0; i < _to.length; ++i) {address to = _to[i];uint256 quantity = _quantities[i];// Grant the items to the callerbalances[_id][to] = quantity.add(balances[_id][to]);// Emit the Transfer/Mint event.// the 0x0 source address implies a mint// It will also provide the circulating supply info.emit TransferSingle(msg.sender, address(0x0), to, _id, quantity);if (to.isContract()) {_doSafeTransferAcceptanceCheck(msg.sender, msg.sender, to, _id, quantity, '');}}}

Source

So, now we have seen how XRC721 can be extended with XRC1155 for implementing semi-fungible tokens with batch functionalities.

The ready-to-use downloadable code is here.

What’s more?

The NFT marketplace has reached astounding levels in the first half of this year. The sales have surged to over $2.4 Billion within the first quarter of 2021. The momentum is still growing and the trading volume is at an all-time high.

I shall bring more blogs to you for further digging. Till then, you can explore xinfin.org.

Further Readings

https://remix.xinfin.network/

https://medium.com/yodaplus/how-is-xinfin-remix-used-for-the-development-of-smart-contracts-for-xinfin-hybrid-blockchain-4c7af98eb8b3

https://chrome.google.com/webstore/detail/xdcpay/bocpokimicclpaiekenaeelehdjllofo?hl=en

--

--

Dolly Bhati
Yodaplus

A technophile with a soul of travel yogi — writing experience in blockchain, cryptocurrency, dApps, software development, yoga, etc.