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 samenftId
. WherenftId
==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 idisEmpty
(bool
) - Balance empty flagcode
(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 idisEmpty
(bool
) - Balance empty flagcodeHash
(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 idowner
(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 idtoken
(address
) - Address of MultiToken wallet contractbalance
(uint128
) - Initial balanceowner
(address
) - Address of MultiToken ownercreator
(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 idcount
(uint128
) - Number of burned tokensowner
(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.
- With zero collection address
- 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 totrue
- On a non-zero balance,
isEmpty
must be set tofalse
- the collection address:
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 idowner
(address
) - The owner of the MultiTokencollection
(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 transferrecipient
(address
) - Address of owner of recipient MultiToken wallet contractdeployTokenWalletValue
(uint128
) - How much Venom send to MultiToken wallet contract on deploymentremainingGasTo
(address
) - Remaining gas receivernotify
(bool
) - Notify receiver on incoming transferpayload
(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 transferrecipientToken
(address
) - Recipient MultiToken wallet contract addressremainingGasTo
(address
) - Remaining gas receivernotify
(bool
) - Notify receiver on incoming transferpayload
(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 transfersender
(address
) - MultiToken wallet owner address that sends MultiTokensremainingGasTo
(address
) - Remaining gas receivernotify (
bool`) - Notify receiver on incoming transferpayload
(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 burnremainingGasTo
(address
) - Remaining gas receivercallbackTo
(address
) - Burn callback addresspayload
(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 idowner
(address
) - Address of MultiToken ownercollection
(address
) - The collection address that initiate MultiToken deploybalance
(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 MultiTokenssenderWallet
(address
) - Sender MultiToken wallet addressrecipient
(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
Example 1: Put on sell by TIP-3 tokens
Example 2: 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
Copyright and related rights waived via CC0.