home / skills / multiversx / mx-ai-skills / mvx_cache_patterns

mvx_cache_patterns skill

/antigravity/skills/mvx_cache_patterns

This skill helps optimize MultiversX contracts by using drop-based write-back caches to minimize gas through selective reads and cached state.

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

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

Files (1)
SKILL.md
2.8 KB
---
name: mvx_cache_patterns
description: Drop-based write-back caches for gas optimization. Use for endpoints reading 3+ storage values.
---

# MultiversX Cache Patterns

## Why Cache?
- Storage operations are the most expensive in MultiversX contracts
- With cache: reads on entry + writes on exit, intermediate reads FREE (in-memory)

## Pattern 1: Write-Back Cache with Drop Trait

Load state into struct on entry, mutate in memory, commit on scope exit via `Drop`.

```rust
pub struct StorageCache<'a, C>
where
    C: crate::storage::StorageModule,
{
    sc_ref: &'a C,
    pub field_a: BigUint<C::Api>,
    pub field_b: BigUint<C::Api>,
    pub field_c: BigUint<C::Api>,
}

impl<'a, C> StorageCache<'a, C>
where
    C: crate::storage::StorageModule,
{
    pub fn new(sc_ref: &'a C) -> Self {
        StorageCache {
            field_a: sc_ref.field_a().get(),
            field_b: sc_ref.field_b().get(),
            field_c: sc_ref.field_c().get(),
            sc_ref,
        }
    }
}

impl<C> Drop for StorageCache<'_, C>
where
    C: crate::storage::StorageModule,
{
    fn drop(&mut self) {
        self.sc_ref.field_a().set(&self.field_a);
        self.sc_ref.field_b().set(&self.field_b);
        self.sc_ref.field_c().set(&self.field_c);
    }
}
```

## Pattern 2: Cache with Computed Methods

Add derived value methods on the cache struct — uses cached fields + contract module methods without extra storage reads:

```rust
impl<C> StateCache<'_, C>
where
    C: crate::storage::StorageModule + crate::math::MathModule,
{
    pub fn exchange_rate(&self) -> BigUint<C::Api> {
        if self.total_shares == 0u64 { return BigUint::from(1u64); }
        &self.total_deposited / &self.total_shares
    }
}
```

## Selective Write-Back

Only write back mutable fields — skip config/read-only fields in Drop to save gas.

## When to Use vs Direct Storage

| Scenario | Approach |
|---|---|
| Endpoint reads 3+ storage values | Use cache |
| Single storage read/write | Direct access is fine |
| View function reading multiple values | Read-only cache (no Drop) |
| Async call boundary | Manually drop cache BEFORE async call |

## Anti-Patterns

### 1. Caching Across Async Boundaries
```rust
// WRONG - async_call_and_exit() terminates execution, drop() never runs!
fn bad_async(&self) {
    let mut cache = StorageCache::new(self);
    cache.balance += &deposit;
    self.tx().to(&other).typed(Proxy).call()
        .callback(self.callbacks().on_done())
        .async_call_and_exit();
}

// CORRECT - manually drop cache before async call
fn good_async(&self) {
    {
        let mut cache = StorageCache::new(self);
        cache.balance += &deposit;
    } // cache.drop() fires here
    self.tx().to(&other).typed(Proxy).call()
        .callback(self.callbacks().on_done())
        .async_call_and_exit();
}
```

### 2. Forgetting Fields in Drop
### 3. Writing Back Immutable Config

Overview

This skill documents drop-based write-back cache patterns for MultiversX smart contracts to reduce gas costs when endpoints read multiple storage values. It provides a minimal, safe pattern that loads storage into an in-memory struct, mutates it during execution, and commits only on scope exit. The patterns include computed helper methods and selective write-back to avoid unnecessary storage writes.

How this skill works

On entry, the cache struct reads multiple storage entries into memory. Mutations happen on the in-memory fields. A Drop implementation writes changed fields back to storage when the cache goes out of scope, making intermediate reads free and reducing round-trip storage operations. Read-only variants skip Drop, and manual scoping is used to avoid crossing async boundaries.

When to use it

  • Endpoint reads 3 or more storage values in a single transaction
  • Complex mutative flows where many intermediate reads occur
  • View functions that read multiple values (use read-only cache without Drop)
  • When you want to minimize storage writes by committing only at the end
  • Before making async calls: ensure cache is dropped manually

Best practices

  • Only include fields that will be mutated in Drop; skip immutable config to save gas
  • Wrap cache usage in a limited scope so Drop always runs before async exits
  • Provide computed helper methods on the cache to derive values without extra storage reads
  • Explicitly document which fields are committed and which are read-only
  • Write tests verifying Drop commits expected fields and that no fields are forgotten

Example use cases

  • A deposit endpoint that reads total_shares, total_deposited, and user_balance and updates all three
  • A liquidation flow that needs multiple storage lookups and heavy intermediate math across entries
  • A view endpoint that computes exchange_rate from cached totals without committing anything
  • A batch update handler that aggregates many changes then writes back a small set of storage keys
  • Pre-async transfer logic: update balances, drop cache, then perform async external call

FAQ

What happens if I forget to drop the cache before an async_call_and_exit?

Drop will not run when execution terminates via async_call_and_exit, so writes will never happen. Always scope the cache so it goes out of scope and drops before initiating async termination.

Can I cache immutable configuration fields?

You can read immutable config into the cache for convenience, but do not write them back in Drop. Excluding them from Drop saves gas and avoids accidental writes.