Skip to content

M0 on Solana: Technical Deep Dive

This document provides a detailed technical overview of the M0 Protocol's implementation on the Solana blockchain. It is intended for developers who want to integrate with or build on top of $M and its extensions on Solana.

It assumes you are familiar with Solana's programming model (programs, accounts, CPIs) and have a conceptual understanding of M0's hub-and-spoke architecture.

Architecture & Core Components

M0's Solana deployment functions as a "spoke" in the cross-chain model, with Ethereum as the "hub". Communication and token transfers are managed by the M Portal, which is a customized implementation of Wormhole's Native Token Transfer (NTT) framework.

The system is composed of three core onchain programs and several offchain services that work in concert.

M0 Solana Program Architecture
The core onchain programs for M0 on Solana.

Onchain Programs

The onchain logic is modular, distributed across three main Solana programs written in Rust using the Anchor framework.

ProgramMainnet Program IDDevnet Program IDDescription
Portalmzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmYmzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmYThe gateway for bridging $M between EVM chains and Solana. A fork of Wormhole's NTT program with M0-specific payload support and executor integration.
EarnMzeRokYa9o1ZikH6XHRiSS5nD8mNjZyHpLCBRTBSY4cMzeRokYa9o1ZikH6XHRiSS5nD8mNjZyHpLCBRTBSY4cManages yield distribution and earner permissions for the base $M token with frozen-by-default security model.
M Extensions3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5da3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5daUnified extension framework (m_ext) compiled with feature flags for NoYield, ScaledUi, and Crank models.
Swap FacilityMSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzHMSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzHPermissioned router for atomic swaps between whitelisted M0 extensions (ext_swap program).

Key Onchain Assets

AssetMainnet Mint AddressDevnet Mint AddressDescription
$M TokenmzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJomzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6The base yield-bearing stablecoin, implemented as a Token-2022 asset with frozen-by-default security.
$wM TokenmzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJpmzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJpThe wrapped, 1:1 $M-backed extension using the Crank model, also a Token-2022 asset.

V2 Upgrades: What Changed

The V2 upgrade introduced seven major enhancements to the Solana implementation:

1. Direct Bridging to Extensions

What Changed: The Portal now supports a destination_token field in the AdditionalPayload, allowing users to bridge directly to extension tokens (like $wM) instead of only receiving base $M.

Why It Matters: Eliminates the need for a separate wrap transaction after bridging, improving UX and reducing costs.

Technical Implementation:
pub struct AdditionalPayload {
    pub index: u64,
    pub destination_token: [u8; 32],  // NEW: Target mint address (M or extension)
    pub earner_root: Option<[u8; 32]>,
}

2. Frozen-by-Default $M

What Changed: All new $M token accounts are initialized with AccountState::Frozen using Token-2022's DefaultAccountState extension.

Why It Matters: Enhances security by preventing unauthorized accounts from holding $M. Only approved earners (verified via Merkle proof) can have their accounts thawed.

Technical Implementation:
// M mint is created with DefaultAccountState::Frozen
let default_state = DefaultAccountState {
    state: AccountState::Frozen,
};

3. Onchain Yield Distribution

What Changed: Yield distribution is now fully onchain via the scaled-ui mechanism, replacing V1's centralized distribution.

Why It Matters: Increases transparency and decentralization. All yield calculations and distributions are verifiable onchain.

Technical Implementation:
  • Yield distributed via scaled-ui mechanism
  • Index updates propagated onchain
  • Extensions automatically sync on wrap/unwrap operations

4. Permissionless Index Updates

What Changed: The sync() instruction for extension index updates is now permissionless—anyone can call it. Additionally, anyone can bridge an index update, and extensions automatically sync on wrap/unwrap operations.

Why It Matters: Enables keeper bots and users to ensure yield indices are always up-to-date without relying on centralized services.

Who Can Call: Anyone (no signer required for sync, though accounts must be valid). Index updates can also be bridged permissionlessly.

5. Executor Support for Generic Bridge Messages

What Changed: Integration with Wormhole's executor pattern (executor-account-resolver-svm) enables arbitrary cross-chain instructions to be executed atomically with bridge transfers.

Why It Matters: Unlocks advanced cross-chain composability, such as "bridge and stake" or "bridge and swap" in a single transaction.

Technical Implementation:
  • Portal validates executor messages
  • Executor Account Resolver resolves required accounts
  • Arbitrary CPI instructions executed after mint

6. Unified Extension Framework

What Changed: All extensions (NoYield, ScaledUi, Crank) are now compiled from a single m_ext program using Rust feature flags.

Why It Matters: Reduces code duplication, simplifies auditing, and ensures consistent behavior across all extension types.

Feature Flags:
[features]
no-yield = []
scaled-ui = []
crank = []

7. Multi-Network Support (Fogo Compatibility)

What Changed: V2 architecture is designed to support both Solana and Fogo (Wormhole's native rollup) with minimal changes.

Why It Matters: Future-proofs the protocol for multi-network deployment and leverages Wormhole's native cross-chain capabilities.

Implementation: Feature flags enable Fogo-specific configurations without forking the codebase.

Program Specifications

1. Portal Program (programs/portal)

The Portal is the entry and exit point for $M into the Solana ecosystem.

Core Logic & Custom Payload

  • Core Logic: Based on Wormhole NTT, it validates Wormhole Verifiable Action Approvals (VAAs) and processes inbound/outbound transfers. The key inbound instruction is release_inbound_mint_multisig, which mints $M tokens and makes a CPI to the Earn program's propagate_index function.

  • Custom Payload (payloads/token_transfer.rs): The standard NativeTokenTransfer struct is extended with an AdditionalPayload. This is the most critical customization, allowing M0-specific data to be securely transmitted with every bridge transfer.

// programs/portal/src/payloads/token_transfer.rs
 
pub struct AdditionalPayload {
    pub index: u64,                      // Latest M Earning Index from Ethereum
    pub destination_token: [u8; 32],     // Target token mint (M or extension)
    pub earner_root: Option<[u8; 32]>,   // Merkle root of approved earners
}
  • index: The latest $M Earning Index from Ethereum.
  • destination_token: The target token mint address on Solana (e.g., $M or $wM). NEW in V2.
  • earner_root: An optional Merkle root of the official $M earner list, as determined by M0 Governance on Ethereum.

Executor Integration

V2 Portal integrates with Wormhole's executor pattern to enable generic cross-chain instructions:

// Executor message structure
pub struct ExecutorMessage {
    pub instruction_data: Vec<u8>,
    pub program_id: Pubkey,
    pub account_metas: Vec<AccountMeta>,
}

The executor account resolver (executor-account-resolver-svm) resolves required accounts and executes instructions atomically with bridge transfers.

2. Earn Program (programs/earn)

This program manages yield and permissions for the base $M token on Solana.

V2 Security Model: Frozen by Default

All $M token accounts are initialized with AccountState::Frozen. Accounts are only thawed after:

  1. User submits Merkle proof via add_registrar_earner
  2. Proof is verified against earner_merkle_root
  3. Account state is updated to AccountState::Initialized

State (state/)

  • Global Account (state/global.rs): A PDA seeded with b"global" that stores the program's configuration.

    • admin: The administrative authority.
    • earn_authority: A permissioned key that can call claim_for instructions.
    • index: The most recently propagated $M Earning Index.
    • claim_cooldown: The minimum time between yield claim cycles.
    • max_yield & distributed: Used to track and cap the amount of yield distributed in a cycle.
    • claim_complete: A flag indicating if the current claim cycle has finished.
    • earner_merkle_root: The Merkle root of the governance-approved earner list.
  • Earner Account (state/earner.rs): A PDA seeded with b"earner" and the user's token account address. It tracks an individual's earning status.

    • last_claim_index: The index at which the user last received yield.
    • user: The wallet address of the earner.
    • user_token_account: The specific token account that is earning yield.

Key Instructions (instructions/)

  • propagate_index: Called by the Portal via CPI. It updates the Global account's index and earner_merkle_root, and can initiate a new claim cycle if conditions are met (cooldown passed, new yield available).

  • add_registrar_earner: A permissionless instruction allowing a user to prove their eligibility to earn yield. The user provides a Merkle proof that their address is included in the earner_merkle_root. On success, thaws the frozen token account.

  • claim_for: A permissioned instruction called by the earn_authority. It calculates the yield owed to a specific earner since their last claim and mints new $M tokens to their account. NEW in V2: fully onchain distribution.

  • complete_claims: Called by the earn_authority to mark the end of a yield distribution cycle.

  • sync: NEW in V2. Permissionless instruction to update the earning index. Anyone can call this to keep indices current.

3. M Extension Framework (programs/m_ext)

NEW in V2: Unified extension framework replacing separate programs for each model.

Architecture

All extension models are compiled from a single m_ext program using Rust feature flags:

// Cargo.toml
[features]
no-yield = []
scaled-ui = []
crank = []
Deployed Instances:
  • NoYield Extensions: Compiled with --features no-yield
  • ScaledUi Extensions: Compiled with --features scaled-ui
  • Crank Extensions: Compiled with --features crank (used by $wM)

State Structures

ExtGlobal Account (shared across all models):
pub struct ExtGlobal {
    pub admin: Pubkey,
    pub earn_authority: Option<Pubkey>,  // Only for Crank model
    pub ext_mint: Pubkey,
    pub m_mint: Pubkey,
    pub m_earn_global_account: Pubkey,
    pub bump: u8,
    pub m_vault_bump: u8,
    pub ext_mint_authority_bump: u8,
    pub yield_config: YieldConfig,
    pub wrap_authorities: Vec<Pubkey>,
}
 
pub struct YieldConfig {
    pub variant: YieldVariant,  // NoYield | ScaledUi | Crank
    pub last_m_index: u64,
    pub last_ext_index: u64,
}
 
pub enum YieldVariant {
    NoYield,
    ScaledUi,
    Crank,
}
Crank-Specific State:
// Only present when compiled with 'crank' feature
pub struct EarnManager {
    pub manager: Pubkey,
    pub fee_bps: u64,
    pub fee_token_account: Pubkey,
    pub bump: u8,
}
 
pub struct Earner {
    pub user: Pubkey,
    pub user_token_account: Pubkey,
    pub recipient_token_account: Pubkey,
    pub earn_manager: Pubkey,
    pub last_balance: u64,
    pub last_index: u64,
    pub bump: u8,
}

Core Instructions (Shared)

  • initialize: Sets up the extension's global state
  • wrap(amount: u64): Wraps $M into extension token
  • unwrap(amount: u64): Unwraps extension token back to $M
  • add_wrap_authority / remove_wrap_authority: Manage wrap permissions
  • transfer_admin: Transfer admin control

Model-Specific Instructions

NoYield:
  • claim_fees(): Admin claims all accrued yield
ScaledUi:
  • sync(): Permissionless index update (updates scaled-ui rate)
  • set_fee(fee_bps: u64): Set optional fee (typically 0 for full pass-through)
Crank:
  • add_earn_manager / remove_earn_manager: Admin manages earn managers
  • add_earner / remove_earner: Earn manager manages earners
  • claim_for(user, snapshot_balance): Earn authority distributes yield
  • sync(): Update extension index from base M program
  • set_recipient: Earner sets custom yield recipient

4. Swap Facility (programs/ext_swap)

NEW in V2: Centralized router for atomic swaps between whitelisted extensions.

Global State

pub struct SwapGlobal {
    pub bump: u8,
    pub admin: Pubkey,
    pub whitelisted_unwrappers: Vec<Pubkey>,
    pub whitelisted_extensions: Vec<WhitelistedExtension>,
}
 
pub struct WhitelistedExtension {
    pub program_id: Pubkey,
    pub ext_global: Pubkey,
}

Core Instructions

  • swap(amount, remaining_accounts_split_idx): Atomic cross-extension swap
  • wrap(amount): Convert $M to extension
  • unwrap(amount): Convert extension to $M (requires whitelisted_unwrapper)
  • whitelist_extension / remove_whitelisted_extension: Admin manages extensions
  • whitelist_unwrapper / remove_whitelisted_unwrapper: Admin manages unwrap permissions
Security Model:
  • Only whitelisted extensions can participate in swaps
  • Only whitelisted unwrappers can call unwrap directly
  • All swaps maintain 1:1 peg

Developer Integration

Interacting via SDK

The recommended way to interact with the M0 Solana programs is through the official TypeScript SDK.

  • Installation: pnpm i @m0-foundation/solana-m-sdk
  • Core Classes: The SDK provides classes that abstract away the onchain complexity:
    • EarnAuthority: For admin-level actions like initiating claims and syncing indexes.
    • EarnManager: For managing a set of earners within an extension (Crank model).
    • Earner: For user-level actions and querying an individual's yield status.
Example: Building a Claim Transaction (V2 Crank Model)
import { EarnAuthority, Earner } from '@m0-foundation/solana-m-sdk';
import { Connection, PublicKey } from '@solana/web3.js';
import { createPublicClient, http } from 'viem';
 
// Setup clients
const connection = new Connection(RPC_URL);
const evmClient = createPublicClient({ transport: http(EVM_RPC_URL) });
 
// Load authority and earner
const auth = await EarnAuthority.load(connection, evmClient, M_EXT_PROGRAM_ID);
const earners = await auth.getAllEarners();
const earnerToClaim = earners[0];
 
// Build the claim instruction
const claimIx = await auth.buildClaimInstruction(earnerToClaim);
 
if (claimIx) {
    // build, sign, and send transaction with the instruction...
}
Example: Permissionless Sync (V2 ScaledUi Model)
import { Program } from '@coral-xyz/anchor';
 
// Anyone can call sync to update yield indices
const tx = await program.methods
  .sync()
  .accounts({
    extGlobal: extGlobalPDA,
    extMint: extMintAddress,
    mEarnGlobal: mEarnGlobalAddress,
    mVault: mVaultPDA,
    token2022Program: TOKEN_2022_PROGRAM_ID,
  })
  .rpc();
 
console.log('Synced yield:', tx);

Offchain Data via API

For historical data, analytics, and building dashboards, use the M0 Solana API, which is powered by a Substreams indexer.

  • Base URL (Mainnet): https://api-production-0046.up.railway.app
  • SDK: pnpm i @m0-foundation/solana-m-api-sdk
  • Key Endpoints:
    • /events/bridges: Get latest bridge events.
    • /events/index-updates: Get historical $M Index updates.
    • /token-account/{pubkey}/{mint}/claims: Get yield claim history for a specific token account.

Onchain Addresses

A full list of program IDs, mints, and other key accounts for both Mainnet and Devnet can be found in the Addresses resource page.

Fogo Network Support

NEW in V2: The architecture is designed to support Fogo, Wormhole's native rollup, alongside Solana.

What is Fogo?
  • Wormhole-native rollup optimized for cross-chain applications
  • SVM-compatible (runs Solana programs with minimal changes)
  • Optimized for low-latency cross-chain messaging
M0 on Fogo:
  • Same program logic as Solana deployment
  • Feature flags enable Fogo-specific optimizations
  • Seamless interoperability with Solana via Wormhole NTT

Deployment Status: Fogo support is live. Check the Addresses page for Fogo-specific program IDs.

Source Code & Audits

Next Steps

  • Build with $M: Integrate the base $M token into your Solana dApp
  • Create an Extension: Deploy your own $M-backed stablecoin using NoYield, ScaledUi, or Crank models