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.zipInstalls 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).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
VecMapperon-chain if it can grow indefinitely. This is a DoS vector (Gas Loop). - Gas: Medium.
- Warning: NEVER iterate a
UnorderedSetMapper: Use for unique collections or whitelists.- Gas: Checks existence before insert. Good for
O(1)membership checks.
- Gas: Checks existence before insert. Good for
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
SingleValueMapperkeyed by a hash or composite key.
- Why: It uses a linked-list structure (4 storage writes per entry). It is ~4x more expensive than
2. Security Patterns
Arithmetic Safety
- Always use
BigUintfor tokens, prices, and financial math.- Why: Prevents overflow/underflow and matches the VM's native big int implementation.
- Avoid
u64/u32for money. Only use them for loop counters or small IDs.
Reentrancy Protection
- Checks-Effects-Interactions:
- Checks: Validate inputs (
require!). - Effects: Update storage (deduct balance, update state).
- Interactions: Send tokens or call other contracts.
- Checks: Validate inputs (
- 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_adminmodule from themultiversx-sc-modulescrate. 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 andself.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 withmultiversx_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,ManagedVecinstead of standard RustVec,Stringto 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.