home / skills / multiversx / mx-ai-skills / multiversx-project-architecture

multiversx-project-architecture skill

/skills/multiversx-project-architecture

This skill provides a production-grade MultiversX project structure guide to accelerate building, refactoring, and coordinating multi-contract systems.

npx playbooks add skill multiversx/mx-ai-skills --skill multiversx-project-architecture

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

Files (1)
SKILL.md
8.9 KB
---
name: multiversx-project-architecture
description: Production-grade project structure patterns for MultiversX smart contracts. Use when starting a new contract project, refactoring an existing one, or building multi-contract systems with shared code.
---

# MultiversX Project Architecture

Production-tested folder structures and module composition patterns for MultiversX contracts.

## Single Contract Structure

For contracts up to ~1000 lines of business logic:

```
my-contract/
├── Cargo.toml
├── src/
│   ├── lib.rs              # #[contract] trait — ONLY trait composition + init/upgrade
│   ├── storage.rs           # All #[storage_mapper] definitions
│   ├── views.rs             # All #[view] endpoints
│   ├── config.rs            # Admin configuration endpoints
│   ├── events.rs            # #[event] definitions
│   ├── validation.rs        # Input validation helpers
│   ├── errors.rs            # Static error constants
│   └── helpers.rs           # Pure business logic functions
├── meta/
│   ├── Cargo.toml
│   └── src/main.rs
├── wasm/
│   ├── Cargo.toml
│   └── src/lib.rs
└── tests/
    └── integration_test.rs
```

## Multi-Contract Workspace Structure

For protocols with multiple interacting contracts:

```
my-protocol/
├── Cargo.toml               # [workspace] members
├── common/                   # Shared crate across all contracts
│   ├── Cargo.toml
│   ├── constants/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs       # Protocol-wide constants
│   ├── errors/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs       # Shared error constants
│   ├── structs/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs       # Shared data types
│   ├── math/
│   │   ├── Cargo.toml
│   │   └── src/math.rs      # Shared math module trait
│   └── events/
│       ├── Cargo.toml
│       └── src/lib.rs       # Shared event definitions
├── contract-a/               # First contract
│   ├── Cargo.toml
│   ├── src/
│   │   ├── lib.rs
│   │   ├── storage/
│   │   │   └── mod.rs        # Local + proxy storage
│   │   ├── cache/
│   │   │   └── mod.rs        # Drop-based cache
│   │   ├── views.rs
│   │   ├── config.rs
│   │   ├── events.rs
│   │   ├── validation.rs
│   │   └── helpers/
│   │       └── mod.rs
│   ├── meta/
│   ├── wasm/
│   └── tests/
├── contract-b/
│   └── ...                   # Same structure
└── proxy-definitions/        # Optional: shared proxy traits
    ├── Cargo.toml
    └── src/lib.rs
```

## lib.rs Pattern: Trait Composition Only

The main contract file should ONLY compose modules — no business logic:

```rust
#![no_std]

multiversx_sc::imports!();

pub mod cache;
pub mod config;
pub mod events;
pub mod helpers;
pub mod storage;
pub mod validation;
pub mod views;

#[multiversx_sc::contract]
pub trait MyContract:
    storage::StorageModule
    + config::ConfigModule
    + views::ViewsModule
    + events::EventsModule
    + validation::ValidationModule
    + helpers::HelpersModule
    + common_math::SharedMathModule
{
    #[init]
    fn init(&self, /* params */) {
        // Only initialization logic
    }

    #[upgrade]
    fn upgrade(&self) {
        // Only upgrade migration logic
    }

    #[endpoint]
    fn main_operation(&self) {
        // Delegate to helpers/validation
        self.validate_payment(&payment);
        let mut cache = cache::StorageCache::new(self);
        self.process_operation(&mut cache, &payment);
        // cache drops and commits
    }
}
```

## errors.rs Pattern

Use `pub static` byte string references for gas-efficient error messages:

```rust
pub static ERROR_NOT_ACTIVE: &[u8] = b"Contract is not active";
pub static ERROR_INVALID_AMOUNT: &[u8] = b"Invalid amount";
pub static ERROR_UNAUTHORIZED: &[u8] = b"Unauthorized";
pub static ERROR_NOT_SUPPORTED: &[u8] = b"Not supported";
pub static ERROR_INSUFFICIENT_BALANCE: &[u8] = b"Insufficient balance";
pub static ERROR_ZERO_AMOUNT: &[u8] = b"Amount must be greater than zero";
```

Usage:
```rust
require!(amount > 0u64, ERROR_ZERO_AMOUNT);
```

This is more gas-efficient than inline string literals because the compiler deduplicates static references.

## events.rs Pattern

Define events as a separate module trait:

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

    #[event("withdraw")]
    fn withdraw_event(
        &self,
        #[indexed] caller: &ManagedAddress,
        #[indexed] token: &TokenId,
        amount: &BigUint,
    );
}
```

## validation.rs Pattern

Centralize all input validation:

```rust
#[multiversx_sc::module]
pub trait ValidationModule: crate::storage::StorageModule {
    fn validate_payment(&self, payment: &Payment<Self::Api>) {
        self.require_token_supported(&payment.token_identifier);
        self.require_amount_positive(payment.amount.as_big_uint());
    }

    fn require_token_supported(&self, token: &TokenId<Self::Api>) {
        require!(self.supported_tokens().contains(token), ERROR_NOT_SUPPORTED);
    }

    fn require_amount_positive(&self, amount: &BigUint) {
        require!(amount > &BigUint::zero(), ERROR_ZERO_AMOUNT);
    }
}
```

## When to Create a Common Workspace Crate

| Signal | Action |
|---|---|
| Same struct used in 2+ contracts | Move to `common/structs/` |
| Same math function in 2+ contracts | Move to `common/math/` |
| Same error messages across contracts | Move to `common/errors/` |
| Same event definitions | Move to `common/events/` |
| Protocol constants (precision, limits) | Move to `common/constants/` |

## Workspace Cargo.toml

```toml
[workspace]
members = [
    "contract-a",
    "contract-a/meta",
    "contract-b",
    "contract-b/meta",
    "common/structs",
    "common/math",
    "common/errors",
    "common/constants",
    "common/events",
]
```

Common crate Cargo.toml:
```toml
[package]
name = "common-structs"
version = "0.0.0"
edition = "2024"

[dependencies.multiversx-sc]
version = "0.64.0"
```

## SDK Standard Modules (`multiversx-sc-modules`)

Reusable modules provided by the SDK. Import in `Cargo.toml` and inherit in your contract trait.

```toml
[dependencies.multiversx-sc-modules]
version = "0.64.0"
```

| Module | Purpose | Import Path |
|---|---|---|
| `only_admin` | Admin-only access control (owner can add/remove admins) | `multiversx_sc_modules::only_admin` |
| `pause` | Pausable contract pattern (`#[endpoint] pause / unpause`) | `multiversx_sc_modules::pause` |
| `default_issue_callbacks` | Standard ESDT token issue/set-role callbacks | `multiversx_sc_modules::default_issue_callbacks` |
| `esdt` | Token issuance, minting, burning via unified `issue_token` | `multiversx_sc_modules::esdt` |
| `governance` | DAO proposals, voting, and execution | `multiversx_sc_modules::governance` |
| `bonding_curve` | Token pricing with bonding curve formulas | `multiversx_sc_modules::bonding_curve` |
| `token_merge` | NFT/SFT merging and splitting | `multiversx_sc_modules::token_merge` |
| `subscription` | Recurring payment subscriptions | `multiversx_sc_modules::subscription` |
| `staking` | Basic staking logic with rewards | `multiversx_sc_modules::staking` |
| `features` | Feature flags for contract capabilities | `multiversx_sc_modules::features` |
| `users` | User ID mapping (address to numeric ID) | `multiversx_sc_modules::users` |
| `ongoing_operation` | Long-running operation checkpointing | `multiversx_sc_modules::ongoing_operation` |
| `claim_developer_rewards` | Claim accumulated developer rewards | `multiversx_sc_modules::claim_developer_rewards` |
| `dns` | MultiversX DNS herotag registration | `multiversx_sc_modules::dns` |

Usage example:
```rust
#[multiversx_sc::contract]
pub trait MyContract:
    multiversx_sc_modules::only_admin::OnlyAdminModule
    + multiversx_sc_modules::pause::PauseModule
    + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule
{
    #[endpoint]
    fn admin_action(&self) {
        self.require_caller_is_admin();
        self.require_not_paused();
        // ...
    }
}
```

## Naming Conventions

| Item | Convention | Example |
|---|---|---|
| Contract crate | kebab-case | `liquidity-pool` |
| Module files | snake_case | `storage.rs`, `helpers.rs` |
| Storage keys | camelCase | `"totalSupply"`, `"feeRate"` |
| Error constants | SCREAMING_SNAKE | `ERROR_INVALID_AMOUNT` |
| Module traits | PascalCase | `StorageModule`, `ValidationModule` |
| Endpoint names | snake_case | `fn deposit_tokens(&self)` |
| View names | camelCase (ABI) | `#[view(getBalance)]` |

Overview

This skill provides production-tested project structures and module composition patterns for MultiversX smart contracts. It prescribes clear folder layouts for single-contract and multi-contract workspaces, plus coding patterns for lib.rs, errors, events, and validation modules. Use it to start new contracts, refactor existing code, or organize multi-contract protocols with shared crates.

How this skill works

The skill inspects common architectural needs and prescribes concrete layouts: a lightweight single-contract layout for projects under ~1000 lines, and a workspace layout for multi-contract systems with a shared common crate. It defines patterns for keeping lib.rs minimal (trait composition and init/upgrade only), gas-efficient error constants, centralized validation modules, event traits, and shared crates for constants, errors, structs, math, and events. It also shows when to extract shared code into common crates and how to declare workspace members in Cargo.toml.

When to use it

  • Starting a new MultiversX smart contract project
  • Refactoring a growing contract to improve maintainability
  • Designing a protocol with multiple interoperating contracts
  • Extracting shared types, errors, or math into reusable crates
  • Preparing code for audits and production deployment

Best practices

  • Keep lib.rs limited to trait composition, init, and upgrade functions
  • Centralize input validation and require checks in a validation module
  • Define reusable, gas-efficient errors as pub static byte strings
  • Split events into a dedicated module trait to keep ABI stable
  • Move any code used by 2+ contracts into a common workspace crate
  • Prefer SDK standard modules (multiversx-sc-modules) for common patterns

Example use cases

  • Single-token swap contract using the single-contract layout with storage.rs, views.rs, and helpers.rs
  • Protocol with staking and governance: separate contract crates plus common/structs and common/math
  • Refactor: extract identical error messages and structs from multiple contracts into common/errors and common/structs
  • Integrate SDK modules like pause and only_admin via trait inheritance in lib.rs
  • Write integration tests that reference workspace members and meta crates

FAQ

When should I create a common crate?

Create a common crate when the same struct, error, math function, or event is used by two or more contracts to avoid duplication and simplify upgrades.

Why use pub static byte strings for errors?

Static byte string references are gas-efficient because the compiler deduplicates them, reducing on-chain gas cost compared with inline string literals.