Skip to main content

VEP-1155: Multi-Token

VEP: 1155
author: Evgeny Shatalov <evgeny.a.shatalov@gmail.com>, Aleksei Kolchanov <a.kolchanov@numi.net>, Aleksandr Khramtsov <aleksandr.hramcov@gmail.com>
status: Review
type: Contract
created: 2023-03-08
requires: TIP-3, TIP-4.1, TIP-4.2, TIP-4.3, TIP-6

Abstract

The following standard describes the basic idea about distributed Multi-Token architecture. This standard provides basic functionality to create, track and transfer both fungible and non-fungible tokens in a collection.

Motivation

The suggested standard differs considerably from [Ethereum ERC-1155] and other smart contract token standards with single registry because of its distributed nature related to Venom blockchain particularities. Given that Venom has a storage fee, VEP-1155 is fully distributed and implies separate storage of each fungible and non-fungible tokens. A standard interface allows any fungible and non-fungible tokens to be re-used by other applications: wallets, explorers, marketplaces, etc.

Architecture

General information about tokens collection is stored in the collection contract. Each non-fungible token (NFT) deployed in separate smart contracts and links to token collection. Each token with supply count more than one deployed in separate MultiToken smart contracts and links to collection. Each MultiToken holder has their own instance of a specific contract. MultiToken transfers SHOULD be implemented in P2P fashion, between sender and receiver.

In general Smart contract architecture based on:

  • Consider an asynchronous type of Venom blockchain. Use callbacks and asynchronous getters.
  • Standardizes one NFT - one smart contract.
  • Gas fee management practicals.
  • Use TIP-3 architecture for MultiTokens.
  • Use TIP-4.1 architecture and interfaces for non-fungible tokens.
  • Use TIP-4.2 and TIP-4.3 architecture and interfaces for all tokens.
  • Use TIP-6.1

Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Contracts

  • Collection - contract that mints tokens.
  • NFT - TIP-4.1 and TIP-4.2 contract that stores token information and tokenSupply.
  • MultiTokenWallet - TIP-3 like and TIP-4.3 compliant constract stores the balance. Each MultiToken holder has its own instance of MultiToken wallet contract.
  • IndexBasis - TIP-4.3 contract, that helps to find all collections by the code hash of which.
  • Index - TIP-4.3 contract, that helps to find:
    • All user tokens in current collection using owner address and collection address.
    • All user tokens in all collections using the owner address.
    • All tokens with a tokenId must have a corresponding nft with the same nftId. Where nftId == tokenId.

code of IndexBasis and Index contracts and code hash of contracts is fixed and CANNOT BE CHANGED

Collection

The contract represents shared information about tokens collection and logic for creation of tokens and burn of NFTs.

Every VEP-1155 compliant collection contract must implement IMultiTokenCollection interface in addtion to TIP4_1Collection and TIP-6.1.

pragma ton-solidity >= 0.58.0;

interface IMultiTokenCollection {

/// @notice This event emits when MulitToken is created
/// @param id Unique Nft id
/// @param token Address of MultiToken wallet contract
/// @param owner Address of MultiToken wallet owner
/// @param balance count of minted tokens
/// @param creator Address of creator that initialize mint
event MultiTokenCreated(uint256 id, address token, uint128 balance, address owner, address creator);

/// @notice This event emits when MultiTokens are burned
/// @param id Unique Nft id
/// @param count Number of burned tokens
/// @param owner Address of MultiToken wallet owner
event MultiTokenBurned(uint256 id, uint128 count, address owner);

/// @notice Returns the MultiToken wallet code
/// @return code Returns the MultiToken wallet code as TvmCell
function multiTokenWalletCode(uint256 id, bool isEmpty) external view responsible returns (TvmCell code);

/// @notice Returns the MultiToken wallet code hash
/// @return codeHash Returns the MultiToken wallet code hash
function multiTokenCodeHash(uint256 id, bool isEmpty) external view responsible returns (uint256 codeHash);

/// @notice Computes MultiToken wallet address by unique MultiToken id and its owner
/// @dev Return unique address for all Ids and owners. You find nothing by address for not a valid MultiToken wallet
/// @param id Unique MultiToken id
/// @param owner Address of MultiToken owner
/// @return token Returns the address of MultiToken wallet contract
function multiTokenWalletAddress(uint256 id, address owner) external view responsible returns (address token);
}

NOTE The TIP-6.1 identifier for this interface is 0x5C435318.

IMultiTokenCollection.multiTokenWalletCode()

    function multiTokenWalletCode(uint256 id, bool isEmpty) external view responsible returns (TvmCell code);
  • id (uint256) - Unique Nft id
  • isEmpty (bool) - Balance empty flag
  • code (TvmCell) - MultiToken contract code

MultiToken wallet is a smart contract deployed from collection smart contract using tokenCode, id and owner address. MultiToken wallet code must be salted by collection address, the nft id, and isEmpty flag that marks if the wallet has a balance of zero.

IMultiTokenCollection.multiTokenCodeHash()

    function multiTokenCodeHash(uint256 id, bool isEmpty) public view responsible returns (uint256 codeHash);
  • id (uint256) - Unique Nft id
  • isEmpty (bool) - Balance empty flag
  • codeHash (uint256) - MultiToken wallet contract code hash

The codeHash allows search of all empty or non-empty MultiToken wallet contracts of this collection for the corresponding nft using base dApp functionality.

IMultiTokenCollection.tokenAddress()

    function multiTokenWalletAddress(uint256 id, address owner) external view responsible returns (address token);
  • id (uint256) - Unique Nft id
  • owner (address) - Address of MultiToken wallet owner

Computes MultiToken wallet contract address by unique Nft id and its owner. You can check number of owned MultiTokens using base dApp functionality.

Events

    event MultiTokenCreated(uint256 id, address token, address owner, address creator);
  • id (uint256) - Unique Nft id
  • token (address) - Address of MultiToken wallet contract
  • balance (uint128) - Initial balance
  • owner (address) - Address of MultiToken owner
  • creator (address) - The initial address who initiate MultiToken deploy

You must emit MultiTokenCreated event when MultiToken is minted (initial MultiToken wallet deployed).

    event MultiTokenBurned(uint256 id, uint128 count, address owner);
  • id (uint256) - Unique Nft id
  • count (uint128) - Number of burned tokens
  • owner (address) - Address of MultiToken owner

You must emit MultiTokenBurned event when MultiTokens are burned.

Mint and burn NFT and MultiTokens

A function's signature is not included in the specification. It's recommended to return the id of the minted MultiToken in the mint function in order to find minted MultiToken wallet contracts.

See the Events for your responsibilities when creating MultiTokens.

NFT

When the supply is just one, the MultiToken is essentially a non-fungible token (NFT). And as is standard for TIP-4.1. Since VEP-1155 collection supports TIP4_1Collection interface, it's compatible and there is no any difference with TIP-4.1 standard when dealing with NFT. In order to be consistent NFT contract must support TIP-4.1, TIP-4.2 and TIP-4.3 standards completely.

Every VEP-1155 compliant nft contract must also implement IMultiTokenNft. In the case that supply is just one, but no tokens, multiTokenSupply() should return 0.

In the case that count == 1, this does not necessarily signify that the sole token owner is the manager or owner of the nft through the TIP4.1 interface, only that he is the sole token owner.

interface IMultiTokenNft {
/// @notice Count total MultiToken supply
/// @return count A count of active MultiTokens minted by collection
function multiTokenSupply() external view responsible returns (uint128 count);
}

NOTE The TIP-6.1 identifier for this interface is 0x243CF87E.

IMultiTokenNft.multiTokenSupply()

    function multiTokenSupply() external view responsible returns (uint128 count);
  • count (uint128) - A count of active MultiTokens minted to this nft

MultiToken Wallet

The contract represents information about current MultiToken and control logic.

MultiToken wallet must:

  • implement IMultiTokenWallet interface and TIP-6.1 interfaces.
  • implement TIP4_3NFT interface from TIP-4.3 standard.
  • deploy three Index contracts TIP-4.3 with stamp = "fungible" and different code salt:
    • With zero collection address collection = "0:0000000000000000000000000000000000000000000000000000000000000000" in code salt.
    • With non-zero collection address collection = "0:3bd8…" in code salt.
  • implement IDestroyable interface, the owner must decide whether to destroy the MultiToken wallet contract or not if the balance is zero.
  • destruct Index before MultiToken wallet is destroyed.
  • have correct codesalt with (in order):
    • the collection address: address collection
    • the token id: uint128 id
    • the balance empty flag bool isEmpty
    • On a zero balance, isEmpty must be set to true
    • On a non-zero balance, isEmpty must be set to false

Every VEP-1155 compliant token contract must implement the IMultiTokenWallet interface and TIP-6.1 interfaces.

    pragma ton-solidity >= 0.58.0;

interface IMultiTokenWallet {

/// @notice The event emits when MultiToken is created
/// @dev Emit the event when MultiToken is ready to use
/// @param id Unique MultiToken id
/// @param owner Address of MultiToken owner
/// @param collection Address of collection smart contract that mint the MultiToken
/// @param balance count of minted tokens
event MultiTokenWalletCreated(uint256 id, address owner, address collection, uint128 balance);

/// @notice The event emits when MultiToken is transfered
/// @dev Emit the event when token is ready to use
/// @param sender MultiToken wallet owner address that sends MultiTokens
/// @param senderWallet Sender MultiToken wallet address
/// @param recipient Address of owner of recipient MultiToken wallet contract
/// @param count How many MultiTokens transfered
/// @param newBalance Recipient wallet balance after transfer
event MultiTokenTransfered(address sender, address senderWallet, address recipient, uint128 count, uint128 newBalance);

/// @notice MultiToken info
/// @return id Unique MultiToken id
/// @return owner Address of wallet owner
/// @return collection Сollection smart contract address
function getInfo() external view responsible returns(uint256 id, address owner, address collection);

/// @notice Returns the number of owned MultiTokens
/// @return value owned MultiTokens count
function balance() external view responsible returns (uint128 value);
/// @notice Transfer MultiTokens to the recipient
/// @dev Can be called only by MultiToken owner
/// @param count How many MultiTokens to transfer
/// @param recipient Address of owner of recipient MultiToken wallet contract
/// @param deployTokenWalletValue How much Venom send to MultiToken wallet contract on deployment. Do not deploy contract if zero.
/// @param remainingGasTo Remaining gas receiver
/// @param notify Notify receiver on incoming transfer
/// @param payload Notification payload
function transfer(uint128 count, address recipient, uint128 deployTokenWalletValue, address remainingGasTo, bool notify, TvmCell payload) external;

/// @notice Transfer MultiTokens to the MultiToken wallet contract
/// @dev Can be called only by MultiToken owner
/// @param count How many MultiTokens to transfer
/// @param recipientToken Recipient MultiToken wallet contract address
/// @param remainingGasTo Remaining gas receiver
/// @param notify Notify receiver on incoming transfer
/// @param payload Notification payload
function transferToWallet(uint128 count, address recipientToken, address remainingGasTo, bool notify, TvmCell payload) external;

/// @notice Callback for transfer operation
/// @dev Can be called only by another valid MultiToken wallet contract with same id and collection
/// @param count How many MultiTokens to receiver
/// @param sender MultiToken wallet owner address that sends MultiTokens
/// @param remainingGasTo Remaining gas receiver
/// @param notify Notify receiver on incoming transfer
/// @param payload Notification payload
function acceptTransfer(uint128 count, address sender, address remainingGasTo, bool notify, TvmCell payload) external;

/// @notice Burn MultiTokens by owner
/// @dev Can be called only by MultiToken owner
/// @param count How many MultiTokens to burn
/// @param remainingGasTo Remaining gas receiver
/// @param callbackTo Burn callback address
/// @param payload Notification payload
function burn(uint128 count, address remainingGasTo, address callbackTo, TvmCell payload) external;
}

NOTE The TIP-6.1 identifier for this interface is 0x2F202802.

IMultiTokenWallet.getInfo()

    function getInfo() external view responsible returns(uint256 id, address owner, address collection);
  • id (uint256) - Unique MultiToken id
  • owner (address) - The owner of the MultiToken
  • collection (address) - The MultiToken collection address

IMultiTokenWallet.balance()

    function balance() public view responsible returns (uint128 value);
  • value (uint128) - How many MultiTokens owned

IMultiTokenWallet.transfer()

    function transfer(uint128 count, address recipient, uint128 deployTokenValue, address remainingGasTo, bool notify, TvmCell payload) external;
  • count (uint128) - How many MultiTokens to transfer
  • recipient (address) - Address of owner of recipient MultiToken wallet contract
  • deployTokenWalletValue (uint128) - How much Venom send to MultiToken wallet contract on deployment
  • remainingGasTo (address) - Remaining gas receiver
  • notify (bool) - Notify receiver on incoming transfer
  • payload (TvmCell) - Notification payload

Transfers the number of MultiTokens to the MultiToken wallet contract, owned by recipient. MultiToken wallet contract address is derived automatically. If deployTokenValue is greater than 0, MultiToken wallet contract MUST be deployed for the recipient. Calls the acceptTransfer on the recipient MultiToken wallet. If notify is true, than the onAcceptTokensTransfer callback message will be sent to the recipient. Decreases the MultiToken balance by count.

IMultiTokenWallet.transferToWallet()

    function transferToWallet(uint128 count, address recipientToken, address remainingGasTo, bool notify, TvmCell payload) external;
  • count (uint128) - How many MultiTokens to transfer
  • recipientToken (address) - Recipient MultiToken wallet contract address
  • remainingGasTo (address) - Remaining gas receiver
  • notify (bool) - Notify receiver on incoming transfer
  • payload (TvmCell) - Notification payload

Transfers the number of MultiTokens to the MultiToken wallet contract, owned by recipient. Calls the acceptTransfer on the recipient MultiToken wallet. If notify is true, than the onAcceptTokensTransfer callback message will be sent to the recipient. Decreases the MultiToken balance by count.

    function acceptTransfer(uint128 count, address sender, address remainingGasTo, bool notify, TvmCell payload) external;
  • count (uint128) - How many MultiTokens to transfer
  • sender(address) - MultiToken wallet owner address that sends MultiTokens
  • remainingGasTo (address) - Remaining gas receiver
  • notify (bool`) - Notify receiver on incoming transfer
  • payload (TvmCell) - Notification payload

Accepts incoming transfer for count of MultiTokens from MultiTokens wallet, owned by sender. Updates transfered number of MultiTokens in the MultiToken wallet contract. transfer and transferToWallet must call acceptTransfer.

IMultiTokenWallet.burn()

    function burn(uint128 count, address remainingGasTo, address callbackTo, TvmCell payload) external;
  • count (uint128) - How many MultiTokens to burn
  • remainingGasTo (address) - Remaining gas receiver
  • callbackTo (address) - Burn callback address
  • payload (TvmCell) - Notification payload

The MultiToken wallet must send an internal message to collection contract before MultiToken burned. If callbackTo is zero address, than all the remaining gas is transferred to the remainingGasTo. Otherwise, message with onAcceptTokensBurn callback is sent to the callbackTo address.

Events

    event MultiTokenWalletCreated(uint256 id, address owner, address collection, uint128 balance);
  • id (uint256) - Unique MultiToken id
  • owner (address) - Address of MultiToken owner
  • collection (address) - The collection address that initiate MultiToken deploy
  • balance (uint128) - Initial balance

You must emit MultiTokenWalletCreated event, when MultiToken wallet created, initialized and ready to use.

    event event MultiTokenTransfered(address senderWallet, address sender, address recipient, uint128 count, uint128 newBalance);
  • sender (address) - MultiToken wallet owner address that sends MultiTokens
  • senderWallet (address) - Sender MultiToken wallet address
  • recipient (address) - Address of owner of recipient MultiToken wallet contract
  • count (uint128) - How many MultiTokens transfered
  • newBalance (uint128) - Recipient wallet balance after transfer

You must emit MultiTokenTransfered event, when MultiTokens transfered from one wallet to another (acceptTransfer method implementaion).

Mint MultiTokenWallet

A function and constructor signature is not included in the specification.

MultiTokenWallet must be deployed from the MultiToken collection smart contract.

The MultiTokenWallet must emit the MultiTokenWalletCreated event after MultiTokenWallet has been deployed and is ready to use. It must be possible to specify whever to notify MultiToken owner or not. In order to be notified the owner contract must implement onAcceptTokensMint.

Callbacks

Mint callback

Notifies MultiToken wallet owner that token minted.

interface IMultiTokenMintCallback {
/// @notice Callback from MultiToken wallet contract on mint initial MultiToken
/// @param collection Address of collection smart contract that mint MultiToken
/// @param tokenId Unique MultiToken id
/// @param count minted MultiTokens count
/// @param remainingGasTo Address specified for receive remaining gas
/// @param payload Additional data attached to transfer by sender
function onMintMultiToken(
address collection,
uint256 tokenId,
uint128 count,
address remainingGasTo,
TvmCell payload
) external;
}

Incoming transfer callback

Notifies MultiToken wallet owner that the incoming transfer has been accepted.

interface IMultiTokenTransferCallback {

/// @notice Callback from MultiToken wallet contract on receive MultiTokens transfer
/// @param collection Address of collection smart contract that mint MultiToken
/// @param tokenId Unique MultiToken id
/// @param count Received MultiTokens count
/// @param sender MultiToken wallet owner address that sends MultiTokens
/// @param senderWallet Sender MultiToken wallet address
/// @param remainingGasTo Address specified for receive remaining gas
/// @param payload Additional data attached to transfer by sender
function onMultiTokenTransfer(
address collection,
uint256 tokenId,
uint128 count,
address sender,
address senderWallet,
address remainingGasTo,
TvmCell payload
) external;
}

Bounced transfer callback

Notifies MultiToken wallet owner that token transfer was bounced.

interface IMultiTokenBounceTransferCallback {

/// @notice Callback from TokenWallet when tokens transfer reverted
/// @param collection Collection of received tokens
/// @param tokenId Unique token id
/// @param count Reverted tokens count
/// @param revertedFrom Address which declained acceptTransfer
function onMultiTokenBounceTransfer(
address collection,
uint256 tokenId,
uint128 count,
address revertedFrom
) external;
}

Burn callback

Smart contract that processes burn callback message must implement.

interface IMultiTokenBurnCallback {

/// @notice Callback from MultiToken wallet contract on burn tokens
/// @param collection Address of collection smart contract that mint MultiToken
/// @param tokenId Unique MultiToken id
/// @param count Burned MultiTokens count
/// @param token Address of MultiToken wallet contract that burns tokens
/// @param remainingGasTo Address specified for receive remaining gas
/// @param payload Additional data attached to transfer by sender
function onMultiTokenBurn(
address collection,
uint256 tokenId,
uint128 count,
address owner,
address token,
address remainingGasTo,
TvmCell payload
) external;
}

IDestroyable

interface IDestroyable {
/// @notice Destruct MultiToken and indexes
/// @param remainingGasTo Address specified for receive remaining gas
function destroy(address remainingGasTo) external;
}

Rationale

Metadata Choices

The symbol() function (found in the TIP-3 standard) was not included as we do not believe this is a globally useful piece of data to identify a generic virtual item / asset and are also prone to collisions. Short-hand symbols are used in tickers and currency trading, but they aren’t as useful outside of that space.

The name() function (for human-readable asset names, on-chain) was removed from the standard

The decimals() function was removed from the standard since the user can't own only fraction of an nft. In practice, we can only set it to zero. There is not need to return obvious value.

MultiTokenWallet destroy

It's possible to destroy MultiTokenWallet or/and indexes contracts when balance is zero. However it results in excessive complexity and additional Venom expenses. We can't handle onBounce if contract is destroyed during transfer. Therefore we need some mechnism to singal back that transfer is completed successfully. This will add no only complexity but additional fee. Besides there are many cases where zero balance is temporary (for example put on sale and cancel sale). As a result it makes sense to have an optional destroy and declare that the owner should decide whether destroy is needed or not.

Reuse interface from TIP-4.3

The TIP4_3NFT interface from TIP-4.3 standard is for MultiTokenWallet. This interface can be used as is and cover needed functinality.

Indexes

Index contracts from TIP-4.3 can help us to find MultiTokenWallet contracts as well as NFT. However they are not completely interchangeable. In order to find MultiTokenWallet only stamp is changed from "nft" to "fungible".

Visualization

Legend

Legend

Example 1: Put on sell by TIP-3 tokens

Put on sell by TIP-3 tokens

Example 2: Buy by TIP-3 tokens

Buy by TIP-3 tokens

Backwards compatibility

This standard is compatible with TIP-4.1, TIP-4.2, TIP-4.3 standards.

Security considerations

There are no security considerations related directly to the implementation of this standard.

Copyright and related rights waived via CC0.

References