name: multiversx-vault-pattern description: 用于在单次交易内跟踪多步操作中间余额的内存代币记账模式。适用于构建处理顺序代币操作的任何合约——聚合器、批处理器、原子交换或多步DeFi流程。
MultiversX 内存代币账本
解决了什么问题?
当合约在单次交易中执行多个代币操作时(例如,交换A→B,然后B→C),您需要在不写入存储的情况下跟踪中间余额。存储写入昂贵,且对于仅在单次调用中存在的临时状态是不必要的。
何时使用
| 场景 | 使用账本? |
|---|---|
| 单次交易中的多步代币操作 | 是 |
| 需要跨顺序操作跟踪余额 | 是 |
| 单次存款/取款 | 否——过度设计 |
| 必须在交易间持久化的状态 | 否——使用存储 |
核心模式:双重数据结构
账本使用两种结构协同工作:
- ManagedMapEncoded — O(1)查找用于余额检查和更新
- ManagedVec — 用于结算的有序迭代(返回所有代币)
use multiversx_sc::api::VMApi;
pub struct TokenLedger<M: VMApi> {
balances: ManagedMapEncoded<M, TokenId<M>, BigUint<M>>,
tokens: ManagedVec<M, TokenId<M>>, // 跟踪插入顺序用于迭代
}
impl<M: VMApi> TokenLedger<M> {
pub fn new() -> Self {
Self {
balances: ManagedMapEncoded::new(),
tokens: ManagedVec::new(),
}
}
/// 从传入支付初始化
pub fn from_payments(payments: &PaymentVec<M>) -> Self {
let mut ledger = Self::new();
for payment in payments.iter() {
ledger.deposit(&payment.token_identifier, payment.amount.as_big_uint());
}
ledger
}
/// 增加代币余额
pub fn deposit(&mut self, token: &TokenId<M>, amount: &BigUint<M>) {
if !self.balances.contains(token) {
self.tokens.push(token.clone());
self.balances.put(token, amount);
} else {
let current = self.balances.get(token);
self.balances.put(token, &(current + amount));
}
}
/// 提取指定金额
pub fn withdraw(&mut self, token: &TokenId<M>, amount: &BigUint<M>) -> BigUint<M> {
let current = self.balance_of(token);
require!(current >= *amount, "账本余额不足");
let new_balance = ¤t - amount;
if new_balance == 0u64 {
self.remove_token(token);
} else {
self.balances.put(token, &new_balance);
}
amount.clone()
}
/// 提取百分比(百万分之一)
pub fn withdraw_percentage(&mut self, token: &TokenId<M>, ppm: u32) -> BigUint<M> {
let balance = self.balance_of(token);
let amount = (&balance * ppm) / 1_000_000u64;
if amount > 0u64 { self.withdraw(token, &amount) } else { BigUint::zero() }
}
/// 提取全部余额(避免零头)
pub fn withdraw_all(&mut self, token: &TokenId<M>) -> BigUint<M> {
let amount = self.balance_of(token);
if amount > 0u64 { self.remove_token(token); }
amount
}
/// 检查余额
pub fn balance_of(&self, token: &TokenId<M>) -> BigUint<M> {
if !self.balances.contains(token) {
return BigUint::zero();
}
self.balances.get(token)
}
/// 结算 — 将所有余额转换为支付对象进行转账
pub fn settle_all(&self) -> ManagedVec<M, Payment<M>> {
let mut payments = ManagedVec::new();
for token in self.tokens.iter() {
let amount = self.balances.get(&token);
if let Some(non_zero_amount) = NonZeroBigUint::new(amount) {
payments.push(Payment::new(token.clone_value(), 0u64, non_zero_amount));
}
}
payments
}
fn remove_token(&mut self, token: &TokenId<M>) {
self.balances.remove(token);
// O(N)扫描 — 适用于小代币集(通常<10)
for (i, t) in self.tokens.iter().enumerate() {
if t.as_managed_buffer() == token.as_managed_buffer() {
self.tokens.remove(i);
break;
}
}
}
}
使用:多步操作
#[endpoint(execute_steps)]
#[payable]
fn execute_steps(&self, steps: ManagedVec<YourStep<Self::Api>>) {
let payments = self.call_value().all();
let mut ledger = TokenLedger::from_payments(&payments);
for step in &steps {
// 从账本提取输入
let input_amount = ledger.withdraw(&step.input_token, &step.amount);
// 执行操作(交换、质押等)
let output = self.execute_step(&step, input_amount);
// 将结果存回账本
ledger.deposit(&output.token_identifier, output.amount.as_big_uint());
}
// 将所有剩余代币返回给调用者
let remaining = ledger.settle_all();
if !remaining.is_empty() {
self.tx().to(&self.blockchain().get_caller()).payment(&remaining).transfer();
}
}
使用正确类型结算
不良做法
// 不要:使用遗留类型结算 — BigUint允许零金额支付
fn settle_bad(&self) -> ManagedVec<EsdtTokenPayment> {
let mut payments = ManagedVec::new();
for token in self.tokens.iter() {
let amount = self.balances.get(&token);
payments.push(EsdtTokenPayment::new(token.into(), 0, amount)); // 发送零金额!
}
payments
}
良好做法
// 做:使用TokenId + NonZeroBigUint — 在类型层面跳过零余额
fn settle_good(&self) -> ManagedVec<Payment> {
let mut payments = ManagedVec::new();
for token in self.tokens.iter() {
let amount = self.balances.get(&token);
if let Some(nz) = NonZeroBigUint::new(amount) {
payments.push(Payment::new(token.clone_value(), 0u64, nz));
}
}
payments
}
反模式
1. 使用存储处理临时余额
// 错误 — 对于仅存在于单次交易中的状态进行昂贵的存储写入
#[storage_mapper("tempBalance")]
fn temp_balance(&self, token: &TokenId) -> SingleValueMapper<BigUint>;
2. 不清理零余额
// 错误 — 零余额代币在settle_all迭代中浪费Gas
pub fn withdraw(&mut self, token: &TokenId<M>, amount: &BigUint<M>) {
let new_balance = &self.balance_of(token) - amount;
self.balances.put(token, &new_balance); // 留下零条目!
}
3. 仅使用ManagedVec(无Map)
// 错误 — 每次余额检查O(N)查找
pub fn balance_of(&self, token: &TokenId<M>) -> BigUint<M> {
for (i, t) in self.tokens.iter().enumerate() {
if t == token { return self.amounts.get(i); }
}
BigUint::zero()
}
Gas优化说明
- ManagedMapEncoded — 使用堆内存,而非存储。读写无Gas费用。
- O(N)代币移除 — 适用于典型多步流程中的<10个代币。
- 零余额清理 — 自动移除代币以保持账本紧凑。
- 批量初始化 —
from_payments高效加载所有传入代币。
变体
生产仓库通过以下方式扩展此模式:
- 结果链式传递 — 将前一步输出作为下一步输入
- 百分比模式 — 基于PPM的部分金额提取
- 选择性结算 — 仅返回特定代币,其余作为协议收入
- 金额模式枚举 — 固定/百分比/全部/前一步结果,用于灵活的步骤定义