Vara Multiple Token
The Vara Multiple Token Standard is the analogue of ERC-1155 on Ethereum.
Introduction
A standard interface for programs that manage multiple token types. A single deployed program may include any combination of fungible tokens, non-fungible tokens or other configurations (e.g. semi-fungible tokens).
The idea is simple and seeks to create a program interface that can represent and control any number of fungible and non-fungible token types. In this way, the gMT token can do the same functions as gFT and gNFT token, and even both at the same time. Can be considered as analog of ERC-1155.
This article explains the programming interface, data structure, basic functions and explains their purpose. It can be used as is or modified to suit any custom scenarios. Anyone can easily create their own application and run it on the Vara Network. The source code is available on GitHub.
Multiple token implementation
Consider the main functionality of the program
- mint(id, amount, token_metadata) is a function that creates a new token with the given id for the account.
metadata
can include any information about the token: it can be a link to a specific resource, a description of the token, etc. Metadata is available only in case of non-fungible tokens (amount equals 1); - mint_batch(ids, amounts, tokens_metadata) is a function similar to the
mint
function, but it creates several tokens at the same time; - burn(id, amount) is a function that removes the token with the mentioned
id
andamount
from the program; - burn_batch(ids, amounts) is a function similar to the
burn
function, but it removes several tokens at the same time; - balance_of(account, id) is a function that provides information about the amount of tokens the specified
account
has with the givenid
; - balance_of_batch(accounts, ids) is a function similar to the
balance_of
function, but it returns info for multiple accounts and tokens; - transfer_from(from, to, id, amount) is a function that allows to make a token transfer from
token_id
to a quantityamount
; - batch_transfer_from(from, to, ids, amounts) is a function similar to the
transfer_from
function, but it allows the transfer of multiple tokens at once; - approve(account) - is a function that gives the
account
access to use tokens; - revoke_approval(account) - is a function that cancels an account's access to use tokens;
- transform(id, amount, nfts) - is a function that converts user tokens into multiple non-fungible tokens.
The multiple token program contains the following information:
pub struct SimpleMtk {
pub tokens: MtkData,
pub creator: ActorId,
pub supply: HashMap<TokenId, u128>,
}
where the MtkData
are defined as follows:
pub struct MtkData {
pub name: String,
pub symbol: String,
pub base_uri: String,
pub balances: HashMap<TokenId, HashMap<ActorId, u128>>,
pub approvals: HashMap<ActorId, HashSet<ActorId>>,
pub token_metadata: HashMap<TokenId, TokenMetadata>,
pub owners: HashMap<TokenId, ActorId>,
}
name
- multitoken namesymbol
- multitoken symbolbase_uri
- multitoken base URIbalances
- stores the ownership information of all tokensapprovals
- contains information about the approvals that have been madetoken_metadata
- token metadata relative to their id (only non-fungible tokens)owners
- owner of non-fungible tokens according to idcreator
- creator of the multitoken collectionsupply
- counting the amount of tokens issued
Initialization
To initialize a program, it needs to be passed name
, symbol
and base_uri
information:
pub struct InitMtk {
pub name: String,
pub symbol: String,
pub base_uri: String,
}
Action
pub enum MtkAction {
Mint {
id: TokenId,
amount: u128,
token_metadata: Option<TokenMetadata>,
},
Burn {
id: TokenId,
amount: u128,
},
BalanceOf {
account: ActorId,
id: TokenId,
},
BalanceOfBatch {
accounts: Vec<ActorId>,
ids: Vec<TokenId>,
},
MintBatch {
ids: Vec<TokenId>,
amounts: Vec<u128>,
tokens_metadata: Vec<Option<TokenMetadata>>,
},
TransferFrom {
from: ActorId,
to: ActorId,
id: TokenId,
amount: u128,
},
BatchTransferFrom {
from: ActorId,
to: ActorId,
ids: Vec<TokenId>,
amounts: Vec<u128>,
},
BurnBatch {
ids: Vec<TokenId>,
amounts: Vec<u128>,
},
Approve {
account: ActorId,
},
RevokeApproval {
account: ActorId,
},
Transform {
id: TokenId,
amount: u128,
nfts: Vec<BurnToNFT>,
},
}
Event
pub enum MtkEvent {
Transfer {
from: ActorId,
to: ActorId,
ids: Vec<TokenId>,
amounts: Vec<u128>,
},
BalanceOf(Vec<BalanceReply>),
Approval {
from: ActorId,
to: ActorId,
},
RevokeApproval {
from: ActorId,
to: ActorId,
},
}
Program implementation
#[no_mangle]
extern fn handle() {
let action: MtkAction = msg::load().expect("Failed to decode `MtkAction` message.");
let multi_token = unsafe { CONTRACT.as_mut().expect("`SimpleMtk` is not initialized.") };
let reply = match action {
MtkAction::Mint {
id,
amount,
token_metadata,
} => multi_token.mint(&msg::source(), vec![id], vec![amount], vec![token_metadata]),
MtkAction::Burn { id, amount } => multi_token.burn(vec![id], vec![amount]),
MtkAction::BalanceOf { account, id } => multi_token.balance_of(vec![account], vec![id]),
MtkAction::BalanceOfBatch { accounts, ids } => multi_token.balance_of(accounts, ids),
MtkAction::MintBatch {
ids,
amounts,
tokens_metadata,
} => multi_token.mint(&msg::source(), ids, amounts, tokens_metadata),
MtkAction::TransferFrom {
from,
to,
id,
amount,
} => multi_token.transfer_from(&from, &to, vec![id], vec![amount]),
MtkAction::BatchTransferFrom {
from,
to,
ids,
amounts,
} => multi_token.transfer_from(&from, &to, ids, amounts),
MtkAction::BurnBatch { ids, amounts } => multi_token.burn(ids, amounts),
MtkAction::Approve { account } => multi_token.approve(&account),
MtkAction::RevokeApproval { account } => multi_token.revoke_approval(&account),
MtkAction::Transform { id, amount, nfts } => multi_token.transform(id, amount, nfts),
};
msg::reply(reply, 0).expect("Failed to encode or reply with `Result<MtkEvent, MtkError>`.");
}
Mint
Requirements for a successful mint:
- Сan't be mined from a zero-address account
- Token ids must be unique
- Length
ids
,amounts
andmeta
must be the same - Сan't mint an nft that has already been created
- If a fungible token is minted, it should not have a metadata
fn mint(
&mut self,
account: &ActorId,
ids: Vec<TokenId>,
amounts: Vec<u128>,
meta: Vec<Option<TokenMetadata>>,
) -> Result<MtkEvent, MtkError> {
if *account == ActorId::zero() {
return Err(MtkError::ZeroAddress);
}
if ids.len() != amounts.len() || ids.len() != meta.len() {
return Err(MtkError::LengthMismatch);
}
let unique_ids: HashSet<_> = ids.clone().into_iter().collect();
if ids.len() != unique_ids.len() {
return Err(MtkError::IdIsNotUnique);
}
ids.iter().enumerate().try_for_each(|(i, id)| {
if self.tokens.token_metadata.contains_key(id) {
return Err(MtkError::TokenAlreadyExists);
} else if let Some(_token_meta) = &meta[i] {
if amounts[i] > 1 {
return Err(MtkError::MintMetadataToFungibleToken);
}
}
Ok(())
})?;
for (i, meta_item) in meta.into_iter().enumerate() {
self.mint_impl(account, &ids[i], amounts[i], meta_item)?;
}
for (id, amount) in ids.iter().zip(amounts.iter()) {
self.supply
.entry(*id)
.and_modify(|quantity| {
*quantity = quantity.saturating_add(*amount);
})
.or_insert(*amount);
}
Ok(MtkEvent::Transfer {
from: ActorId::zero(),
to: *account,
ids,
amounts,
})
}
fn mint_impl(
&mut self,
account: &ActorId,
id: &TokenId,
amount: u128,
meta: Option<TokenMetadata>,
) -> Result<(), MtkError> {
if let Some(metadata) = meta {
self.tokens.token_metadata.insert(*id, metadata);
// since having metadata = means having an nft, so add it to the owners
self.tokens.owners.insert(*id, *account);
}
let prev_balance = self.get_balance(account, id);
self.set_balance(account, id, prev_balance.saturating_add(amount));
Ok(())
}
Burn
fn burn(&mut self, ids: Vec<TokenId>, amounts: Vec<u128>) -> Result<MtkEvent, MtkError> {
if ids.len() != amounts.len() {
return Err(MtkError::LengthMismatch);
}
let msg_src = &msg::source();
ids.iter()
.zip(amounts.clone())
.try_for_each(|(id, amount)| {
if self.tokens.token_metadata.contains_key(id) && amount > 1 {
return Err(MtkError::AmountGreaterThanOneForNft);
}
self.check_opportunity_burn(msg_src, id, amount)
})?;
ids.iter()
.enumerate()
.for_each(|(i, id)| self.burn_impl(msg_src, id, amounts[i]));
for (id, amount) in ids.iter().zip(amounts.iter()) {
let quantity = self.supply.get_mut(id).ok_or(MtkError::WrongId)?;
*quantity = quantity.saturating_sub(*amount);
}
Ok(MtkEvent::Transfer {
from: *msg_src,
to: ActorId::zero(),
ids,
amounts,
})
}
fn burn_impl(&mut self, msg_source: &ActorId, id: &TokenId, amount: u128) {
self.tokens.owners.remove(id);
self.set_balance(
msg_source,
id,
self.get_balance(msg_source, id).saturating_sub(amount),
);
}
fn balance_of(&self, accounts: Vec<ActorId>, ids: Vec<TokenId>) -> Result<MtkEvent, MtkError> {
if accounts.len() != ids.len() {
return Err(MtkError::LengthMismatch);
}
let res = ids
.iter()
.zip(accounts)
.map(|(id, account)| BalanceReply {
account,
id: *id,
amount: self.get_balance(&account, id),
})
.collect();
Ok(MtkEvent::BalanceOf(res))
}
Transfer
Requirements for a successful transfer:
- The address of the sender and the recipient cannot be the same
- The sender is not the owner or approved account
- Can't make a transfer to a zero address
- Ids and amounts must have the same length
- The user's balance must contain the required number of tokens for the transfer
fn transfer_from(
&mut self,
from: &ActorId,
to: &ActorId,
ids: Vec<TokenId>,
amounts: Vec<u128>,
) -> Result<MtkEvent, MtkError> {
let msg_src = msg::source();
if from == to {
return Err(MtkError::SenderAndRecipientAddressesAreSame);
}
if from != &msg_src && !self.is_approved(from, &msg_src) {
return Err(MtkError::CallerIsNotOwnerOrApproved);
}
if to == &ActorId::zero() {
return Err(MtkError::ZeroAddress);
}
if ids.len() != amounts.len() {
return Err(MtkError::LengthMismatch);
}
for (id, amount) in ids.iter().zip(amounts.clone()) {
self.check_opportunity_transfer(from, id, amount)?;
}
for (i, id) in ids.iter().enumerate() {
self.transfer_from_impl(from, to, id, amounts[i])?;
}
Ok(MtkEvent::Transfer {
from: *from,
to: *to,
ids,
amounts,
})
}
fn transfer_from_impl(
&mut self,
from: &ActorId,
to: &ActorId,
id: &TokenId,
amount: u128,
) -> Result<(), MtkError> {
let from_balance = self.get_balance(from, id);
self.set_balance(from, id, from_balance.saturating_sub(amount));
let to_balance = self.get_balance(to, id);
self.set_balance(to, id, to_balance.saturating_add(amount));
Ok(())
}
Approve
fn approve(&mut self, to: &ActorId) -> Result<MtkEvent, MtkError> {
if to == &ActorId::zero() {
return Err(MtkError::ZeroAddress);
}
let msg_src = &msg::source();
self.tokens
.approvals
.entry(*msg_src)
.and_modify(|approvals| {
approvals.insert(*to);
})
.or_insert_with(|| HashSet::from([*to]));
Ok(MtkEvent::Approval {
from: *msg_src,
to: *to,
})
}
Revoke approval
fn revoke_approval(&mut self, to: &ActorId) -> Result<MtkEvent, MtkError> {
let msg_src = &msg::source();
let approvals = self.tokens.approvals.get_mut(msg_src).ok_or(MtkError::NoApprovals)?;
if !approvals.remove(to) {
return Err(MtkError::ThereIsNoThisApproval);
}
Ok(MtkEvent::RevokeApproval {
from: *msg_src,
to: *to,
})
}
Transform
fn transform(
&mut self,
id: TokenId,
amount: u128,
nfts: Vec<BurnToNFT>,
) -> Result<MtkEvent, MtkError> {
// pre-checks
let mut nft_count = 0;
for info in &nfts {
nft_count += info.nfts_ids.len();
}
if amount as usize != nft_count {
return Err(MtkError::IncorrectData);
}
// burn FT (not to produce another message - just simply use burn_impl)
let msg_src = &msg::source();
self.check_opportunity_burn(msg_src, &id, amount)?;
self.burn_impl(msg_src, &id, amount);
for burn_info in nfts.iter() {
if burn_info.account.is_zero() {
return Err(MtkError::ZeroAddress);
}
if burn_info.nfts_ids.len() != burn_info.nfts_metadata.len() {
return Err(MtkError::LengthMismatch);
}
}
let mut ids = vec![];
for burn_info in nfts {
burn_info
.nfts_metadata
.into_iter()
.zip(burn_info.nfts_ids.iter())
.try_for_each(|(meta, &id)| {
self.mint_impl(&burn_info.account, &id, NFT_COUNT, meta)
})?;
ids.extend_from_slice(&burn_info.nfts_ids);
}
let quantity = self.supply.get_mut(&id).ok_or(MtkError::WrongId)?;
*quantity = quantity.saturating_sub(amount);
Ok(MtkEvent::Transfer {
from: ActorId::zero(),
to: ActorId::zero(),
ids,
amounts: vec![NFT_COUNT; amount as usize],
})
}
Program metadata and state
Metadata interface description:
pub struct MultitokenMetadata;
impl Metadata for MultitokenMetadata {
type Init = In<InitMtk>;
type Handle = InOut<MtkAction, Result<MtkEvent, MtkError>>;
type Others = ();
type Reply = ();
type Signal = ();
type State = Out<State>;
}
To display the program state information, the state()
function is used:
#[no_mangle]
extern fn state() {
let contract = unsafe { CONTRACT.take().expect("Unexpected error in taking state") };
msg::reply::<State>(contract.into(), 0).expect(
"Failed to encode or reply with `<ContractMetadata as Metadata>::State` from `state()`",
);
}
Conclusion
The Multiple Token program source code is available on Github.
See also an example of the program testing implementation based on gtest
and gclient
: gear-foundation/dapps/contracts/multi-token/tests.
For more details about testing programs on Vara, refer to this article: Program Testing.