home / skills / multiversx / mx-ai-skills / mvx_vault_pattern

mvx_vault_pattern skill

/antigravity/skills/mvx_vault_pattern

This skill helps manage in-memory token ledgers for multi-step transactions, enabling fast balance tracking without persistent storage.

npx playbooks add skill multiversx/mx-ai-skills --skill mvx_vault_pattern

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.3 KB
---
name: mvx_vault_pattern
description: In-memory token ledger for tracking intermediate balances during multi-step operations within a single transaction.
---

# MultiversX In-Memory Token Ledger

## Problem

Multi-step token operations in one transaction need intermediate balance tracking without storage writes.

## When to Use

- Multi-step token operations in one tx (swaps, batch processing, atomic flows)
- NOT for single operations or state that must persist across transactions

## Core Pattern: Dual Data Structure

Uses **ManagedMapEncoded** (O(1) lookup) + **ManagedVec** (ordered iteration for settlement).

```rust
pub struct TokenLedger<M: VMApi> {
    balances: ManagedMapEncoded<M, TokenIdentifier<M>, BigUint<M>>,
    tokens: ManagedVec<M, TokenIdentifier<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: &TokenIdentifier<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: &TokenIdentifier<M>, amount: &BigUint<M>) -> BigUint<M> {
        let current = self.balance_of(token);
        require!(current >= *amount, "Insufficient ledger balance");
        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_all(&mut self, token: &TokenIdentifier<M>) -> BigUint<M> {
        let amount = self.balance_of(token);
        if amount > 0u64 { self.remove_token(token); }
        amount
    }

    pub fn balance_of(&self, token: &TokenIdentifier<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 amount > 0u64 {
                payments.push(Payment::new(token.clone_value(), 0u64, amount));
            }
        }
        payments
    }

    fn remove_token(&mut self, token: &TokenIdentifier<M>) {
        self.balances.remove(token);
        for (i, t) in self.tokens.iter().enumerate() {
            if t.as_managed_buffer() == token.as_managed_buffer() {
                self.tokens.remove(i);
                break;
            }
        }
    }
}
```

## Anti-Patterns

- Using storage for temporary balances (expensive, unnecessary)
- Not cleaning up zero-balance entries (wastes gas during settlement)
- Using only ManagedVec without a map (O(N) lookups)

## Variations

Production repos extend with: result chaining, PPM-based withdrawals, selective settlement (keeping dust as revenue), amount mode enums (Fixed/Percentage/All/PreviousResult).

Overview

This skill implements an in-memory token ledger for tracking intermediate token balances during multi-step operations inside a single transaction. It provides O(1) lookups and ordered iteration for efficient settlement without writing temporary state to storage. The pattern is optimized to minimize gas and avoid persistent state for ephemeral flows.

How this skill works

The ledger uses a dual data structure: a hash-like map for O(1) balance lookup and a vector for ordered token iteration. Deposits update the map and push new tokens into the vector. Withdrawals enforce ledger balance checks, remove tokens when zero, and maintain the vector for settlement. A settle operation converts current balances into a payment list for final execution.

When to use it

  • Atomic multi-step token operations within a single transaction (swaps, batch transfers, route hops)
  • When you need intermediate balance tracking but do not want storage writes
  • During gas-sensitive flows where minimizing storage operations matters
  • Not for state that must persist across transactions

Best practices

  • Keep the ledger ephemeral and confined to the transaction scope
  • Remove zero-balance tokens to avoid extra gas during settlement
  • Use the map+vector dual structure: map for lookups, vector for deterministic iteration
  • Validate sufficient ledger balance on withdraw to prevent underflow and logical errors
  • Consider variations like selective settlement or amount-mode enums for production features

Example use cases

  • Swap aggregator combining multiple token hops and tracking intermediate balances before final settlement
  • Batch payment processing within a single transaction, aggregating same-token amounts for fewer transfers
  • Atomic liquidity routing where temporary balances determine downstream transfers
  • Fee or dust capture by selectively not settling tiny leftover balances

FAQ

Why use a map and a vector together?

The map enables O(1) balance lookups while the vector preserves token order and enables efficient settlement iteration; using one alone leads to either O(N) lookups or unordered settlement.

Can this be used across transactions?

No. The ledger is designed for in-memory, transaction-local operations only; persistent state should use normal storage structures.