--- name: contracts description: Expert MultiversX Smart Contract Development (Rust). Use for writing, testing, optimizing, and auditing smart contracts. --- # MultiversX Smart Contract Expert This skill provides comprehensive capabilities for developing secure, gas-efficient smart contracts on MultiversX using Rust. ## Core Capabilities 1. **Smart Contract Implementation** * **Framework**: `multiversx-sc` * **Standards**: ESDT (Tokens), SFT/NFTs, Digital ID * **Patterns**: Data storage, Upgradability, Pausability 2. **Testing & Verification** * **Rust Scenario Tests**: Preferred method for end-to-end blockchain simulation using the `multiversx-sc-scenario` crate. * **RustVM**: High-speed unit testing. * **Property Testing**: Fuzzing for edge cases. 3. **Security & Auditing** * **Static Analysis**: Detecting common vulnerabilities. * **Gas Optimization**: Efficient storage and execution. * **Audit Readiness**: Code structure and best practices. ## πŸ› οΈ Development Workflow ### 1. New Contract Setup When creating a new contract, use `sc-meta` or `mxpy` templates. * **Structure**: ```text /meta # Build tool (meta-crate) - manages code generation and ABI /src # Source code - the contract logic trait /wasm # WASM entry point - links the logic to the VM interface /tests # Pure Rust tests (Unit & Scenario) /output # GENERATED after build (.wasm, .abi.json, .mxsc.json) mxpy.json # Python CLI configuration ``` ### 2. Coding Standards (Critical) * **Arithmetic**: ALWAYS use `checked_add`, `checked_mul`, etc. NEVER use standard operators (`+`, `-`, `*`) on user input. * **Storage**: * `SingleValueMapper`: Single items. * `VecMapper`: Lists (careful with iteration). * `UnorderedSetMapper`: O(1) checks. * **Optimization**: Do not use `VecMapper` for large datasets; use `MapMapper` or `UnorderedSetMapper`. * **Naming**: Endpoints and views MUST use **camelCase**. * **Reentrancy**: Use `#[reentrancy_lock]` on potentially dangerous endpoints. * **Payment**: ALWAYS specify `#[payable("*")]` or `#[payable("EGLD")]` if expecting tokens. ### 3. Testing * **Rust Scenarios**: Use Rust-based tests (`multiversx-sc-scenario`) instead of legacy JSON Mandos tests. * **RustVM**: Use `multiversx_sc_scenario::imports::*` for unit tests in `src/lib.rs` or `tests/`. ## πŸ“š Expert Resources (Deep Dive) For specialized tasks, referencing these expert modules: * **Security Audit Guidelines**: `skills/expert/mvx_dapp_audit/SKILL.md` * **Gas Optimization**: `skills/expert/mvx_sc_best_practices/SKILL.md` * **Testing Handbook**: `skills/expert/mvx_testing_handbook/SKILL.md` * **Sharp Edges/Gotchas**: `skills/expert/mvx_sharp_edges/SKILL.md` * **Static Analysis**: `skills/expert/mvx_static_analysis/SKILL.md` ## ⚑ Common Operations ### Token Transfers ```rust self.send().direct_esdt(&to, &token_id, 0, &amount); ``` ### Storage Mapping ```rust #[view(getMyValue)] #[storage_mapper("myValue")] fn my_value(&self) -> SingleValueMapper; ``` ### Event Logs ```rust #[event("deposit")] fn deposit_event(&self, #[indexed] user: &ManagedAddress, amount: &BigUint); ``` # Smart Contracts Base --- name: multiversx-smart-contracts description: Build MultiversX smart contracts with Rust. Use when app needs blockchain logic, token creation, NFT minting, staking, crowdfunding, or any on-chain functionality requiring custom smart contracts. --- # MultiversX Smart Contract Development Build, test, and deploy MultiversX smart contracts using Rust, sc-meta, and mxpy. ## Prerequisites Tools available on the VM: - **Rust** (version 1.83.0+) - **sc-meta** - Smart contract meta tool - **mxpy** - MultiversX Python CLI for deployment If sc-meta is not installed: ```bash cargo install multiversx-sc-meta --locked ``` ## Creating a New Contract Use sc-meta to scaffold a new contract from templates: ```bash # List available templates sc-meta templates # Create from template sc-meta new --template adder --name my-contract # Available templates: # - empty: Minimal contract structure # - adder: Basic arithmetic operations # - crypto-zombies: NFT game example ``` ## Project Structure After creating a contract, you get this structure: ``` my-contract/ β”œβ”€β”€ Cargo.toml # Dependencies β”œβ”€β”€ src/ β”‚ └── lib.rs # Contract code β”œβ”€β”€ meta/ β”‚ β”œβ”€β”€ Cargo.toml # Meta crate dependencies β”‚ └── src/ β”‚ └── main.rs # Build tooling entry β”œβ”€β”€ wasm/ β”‚ β”œβ”€β”€ Cargo.toml # WASM output config β”‚ └── src/ β”‚ └── lib.rs # WASM entry point └── tests/ # Rust-based tests (Unit & Scenarios) ``` ## Cargo.toml Configuration ```toml [package] name = "my-contract" version = "0.0.0" edition = "2021" [lib] path = "src/lib.rs" [dependencies.multiversx-sc] version = "0.54.0" [dev-dependencies.multiversx-sc-scenario] version = "0.54.0" ``` ## Basic Contract Structure ```rust #![no_std] use multiversx_sc::imports::*; #[multiversx_sc::contract] pub trait MyContract { #[init] fn init(&self, initial_value: BigUint) { self.stored_value().set(initial_value); } #[upgrade] fn upgrade(&self) { // Called when contract is upgraded } #[endpoint(add)] fn add(&self, value: BigUint) { self.stored_value().update(|v| *v += value); } #[view(getValue)] fn get_value(&self) -> BigUint { self.stored_value().get() } #[storage_mapper("storedValue")] fn stored_value(&self) -> SingleValueMapper; } ``` ## Core Annotations ### Contract & Module Level | Annotation | Purpose | |------------|---------| | `#[multiversx_sc::contract]` | Marks trait as main contract (one per crate) | | `#[multiversx_sc::module]` | Marks trait as reusable module | | `#[multiversx_sc::proxy]` | Creates proxy for calling other contracts | ### Method Level | Annotation | Purpose | |------------|---------| | `#[init]` | Constructor, called on deploy | | `#[upgrade]` | Called when contract is upgraded | | `#[endpoint]` | Public callable method | | `#[view]` | Read-only public method | | `#[endpoint(customName)]` | Endpoint with custom ABI name | | `#[view(customName)]` | View with custom ABI name | ### Payment Annotations | Annotation | Purpose | |------------|---------| | `#[payable("*")]` | Accepts any token payment | | `#[payable("EGLD")]` | Accepts only EGLD | | `#[payable("TOKEN-ID")]` | Accepts specific token | ### Event Annotations | Annotation | Purpose | |------------|---------| | `#[event("eventName")]` | Defines contract event | | `#[indexed]` | Marks event field as searchable topic | ## Storage Mappers ### SingleValueMapper Stores a single value. ```rust #[storage_mapper("owner")] fn owner(&self) -> SingleValueMapper; // Usage self.owner().set(caller); let owner = self.owner().get(); self.owner().is_empty(); self.owner().clear(); self.owner().update(|v| *v = new_value); ``` ### VecMapper Stores indexed array (1-indexed). ```rust #[storage_mapper("items")] fn items(&self) -> VecMapper; // Usage self.items().push(&value); let item = self.items().get(1); // 1-indexed! self.items().set(1, &new_value); let len = self.items().len(); for item in self.items().iter() { } self.items().swap_remove(1); ``` ### SetMapper Unique collection with O(1) lookup, preserves insertion order. ```rust #[storage_mapper("whitelist")] fn whitelist(&self) -> SetMapper; // Usage self.whitelist().insert(address); // Returns false if duplicate self.whitelist().contains(&address); // O(1) self.whitelist().remove(&address); for addr in self.whitelist().iter() { } ``` ### UnorderedSetMapper Like SetMapper but more efficient when order doesn't matter. ```rust #[storage_mapper("participants")] fn participants(&self) -> UnorderedSetMapper; ``` ### MapMapper Key-value pairs. **Expensive - avoid when iteration not needed.** ```rust #[storage_mapper("balances")] fn balances(&self) -> MapMapper; // Usage self.balances().insert(address, amount); let balance = self.balances().get(&address); // Returns Option self.balances().contains_key(&address); self.balances().remove(&address); for (addr, bal) in self.balances().iter() { } ``` ### LinkedListMapper Doubly-linked list for queue operations. ```rust #[storage_mapper("queue")] fn queue(&self) -> LinkedListMapper; // Usage self.queue().push_back(value); self.queue().push_front(value); self.queue().pop_front(); self.queue().pop_back(); ``` ### FungibleTokenMapper Manages fungible token with built-in ESDT operations. ```rust #[storage_mapper("token")] fn token(&self) -> FungibleTokenMapper; // Usage self.token().issue_and_set_all_roles(...); self.token().mint(amount); self.token().burn(amount); self.token().get_balance(); ``` ### NonFungibleTokenMapper Manages NFT/SFT/META-ESDT tokens. ```rust #[storage_mapper("nft")] fn nft(&self) -> NonFungibleTokenMapper; // Usage self.nft().nft_create(amount, &attributes); self.nft().nft_add_quantity(nonce, amount); self.nft().get_all_token_data(nonce); ``` ## Data Types ### Core Types | Type | Description | |------|-------------| | `BigUint` | Unsigned arbitrary-precision integer | | `BigInt` | Signed arbitrary-precision integer | | `ManagedBuffer` | Byte array (strings, raw data) | | `ManagedAddress` | 32-byte address | | `TokenIdentifier` | Token ID (e.g., "EGLD", "TOKEN-abc123") | | `EgldOrEsdtTokenIdentifier` | Either EGLD or ESDT token ID | | `EsdtTokenPayment` | Token ID + nonce + amount | ### Creating Values ```rust // BigUint let amount = BigUint::from(1000u64); let zero = BigUint::zero(); // ManagedBuffer (strings) let buffer = ManagedBuffer::from("hello"); // Address let caller = self.blockchain().get_caller(); // Token identifier let token = TokenIdentifier::from("TOKEN-abc123"); ``` ## Payment Handling ### Receiving EGLD ```rust #[payable("EGLD")] #[endpoint(depositEgld)] fn deposit_egld(&self) { let payment = self.call_value().egld_value(); let amount = payment.clone_value(); // process payment... } ``` ### Receiving Any Single Token ```rust #[payable("*")] #[endpoint(deposit)] fn deposit(&self) { let payment = self.call_value().single_esdt(); let token_id = payment.token_identifier; let nonce = payment.token_nonce; let amount = payment.amount; } ``` ### Receiving EGLD or Single ESDT ```rust #[payable("*")] #[endpoint(flexibleDeposit)] fn flexible_deposit(&self) { let payment = self.call_value().egld_or_single_esdt(); // Returns EgldOrEsdtTokenPayment } ``` ### Receiving Multiple Tokens ```rust #[payable("*")] #[endpoint(multiDeposit)] fn multi_deposit(&self) { let payments = self.call_value().all_esdt_transfers(); for payment in payments.iter() { // process each payment } } ``` ### Sending Tokens ```rust // Send EGLD self.tx() .to(&recipient) .egld(amount) .transfer(); // Send ESDT self.tx() .to(&recipient) .single_esdt(&token_id, nonce, &amount) .transfer(); // Send EGLD or ESDT (Unified) self.tx() .to(&recipient) .payment((token_id, nonce, amount)) .transfer(); ``` **CRITICAL:** You cannot send both EGLD and ESDT in the same transaction. ## Events ```rust #[event("deposit")] fn deposit_event( &self, #[indexed] caller: &ManagedAddress, #[indexed] token: &TokenIdentifier, amount: &BigUint, ); // Emit event self.deposit_event(&caller, &token_id, &amount); ``` ## Modules Split large contracts into modules: ```rust // In src/storage.rs #[multiversx_sc::module] pub trait StorageModule { #[storage_mapper("owner")] fn owner(&self) -> SingleValueMapper; } // In src/lib.rs mod storage; #[multiversx_sc::contract] pub trait MyContract: storage::StorageModule { #[init] fn init(&self) { self.owner().set(self.blockchain().get_caller()); } } ``` ## Error Handling ```rust // Using require! #[endpoint(withdraw)] fn withdraw(&self, amount: BigUint) { let caller = self.blockchain().get_caller(); require!( caller == self.owner().get(), "Only owner can withdraw" ); require!(amount > 0, "Amount must be positive"); } // Using sc_panic! if condition_failed { sc_panic!("Operation failed"); } ``` ## Building Contracts ### Build All Contracts in Workspace ```bash sc-meta all build ``` ### Build Single Contract ```bash cd my-contract/meta cargo run build ``` ### Build with Options ```bash # Build with locked dependencies sc-meta all build --locked # Debug build with WAT output cd meta && cargo run build-dbg ``` ### Build Output After building, find outputs in the `output/` folder. * **`.wasm`**: The binary deployed to the blockchain. * **`.abi.json`**: Interface definition for frontends and interaction tools. * **`.mxsc.json`**: Metadata used by the Rust scenario framework to locate the contract. * **`.imports.json`**: (Generated) lists dependencies for the VM. > [!TIP] > **First Build**: The first `sc-meta all build` will download a specific Rust toolchain and perform a full compile. This can take several minutes. Subsequent builds are incremental and much faster. ## Contract Lifecycle ### 1. Creation Always start from a template to ensure the cargo workspace is correctly linked: ```bash sc-meta new --template empty --name my-contract ``` ### 2. Deployment Deployment installs the code at a new address and runs `#[init]`. ```bash mxpy contract deploy \ --bytecode output/my-contract.wasm \ --proxy https://devnet-api.multiversx.com --chain D \ --pem wallet.pem --gas-limit 60000000 \ --arguments 1000 "hex:0011" \ --send ``` ### 3. Upgrading Upgrading replaces the binary but **preserves all storage**. * **Storage Reuse**: The new contract must use the same storage keys (`#[storage_mapper("key")]`) to access existing data. * **Init is NOT called**: Upgrades trigger the `#[upgrade]` function, NOT `#[init]`. ```rust #[upgrade] fn upgrade(&self, new_param: Option) { if let Some(val) = new_param { self.config_val().set(val); } } ``` ```bash mxpy contract upgrade
\ --bytecode output/my-contract.wasm \ --proxy https://devnet-api.multiversx.com --chain D \ --pem wallet.pem --gas-limit 60000000 \ --send ``` ## Testing Lifecycle (Deployment Tests) Don't just test endpoints; test the deployment flow in Rust: ```rust #[test] fn test_lifecycle() { let mut world = ScenarioWorld::new(); world.register_contract("mxsc:output/my-contract.mxsc.json", MyContract::ContractBuilder); // Deploy world.tx() .from("address:owner") .typed(my_contract_proxy::MyContractProxy) .init(initial_value) .code("file:output/my-contract.wasm") .new_address("address:contract") .run(); // Call world.tx() .from("address:user") .to("address:contract") .typed(my_contract_proxy::MyContractProxy) .my_endpoint(param) .run(); } ``` ## Testing ### Rust-Based Tests Create tests in `tests/` folder: ```rust use multiversx_sc_scenario::*; fn world() -> ScenarioWorld { let mut blockchain = ScenarioWorld::new(); blockchain.register_contract( "mxsc:output/my-contract.mxsc.json", my_contract::ContractBuilder, ); blockchain } #[test] fn test_deploy() { let mut world = world(); // world.run("scenarios/deploy.scen.json"); // Legacy JSON format // Preferred: Pure Rust-based scenario world.sc_deploy() .from("address:owner") .code("file:output/my-contract.wasm") .expect(ExpectStatus::ok()); } ``` ### Running Tests ```bash # Run all tests sc-meta test # Run specific test cargo test test_deploy ``` ## Deploying with mxpy ### Deploy New Contract ```bash mxpy contract deploy \ --bytecode output/my-contract.wasm \ --proxy https://devnet-api.multiversx.com \ --chain D \ --pem wallet.pem \ --gas-limit 60000000 \ --arguments 1000 \ --send ``` ### Upgrade Existing Contract ```bash mxpy contract upgrade \ --bytecode output/my-contract.wasm \ --proxy https://devnet-api.multiversx.com \ --chain D \ --pem wallet.pem \ --gas-limit 60000000 \ --send ``` ### Call Contract Endpoint ```bash mxpy contract call \ --proxy https://devnet-api.multiversx.com \ --chain D \ --pem wallet.pem \ --gas-limit 5000000 \ --function "add" \ --arguments 100 \ --send ``` ### Query View Function ```bash mxpy contract query \ --proxy https://devnet-api.multiversx.com \ --function "getValue" ``` ### Network Endpoints | Network | Proxy URL | Chain ID | |---------|-----------|----------| | Devnet | `https://devnet-api.multiversx.com` | D | | Testnet | `https://testnet-api.multiversx.com` | T | | Mainnet | `https://api.multiversx.com` | 1 | ## Advanced Patterns ### Cross-Contract Calls with Proxy ```rust // Define proxy trait #[multiversx_sc::proxy] pub trait OtherContract { #[endpoint] fn some_endpoint(&self, value: BigUint); } // Use proxy #[endpoint] fn call_other(&self, other_address: ManagedAddress, value: BigUint) { self.tx() .to(&other_address) .typed(other_contract_proxy::OtherContractProxy) .some_endpoint(value) .sync_call(); } ``` ### Async Calls with Callbacks ```rust #[endpoint] fn async_call(&self, other_address: ManagedAddress) { self.tx() .to(&other_address) .typed(other_contract_proxy::OtherContractProxy) .some_endpoint() .callback(self.callbacks().my_callback()) .async_call_and_exit(); } #[callback] fn my_callback(&self, #[call_result] result: ManagedAsyncCallResult) { match result { ManagedAsyncCallResult::Ok(value) => { // Handle success } ManagedAsyncCallResult::Err(err) => { // Handle error } } } ``` ### Token Issuance (Modular Approach) The recommended way to handle token issuance is by importing and inheriting the `EsdtModule` from the framework (`multiversx-sc-modules`). This module provides a unified `issue_token` method that can be used to issue any type of token on MultiversX (Fungible, NonFungible, SemiFungible, Meta, Dynamic). ```rust #[multiversx_sc::contract] pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule { // Note: Only Fungible and Meta tokens have decimals // Example: Issuing a Fungible Token #[payable("EGLD")] #[endpoint(issueFungible)] fn issue_fungible( &self, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer, num_decimals: usize, ) { // Calls the inherited issue_token method from EsdtModule self.issue_token( token_display_name, token_ticker, EsdtTokenType::Fungible, OptionalValue::Some(num_decimals), ); } // Example: Issuing an NFT #[payable("EGLD")] #[endpoint(issueNft)] fn issue_nft( &self, token_display_name: ManagedBuffer, token_ticker: ManagedBuffer, ) { self.issue_token( token_display_name, token_ticker, EsdtTokenType::NonFungible, OptionalValue::None, ); } } ``` ### Token Minting Similarly, the `EsdtModule` provides a `mint` method to create new units of a token that has already been issued by the contract. ```rust #[multiversx_sc::contract] pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule { #[endpoint(mintTokens)] fn mint_tokens(&self, amount: BigUint) { // Mints tokens using the inherited mint method from EsdtModule. // The token_id is managed by the module's storage. // For fungible tokens, the nonce is 0. self.mint(0, &amount); } } ``` ## Code Examples ### Crowdfunding Contract Pattern ```rust #![no_std] use multiversx_sc::imports::*; #[multiversx_sc::contract] pub trait Crowdfunding { #[init] fn init(&self, target: BigUint, deadline: u64, token_id: EgldOrEsdtTokenIdentifier) { self.target().set(target); self.deadline().set(deadline); self.token_identifier().set(token_id); } #[payable("*")] #[endpoint(fund)] fn fund(&self) { require!( self.blockchain().get_block_timestamp() < self.deadline().get(), "Funding period ended" ); let payment = self.call_value().egld_or_single_esdt(); require!( payment.token_identifier == self.token_identifier().get(), "Wrong token" ); let caller = self.blockchain().get_caller(); self.deposit(&caller).update(|deposit| *deposit += payment.amount); } #[endpoint(claim)] fn claim(&self) { require!( self.blockchain().get_block_timestamp() >= self.deadline().get(), "Funding period not ended" ); let caller = self.blockchain().get_caller(); let deposit = self.deposit(&caller).get(); if self.get_current_funds() >= self.target().get() { // Target reached - owner claims require!(caller == self.blockchain().get_owner_address(), "Not owner"); // Transfer funds to owner... } else { // Target not reached - refund depositors require!(deposit > 0, "No deposit"); self.deposit(&caller).clear(); self.send_tokens(&caller, &deposit); } } fn send_tokens(&self, to: &ManagedAddress, amount: &BigUint) { let token_id = self.token_identifier().get(); self.tx() .to(to) .egld_or_single_esdt(&token_id, 0, amount) .transfer(); } #[view(getCurrentFunds)] fn get_current_funds(&self) -> BigUint { let token_id = self.token_identifier().get(); self.blockchain().get_sc_balance(&token_id, 0) } #[storage_mapper("target")] fn target(&self) -> SingleValueMapper; #[storage_mapper("deadline")] fn deadline(&self) -> SingleValueMapper; #[storage_mapper("tokenIdentifier")] fn token_identifier(&self) -> SingleValueMapper; #[storage_mapper("deposit")] fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; } ``` ## Critical Knowledge ### WRONG: Using MapMapper when not iterating ```rust // WRONG - MapMapper is expensive (4*N + 1 storage entries) #[storage_mapper("balances")] fn balances(&self) -> MapMapper; ``` ### CORRECT: Use SingleValueMapper with address key ```rust // CORRECT - Efficient when you don't need to iterate #[storage_mapper("balance")] fn balance(&self, user: &ManagedAddress) -> SingleValueMapper; ``` ### WRONG: Large modules and functions ```rust // WRONG - Everything in one file #[multiversx_sc::contract] pub trait MyContract { // 500+ lines of code... } ``` ### CORRECT: Split into modules ```rust // CORRECT - Organized modules mod storage; mod logic; mod events; #[multiversx_sc::contract] pub trait MyContract: storage::StorageModule + logic::LogicModule + events::EventsModule { #[init] fn init(&self) { } } ``` ### WRONG: Duplicated error messages ```rust // WRONG - Duplicated strings increase contract size require!(amount > 0, "Amount must be positive"); require!(other_amount > 0, "Amount must be positive"); ``` ### CORRECT: Static error messages ```rust // CORRECT - Single definition const ERR_AMOUNT_POSITIVE: &str = "Amount must be positive"; require!(amount > 0, ERR_AMOUNT_POSITIVE); require!(other_amount > 0, ERR_AMOUNT_POSITIVE); ``` ### WRONG: Trying to send EGLD + ESDT together ```rust // WRONG - Impossible on MultiversX self.tx() .to(&recipient) .egld(&egld_amount) .single_esdt(&token, 0, &esdt_amount) // Cannot combine! .transfer(); ``` ### CORRECT: Separate transactions ```rust // CORRECT - Separate transfers self.tx().to(&recipient).egld(&egld_amount).transfer(); self.tx().to(&recipient).single_esdt(&token, 0, &esdt_amount).transfer(); ``` ## Documentation Links Always consult official documentation: - **Smart Contracts Overview**: https://docs.multiversx.com/developers/smart-contracts - **sc-meta Tool**: https://docs.multiversx.com/developers/meta/sc-meta - **Storage Mappers**: https://docs.multiversx.com/developers/developer-reference/storage-mappers - **Annotations**: https://docs.multiversx.com/developers/developer-reference/sc-annotations - **Payments**: https://docs.multiversx.com/developers/developer-reference/sc-payments - **Example Contracts**: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples - **Framework Repository**: https://github.com/multiversx/mx-sdk-rs ## Verification Checklist Before completion, verify: - [ ] Contract created with `sc-meta new --template