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_esdt_transfers()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.