跳到主要内容

Gear Multiple Token (gMT)

介绍

用于管理多种代币类型的合约的标准接口。单个合约可以包括同质化代币、非同质化代币或其他配置(如半同质化代币)的任何组合。

这个想法很简单,可以创建一个智能合约接口,就可以代表和控制任何数量的同质化和非同质化的代币类型。这样一来,gMT 代币可以具有与 GFT 和 GNFT 代币相同的功能,甚至可以同时兼容两者。gMT 与 ERC-1155 是相似的。

multitoken 实现

每个 multi-token 合约必须支持的函数:

  • mint(to, []token_id, []metadata, []amounts) 是一个创建单个/多个新代币的函数(与金额数组中的相应供应)。元数据可以包括关于代币的任何信息:它可以是一个指向特定资源的链接,也可以是代币的描述,等等。元数据只存储在NFTs中。

  • burn(from, []token_id, []amounts) - 是一个函数,它从合约中删除指定数量的具有此 id 的代币;

  • transfer(to, []token_id, []amounts) - 是一个函数,允许你将 token 与 token_id 转移到 to 帐户;

  • approve/revoke approval(approved account, token_id) - 是一个函数,允许你将处置令牌的权利交给指定的批准的账户。这个功能在市场或拍卖会上很有用,因为当所有者想出售他的代币时,他们可以把它放在市场/拍卖会上,所以合约将能够在某个时候将这个代币发送给新的所有者。

  • balance(account) - 是返回用户拥有的不同令牌的 ID 和数量的函数;

gMT 合约的实现为 gear-lib/multitoken

要使用默认实现需要在 Cargo.toml 配置:

gear-contract-libraries = { path = "https://github.com/gear-dapps/gear-lib" }
derive_traits = { path = "https://github.com/gear-dapps/gear-lib/tree/master/derive" }

multitoken 合约的存储状态在结构 MTKState 中定义:

#[derive(Debug, Default)]
pub struct MTKState {
pub name: String,
pub symbol: String,
pub base_uri: String,
pub balances: BTreeMap<TokenId, BTreeMap<ActorId, u128>>,
pub approvals: BTreeMap<ActorId, BTreeMap<ActorId, bool>>,
pub token_metadata: BTreeMap<TokenId, TokenMetadata>,
// owner for nft
pub owners: BTreeMap<TokenId, ActorId>,
}

要重复使用默认结构,你需要派生出 MTKTokenState,并用 #[MTKStateKeeper] 属性标记相应的字段。

你也可以在 MTK 合约中添加字段。例如,在合约中添加所有者的地址,token_id 将跟踪当前的代币数量,供应量储存不同代币的铸造数量:

use derive_traits::{MTKCore, MTKTokenState, StateKeeper};
use gear_contract_libraries::multitoken::{io::*, mtk_core::*, state::*};

#[derive(Debug, Default, MTKTokenState, MTKCore, StateKeeper)]
pub struct SimpleMTK {
#[MTKStateKeeper]
pub tokens: MTKState,
pub token_id: TokenId,
pub owner: ActorId,
pub supply: BTreeMap<TokenId, u128>,
}

为了继承默认的逻辑功能,你需要派生出 MTKCore 。相应地,为了读取合约状态,你需要 MTKTokenState。 让我们来编写 MTK 合约的整体实现。首先,我们定义消息,它将初始化合约和合约将处理的消息。

#[derive(Debug, Encode, Decode, TypeInfo)]
pub struct InitMTK {
pub name: String,
pub symbol: String,
pub base_uri: String,
}

#[derive(Debug, Encode, Decode, TypeInfo)]
pub enum MTKAction {
Mint {
token_id: TokenId,
amount: u128,
token_metadata: Option<TokenMetadata>,
},
Burn {
token_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,
},
}

默认的 MTK 合约实现:

#[derive(Debug, Default, MTKTokenState, MTKCore, StateKeeper)]
pub struct SimpleMTK {
#[MTKStateKeeper]
pub tokens: MTKState,
pub token_id: TokenId,
pub owner: ActorId,
pub supply: BTreeMap<TokenId, u128>,
}

static mut CONTRACT: Option<SimpleMTK> = None;

#[no_mangle]
pub unsafe extern "C" fn init() {
let config: InitMTK = msg::load().expect("Unable to decode InitConfig");
let mut multi_token = SimpleMTK::default();
multi_token.tokens.name = config.name;
multi_token.tokens.symbol = config.symbol;
multi_token.tokens.base_uri = config.base_uri;
multi_token.owner = msg::source();
CONTRACT = Some(multi_token);
}

#[no_mangle]
pub unsafe extern "C" fn handle() {
let action: MTKAction = msg::load().expect("Could not load msg");
let multi_token = CONTRACT.get_or_insert(SimpleMTK::default());
match action {
MTKAction::Mint {
amount,
token_metadata,
} => MTKCore::mint(multi_token, token_id, amount, token_metadata),
MTKAction::Burn { token_id, amount } => MTKCore::burn(multi_token, token_id, amount),
MTKAction::BalanceOf { account, id } => {
MTKCore::balance_of(multi_token, vec![account], vec![id])
}
MTKAction::BalanceOfBatch { accounts, ids } => {
MTKCore::balance_of(multi_token, accounts, ids)
}
MTKAction::MintBatch {
ids,
amounts,
tokens_metadata,
} => MTKCore::mint(multi_token, &msg::source(), ids, amounts, tokens_metadata),
MTKAction::TransferFrom {
from,
to,
id,
amount,
} => MTKCore::transfer_from(multi_token, &from, &to, vec![id], vec![amount]),
MTKAction::BatchTransferFrom {
from,
to,
ids,
amounts,
} => MTKCore::transfer_from(multi_token, &from, &to, ids, amounts),
MTKAction::BurnBatch { ids, amounts } => MTKCore::burn(multi_token, ids, amounts),
MTKAction::Approve { account } => MTKCore::approve(multi_token, &account),
MTKAction::RevokeApproval { account } => MTKCore::revoke_approval(multi_token, &account),
}
}

部署 multitoken 合约

接下来,让我们重写 mintburn 函数的实现。mint 函数将为发送 Mint 消息的账户创建代币,并把元数据作为输入参数。至于 burn 功能 - 将与默认功能相同,但我们将其覆盖,因为我们希望从合约中销毁代币时处理供应量。

pub enum MTKAction {
Mint {
amount: u128,
token_metadata: Option<TokenMetadata>,
},
Burn {
id: TokenId,
amount: u128,
},
}

TokenMetadata 同样定义在 gear NFT library:

#[derive(Debug, Default, Encode, Decode, Clone, TypeInfo)]
pub struct TokenMetadata {
// ex. "CryptoKitty #100"
pub name: String,
// free-form description
pub description: String,
// URL to associated media, preferably to decentralized, content-addressed storage
pub media: String,
// URL to an off-chain JSON file with more info.
pub reference: String,
}

定义 1 个新方法,它将扩展默认的 MTKCore

pub trait SimpleMTKCore: MTKCore {
fn mint(&mut self, amount: u128, token_metadata: Option<TokenMetadata>);

fn burn(&mut self, id: TokenId, amount: u128);
}

编写实现:

impl SimpleMTKCore for SimpleMTK {
fn mint(&mut self, amount: u128, token_metadata: Option<TokenMetadata>) {
MTKCore::mint(
self,
&msg::source(),
vec![(self.token_id)],
vec![amount],
vec![token_metadata],
);
self.supply.insert(self.token_id, amount);
self.token_id = self.token_id.saturating_add(1);
}

fn burn(&mut self, id: TokenId, amount: u128) {
MTKCore::burn(self, vec![id], vec![amount]);
let sup = self.supply(id);
let mut _balance = self
.supply
.insert(self.token_id, sup.saturating_sub(amount));
}
}

因此,有必要对 handle 函数进行修改。

    // code before remains the same...
MTKAction::Mint {
amount,
token_metadata,
} => SimpleMTKCore::mint(multi_token, amount, token_metadata),
MTKAction::Burn { id, amount } => SimpleMTKCore::burn(multi_token, id, amount),
// code after remains the same...

总结

Gear 为 gMT 协议的核心功能提供了一个可重复使用的库。通过使用对象组合,该库可以在自定义的 gMT/MTK 合约实现中使用,减少可重复代码。

由 Gear 提供的智能合约源码在 Github 上可以找到:gear-lib/src/multitoken

关于 multitoken 源码在的 Github 上可以找到:multitoken/src

同样可以找到基于 gtest 实现的智能合约测试范例:multitoken/tests

更多关于在 Gear 上测试智能合约的细节,请参考这篇文章:程序测试