内存代币账本模式Skill multiversx-vault-pattern

这个技能用于区块链智能合约开发中,通过内存数据结构(如ManagedMapEncoded和ManagedVec)高效追踪多步代币操作的中间余额,避免昂贵的存储写入,优化Gas使用,适用于DeFi应用、批量处理、原子交换等场景。关键词:内存代币记账、智能合约优化、多步操作、代币追踪、Gas节省、区块链开发。

智能合约 0 次安装 0 次浏览 更新于 3/21/2026

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 = &current - 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优化说明

  1. ManagedMapEncoded — 使用堆内存,而非存储。读写无Gas费用。
  2. O(N)代币移除 — 适用于典型多步流程中的<10个代币。
  3. 零余额清理 — 自动移除代币以保持账本紧凑。
  4. 批量初始化from_payments高效加载所有传入代币。

变体

生产仓库通过以下方式扩展此模式:

  • 结果链式传递 — 将前一步输出作为下一步输入
  • 百分比模式 — 基于PPM的部分金额提取
  • 选择性结算 — 仅返回特定代币,其余作为协议收入
  • 金额模式枚举 — 固定/百分比/全部/前一步结果,用于灵活的步骤定义