agentskills.codes
MV

mvx-sc-best-practices

Expert guidelines for developing, auditing, and optimizing MultiversX Smart Contracts (Rust).

Install

mkdir -p .claude/skills/mvx-sc-best-practices && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16057" && unzip -o skill.zip -d .claude/skills/mvx-sc-best-practices && rm skill.zip

Installs to .claude/skills/mvx-sc-best-practices

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Expert guidelines for developing, auditing, and optimizing MultiversX Smart Contracts (Rust).
93 charsno explicit “when” trigger

About this skill

MultiversX Smart Contract Best Practices

This skill provides expert-level guidance on writing secure, gas-efficient, and idiomatic Smart Contracts on MultiversX using the multiversx-sc framework.

1. Storage Optimization (Critical)

Storage is the most expensive resource.

  • SingleValueMapper: Use for individual items (flags, configs, IDs).
    • Gas: Cheapest (~1 slot write).
    • Pattern: #[storage_mapper("myValue")] fn my_value(&self) -> SingleValueMapper<MyType>;
  • VecMapper: Use for ordered lists where you need index access.
    • Warning: NEVER iterate a VecMapper on-chain if it can grow indefinitely. This is a DoS vector (Gas Loop).
    • Gas: Medium.
  • UnorderedSetMapper: Use for unique collections or whitelists.
    • Gas: Checks existence before insert. Good for O(1) membership checks.
  • MapMapper: AVOID unless strictly necessary.
    • Why: It uses a linked-list structure (4 storage writes per entry). It is ~4x more expensive than SingleValueMapper.
    • Alternative: If you don't need to iterate keys, use a SingleValueMapper keyed by a hash or composite key.

2. Security Patterns

Arithmetic Safety

  • Always use BigUint for tokens, prices, and financial math.
    • Why: Prevents overflow/underflow and matches the VM's native big int implementation.
  • Avoid u64/u32 for money. Only use them for loop counters or small IDs.

Reentrancy Protection

  • Checks-Effects-Interactions:
    1. Checks: Validate inputs (require!).
    2. Effects: Update storage (deduct balance, update state).
    3. Interactions: Send tokens or call other contracts.
  • Async Calls: MultiversX async calls are safer than synchronous calls regarding reentrancy of the same execution context, but state changes happen in a separate transaction (callback).
  • Callback Verification: Always validate the state in the #[callback] function. Do not assume the async call succeeded just because it was sent.

Access Control

  • Use #[only_owner] for admin functions.
  • For fine-grained control, use the only_admin module from the multiversx-sc-modules crate. It provides a standard implementation for managing multiple admins.

3. Data Flow & Testing

Transfer-Execute Pattern

  • When sending tokens to a contract, prefer MultiESDTNFTTransfer (built-in function) over 2 transactions (Approve + TransferFrom).
  • In the contract, use #[payable] to accept tokens and self.call_value().all() to inspect them.

Testing (Mandos/Scenarios)

  • Mandos (.scen.json) are mandatory for integration testing.
  • Cover all pathways:
    • Happy path.
    • Error path (expect status 4).
  • Whitebox Testing: Use #[cfg(test)] modules with multiversx_sc_scenario::imports::* to test internal functions without deploying.

4. Code Structure

  • Endpoints: Public functions #[endpoint].
  • Views: Read-only #[view].
  • Private: Helper functions (no annotation, or pure Rust).
  • Events: #[event] for indexing, but don't store critical data solely in events.

5. Common Pitfalls / "Sharp Edges"

  • Token Identifier Validation: Always validate token_id. Don't assume the user sent the correct token.
  • Gas Limit: Be aware of the block gas limit (1.5B gas). Large loops will revert.
  • Managed Types: Use ManagedBuffer, ManagedAddress, ManagedVec instead of standard Rust Vec, String to avoid serialization overhead.

6. Production Patterns (Advanced)

Cache-Based Gas Optimization

Use a Drop-trait cache struct to batch storage reads/writes:

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

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

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

Error Constants Organization

// errors.rs — static byte strings for gas efficiency
pub static ERROR_NOT_ACTIVE: &[u8] = b"Not active";
pub static ERROR_UNAUTHORIZED: &[u8] = b"Unauthorized";
pub static ERROR_ZERO_AMOUNT: &[u8] = b"Zero amount";

Event Trait Composition

#[multiversx_sc::module]
pub trait EventsModule {
    #[event("deposit")]
    fn deposit_event(&self, #[indexed] caller: &ManagedAddress, amount: &BigUint);
}

View Endpoint Separation

Keep all #[view] endpoints in a dedicated views.rs module for clarity.

Validation Module Pattern

Centralize all require! checks in a validation.rs module so security rules are auditable in one place.

Cross-Contract Storage Reads

Use #[storage_mapper_from_address("key")] to read other contracts' storage without async call overhead:

#[storage_mapper_from_address("reserve")]
fn external_reserve(&self, addr: ManagedAddress, token: &TokenIdentifier)
    -> SingleValueMapper<BigUint, ManagedAddress>;

Only works same-shard. Read-only. Key must match target contract exactly.

Search skills

Search the agent skills registry