# Architecture overview ## The M0 architecture At the center of M0 is a shared infrastructure. Builders create **M0 Stablecoin Extensions** - custom stablecoins that inherit the security and liquidity of the underlying platform, but can be configured with their own brand, monetization model, transfer rules, and economic parameters. Stablecoin extensions are audited. They interoperate with every other M0-powered stablecoin through Onchain Orchestration. And they can be transferred cross-chain. Multiple regulated financial institutions (issuers) connect to the platform, pledge eligible collateral, and mint these application-specific stablecoins designed by builders. The result: you get the programmability and control of a custom stablecoin, without having to rebuild the monetary infrastructure from scratch. --- ## Why M0 ### For builders - Your own stablecoin, configured for your use case - not a white-label someone else controls. - Issuer portability: partner with any M0 issuer and retain the right to switch. - Shared liquidity across the M0 infrastructure from day one. - Audited, production-ready contracts as your starting point. - Trusted by PayPal, MetaMask, Kast, and others. ### For issuers - Enter stablecoin issuance without building proprietary infrastructure. - Go live in weeks, not months. - Issue across multiple blockchains and currencies from a single stack. - Near-real-time rewards distribution - Trusted by Bridge (Stripe), MoonPay, 1Money, and others. --- ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Start building your stablecoin on M0. ::: :::card --- icon: i-lucide-landmark title: Power Stablecoin Issuance to: https://docs.m0.org/issuers/overview --- Learn how qualified institutions connect to M0. ::: :: # About M0 The financial system runs on money designed for a different era. Base money, commercial bank money, fintech money - all of it fragmented, intermediated, and running on technology that predates the modern internet. Stablecoins were supposed to change that: money that moves as freely as information. But the first generation solved only a narrow version of the problem - a dollar on a blockchain, issued by a single company, with no real room for customization. The structures stablecoins were meant to replace simply reappeared in a new form. M0 was built to go further. We believe money should be plural. A business should be able to create the exact form of digital money its product demands, configure how that money behaves, and have it interoperate with other stablecoins out there - rather than inheriting someone else's constraints. That matters because everyone considering stablecoins as infrastructure faces the same limited options, and each path costs something you can't get back. 1. Build your own stack, and you take on massive investment, regulatory complexity, and a slow path to market. 2. Adopt someone else's stablecoin, and you get zero customization and no control over economics or UX. 3. Partner with a vertically integrated vendor, and you accept lock-in and forced bundling. M0 is the alternative: every layer is yours to configure, assign, and replace - so the money your product runs on is genuinely yours. ![Hub](https://docs.m0.org/images/the_m0_stack.png) --- ## How M0 works: the M0 stack A stablecoin's architecture has three foundational layers. M0 spans the entire lifecycle, and you choose what to build and what to plug in. **Application — design your stablecoin.** How your money behaves: who can hold and use it, what happens on interaction, how it complies, and how it's monetized. With [**Stablecoin Extensions**](https://docs.m0.org/build/overview), you start from a proven monetization template - Treasury, User Rewards, or Institution - then layer your own behavior on top with access and risk controls (including onchain screening via [Predicate](https://predicate.io/){rel=""nofollow""}) and, where templates don't fit, bespoke logic shipped as a native part of your stablecoin. **Distribution — integrate your stablecoin.** How your money reaches your use case and stays liquid. [**Onchain Orchestration**](https://docs.m0.org/build/accessing-liquidity) gives you scalable liquidity from day one (1:1 conversion against USDC and USDT), multi-chain reach without integrating bridges yourself, and interoperability with every other M0-powered stablecoin. **Issuance — issue your stablecoin.** What keeps the money worth what it should be: reserves, minting, and burning. M0 makes every path possible. Partner with a regulated issuer running a **Stablecoin Core** (issuer-based), build your stablecoin as a wrapper around onchain reserves (wrapper-based), or - depending on your use case and licensing - hold reserves and run a Stablecoin Core to become your own issuer. ::card --- icon: i-lucide-shield-check title: Stablecoin Features to: https://docs.m0.org/build/stablecoin-features --- Explore the full scope of features you can customize on your stablecoin. :: --- ## Why M0 - **Modularity** - Configure each element of the stablecoin stack independently. - **Optionality** - Change your mind, revisit designs, and upgrade partners as the business grows. - **Open architecture** - Use the full stack from the start, with the freedom to plug in specialized partners as needed. --- ## Who M0 is For ### Stablecoin Builders Builders are the application developers and product companies M0 is built for - teams embedding a branded, customized digital money instrument into their product because how their money behaves is part of their competitive advantage. They don't want to take on the infrastructure burden of building issuance from scratch, and they don't want vendor lock-in to avoid it. With M0, builders: - **Design** how their money behaves at the application layer - monetization model, access and risk controls, and bespoke logic - with Stablecoin Extensions. - **Integrate** with shared liquidity, multi-chain reach, and ecosystem-wide interoperability through Onchain Orchestration, liquid from day one. - **Issue** on the path that fits their use case and jurisdiction - partnering with an issuer powered by M0, or becoming their own. Builders stay in control of their product while M0 provides the monetary infrastructure underneath. ### Issuing Partners Issuance is a serious craft: licensing, reserves, compliance, and the trust that comes from doing all of it well. Issuing partners bring that craft to the builders who need it, running a Stablecoin Core to fulfill the regulated role only a financial institution can. For builders, a choice of issuing partners on a common standard means real optionality - the technology stack is standard, while each partner competes on the strength of its license, the quality of its reserves, the depth of its expertise, and the markets it can reach. ::card --- icon: i-lucide-rocket title: Launch a Stablecoin to: https://docs.m0.org/build/overview --- Everything you need to go from zero to a deployed, live stablecoin extension on M0. :: --- ## Explore M0 ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Everything you need to deploy your own stablecoin extension. ::: :::card --- icon: i-lucide-landmark title: Power Stablecoin Issuance to: https://docs.m0.org/issuers/overview --- Learn how qualified institutions connect to M0 as issuers. ::: :: # Platform mechanics The M0 platform powers builders of safe, programmable, interoperable stablecoins. It introduces a superior coordination mechanism that democratizes access to the generation and management of programmable, digital cash instruments. There are a few key areas to understand how value flows through the platform: ## Eligible Collateral With the goal of maintaining a standard issuance layer, as well as ensuring stablecoins built on M0 are interoperable, M0 indicates a list of portfolio components that are deemed eligible as reserve, or collateral, for stablecoins minted on the M0 platform. The spirit of those eligibility criteria remains that of ensuring that only the safest and regulatory aligned assets will continue to constitute the value reserve, limiting regulatory arbitrage in a scenario of international issuance. The current US regulatory landscape, thanks to the regulatory advancement brought forward by the GENIUS act, makes it much easier for the standardization of eligible collateral across multiple issuers. ## Mint Ratio The mint ratio defines the percentage of a issuer's collateral value that can be used to generate M0-powered stablecoins. It sets the required level of over-collateralization for each issuer and ensures that the system remains fully backed while accounting for operational and risk management needs. ## Minter Rate M0 is an accrual-based, rather than cash-based, system. The minter rate is a universal, continuously compounding rate applied to each issuer's outstanding issued supply on the platform. This rate is typically calibrated below the yield possibly obtained from Eligible Collateral to allow for coverage of operational costs and ensure there is a viable business for issuers. ## Earner Rate The protocol also sets an *Earner Rate* paid (only) to approved Earners on their holdings. Smart contracts ensure that value paid out via the Earner Rate does not exceed what accrued via the Minter Rate. When set equal to the Minter Rate-and 100% of holdings sit in approved earning extensions-100% of Issuer' payments flow through as rewards to Earners, continuously and programmatically. ## Automatic Fee Distribution All fees accumulate within the platform and are redistributed automatically via smart contracts. Issuers do not need to make manual payments. Instead, platform logic take care of internal accounting (indexing) and ensure that all risk and reward parameters flow consistently across all the different environments where M0-powered stablecoins exist. The superiority of an on-chain native, rather than off-chain manual, reward streaming system grows exponentially with the nesting of reward distribution logics, and the omni-chain expansion of all those use cases. Effectively, universal indexing allows the seamless management of exponentially complex reward streaming configuration. ## Extension Yield Allocation Each stablecoin extension defines how yield is allocated-whether directly to a treasury, to users, or through customized logic. The platform continuously streams rewards on-chain, allowing balances to grow incrementally instead of through periodic payouts. This transparency and programmability enable flexible product designs, such as real-time savings or incentive systems tailored to each partner's needs. # Liquidity A key benefit of the M0 infrastructure is the shared liquidity among all stablecoin extensions. Rather than relying on fragmented third-party aggregators or static onchain liquidity pools, M0 provides a unified liquidity layer that gives your stablecoin extension the most flexible and lowest friction access to liquidity from day one. ## What is the Onchain Orchestration service? Onchain Orchestration is M0's infrastructure for facilitating swaps and cross-chain transfers between M0 stablecoin extensions and other instruments. It enables users to move between desired quote tokens (such as USDC and USDT) and any M0-powered stablecoin via deterministic 1:1 swaps through a unified integration surface. The product: - **Aggregates multiple liquidity sources** built specifically for M0-powered stablecoins behind a single API. - **Supports cross-chain transfers** between any configured chains and across any M0-powered stablecoin extension. - **Enables any transaction size** through partial fills and solver inventory cycling. - **Leverages M0's balance sheet** so customers are able to partially bootstrap liquidity. ## How It Works The liquidity flow works in four stages: 1. **Client creates an order** through the [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview). 2. **Market Maker fills the order** using their onchain balance. 3. **Market Maker rebalances** with the Issuer as needed. 4. **Issuer balances their backbook** to replenish inventory. ## Onchain Orchestration Key Components ### Orchestration API The [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview) is the primary integration point for customers. It provides a single endpoint that: - **Returns quotes** for moving between M0 stablecoin extensions and other stablecoins such as USDC or USDT. - **Aggregates routes** from multiple providers (M0 Portals, M0 Swap Facility, Limit Order Protocol, third-party bridges and DEXs). - **Generates executable transaction payloads** for both EVM and Solana. - **Abstracts away** the complexity of multi-chain routing. ### Limit Order Protocol The [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) is the core settlement layer for moving in and out of stablecoins powered by M0 from other major stablecoins. It allows users to submit same-chain or cross-chain limit orders that are filled by approved solvers. Key features: - **Deterministic pricing** - client specify exact input and output amounts upfront. - **Partial fills** - solvers can cycle inventory for large orders. - **Cross-chain support** - orders created on the source chain, filled on any destination. - **Gasless orders** - client can sign orders without paying gas. - **Multi-chain support** across major EVM chains and Solana. ### M0 Swap Facility (MSF) M0 smart contracts that handle interoperability between M0 stablecoin extensions, enabling direct conversions between any two approved M0 stablecoin extensions. Key features: - **Direct 1:1 conversions** between any two M0 stablecoin extensions, with no slippage, trading fees, or AMM pools required. - **Permissionless for end users** to swap between standard stablecoin extensions. - **Restricted to approved extensions** on the M0 earner list, with configuration managed by M0. - **Atomic conversions** - they either complete fully or revert. ### Portal V2 [Portal V2](https://docs.m0.org/protocol/portal-v2) is M0's cross-chain bridge and messaging system. It underpins all cross-chain transfers of M0 stablecoin extension and serves as the trusted message relay that the Limit Order Protocol depends on to deliver fill and cancel reports between chains. Portal V2 uses a modular bridge adapter design - each adapter implements a standard interface and handles message delivery via a specific cross-chain messaging protocol. It currently supports adapters for **Wormhole**, **Hyperlane**, and **LayerZero**. ## Primary Market Liquidity (Direct with Issuers) For large-scale minting/redeeming of your stablecoin extension directly against supported stablecoins or USD. This involves an onboarding process with the Issuer of the desired stablecoin. Suitable for centralized businesses and zero-slippage constraints. ## Onchain Liquidity (DEXs) Through deep liquidity pools on major chains like Ethereum, Solana, and various L2s. These pools are typically against USDC and aim for efficient pricing. Ideal for decentralized applications and 24/7 access. | DEX Name | Pool Link | | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Uniswap | [ethereum/0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288](https://app.uniswap.org/explore/pools/ethereum/0x970A7749EcAA4394C8B2Bf5F2471F41FD6b79288){rel=""nofollow""} | | Raydium | [pool\_id=CsMzKUUJNoAoU7N4zh3hAS6qcByU81TcQMPJqCdmmcEF](https://raydium.io/clmm/create-position/?pool_id=CsMzKUUJNoAoU7N4zh3hAS6qcByU81TcQMPJqCdmmcEF){rel=""nofollow""} | ## Supported Routes The Orchestration API supports both same-chain swaps and cross-chain transfers between M0 stablecoin extensions and external stablecoins (USDC, USDT). The system currently operates across **Ethereum**, **Base**, **Arbitrum**, **Optimism**, **Solana**, **Plasma**, **Mantra**, and **Soneium**. Same-chain limit orders are live on **Base**, **Solana**, and **Ethereum**, with cross-chain routes and additional features actively expanding. Use the [`/supported-assets`](https://docs.m0.org/api-reference/orchestration/supported-assets) endpoint to get the current list of supported tokens and chains. ## Use Cases for Stablecoin Builders ### Inflows: Converting USDC/USDT to Your Stablecoin Extension Users can convert USDC or USDT into your M0 stablecoin extension 1:1 through the Orchestration API. The system handles routing through the optimal path - whether that's direct swaps, cross-chain bridges, or the limit order system. ### Outflows: Converting Your Stablecoin Extension to USDC/USDT Users can exit your extension back to USDC or USDT 1:1. The solver network ensures liquidity is available even for large redemptions, with the minter backbook providing additional depth. ### Converting Between M0 Stablecoin Extensions The Orchestration API can be used to convert your stablecoin directly into any other M0 stablecoin extension 1:1 via the M0 Swap Facility. ### Cross-Chain Transfers Move your stablecoin extension between supported chains. The system automatically handles bridging through M0's [Portal infrastructure](https://docs.m0.org/protocol/portal-v2) and third-party bridges as needed. ## Next Steps ::card-group :::card --- icon: i-lucide-gavel title: Limit Order Protocol to: https://docs.m0.org/protocol/limit-order-protocol --- Deep dive into the onchain settlement layer for cross-chain swaps. ::: :::card --- icon: i-lucide-route title: Portal V2 to: https://docs.m0.org/protocol/portal-v2 --- Technical documentation for M0's cross-chain bridge and messaging system. ::: :::card --- icon: i-lucide-code title: Orchestration API to: https://docs.m0.org/api-reference/orchestration/overview --- Full API reference for requesting quotes and transaction payloads. ::: :::card --- icon: i-lucide-link title: Cross-Chain to: https://docs.m0.org/build/cross-chain --- Guide to deploying and bridging stablecoins across networks. ::: :: # Integrating with the M0 Portals This guide provides technical details and step-by-step workflows for developers integrating with the M0 stablecoin bridging infrastructure. The system uses two distinct protocols for different chains, so it is crucial to use the correct integration path. - **Portal (NTT) Repository**: [m-portal on GitHub](https://github.com/m0-foundation/m-portal/blob/main/README.md){rel=""nofollow""} - **Portal Lite (Hyperlane) Repository**: [m-portal-lite on GitHub](https://github.com/m0-foundation/m-portal-lite/blob/main/README.md){rel=""nofollow""} The Portal system is built on the Wormhole Native Token Transfer (NTT) standard. It is used for bridging tokens to and from **Arbitrum One** and **Optimism**. ## **Key Concepts** - **Protocol:** Wormhole NTT - **Chains:** `Ethereum` ↔ `Arbitrum One`, `Ethereum` ↔ `Optimism` - **Chain IDs:** Uses **Wormhole Chain IDs** (`uint16`), e.g., Ethereum = `2`, Arbitrum = `23`, Optimism = `24`. - **Addresses:** Recipient and token addresses are passed as `bytes32`. ## **Contract Addresses** | Contract | Network(s) | Address | | ------------------------ | ------------------ | -------------------------------------------- | | **Hub Portal** | Ethereum | `0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd` | | **Spoke Portal** | Arbitrum, Optimism | `0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd` | | **Wormhole Transceiver** | All | `0x0763196A091575adF99e2306E5e90E0Be5154841` | ## M0 Portal - Hub to Spoke (e.g., Ethereum -> Arbitrum) This workflow details how to lock a token (M or wM) on Ethereum and mint its equivalent on a spoke chain like Arbitrum. ## M0 Portal - Spoke to Hub (e.g., Arbitrum -> Ethereum) This workflow is the mirror image of the first. You will interact with the Spoke Portal contract on the source chain (e.g., Arbitrum). 1. **Approve:** Call `approve` on the token contract on the spoke chain, granting an allowance to the Spoke Portal (`0xD92...28fd`). 2. **Quote Fee:** Obtain the delivery fee for sending a message from the spoke chain to Ethereum. 3. **Bridge:** Call `transferMLikeToken`on the Spoke Portal. - `destinationChainId`: `2` (Wormhole ID for Ethereum). - `destinationToken`: The `bytes32` address of the target token on Ethereum. The Portal Lite system is built on the Hyperlane protocol. It is used for bridging tokens to and from **Plume** and **HyperEVM**. ### **Key Concepts** - **Protocol:** Hyperlane - **Chains:** `Ethereum` ↔ `Plume`, `Ethereum` ↔ `HyperEVM` - **Chain IDs:** Uses standard **EVM Chain IDs** (`uint256`), e.g., Ethereum = `1`, Plume = `98866`. - **Addresses:** All addresses are standard `address` types. ### **Contract Addresses** | Contract | Network(s) | Address | | --------------- | ------------------------- | -------------------------------------------- | | **Portal Lite** | Ethereum, Plume, HyperEVM | `0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE` | ## M0 Portal Lite - Hub to Spoke (e.g., Ethereum -> Plume) This workflow details locking a token on Ethereum and minting its equivalent on Plume. ## M0 Portal Lite - Spoke to Hub (e.g., Plume -> Ethereum) The process is identical to Workflow 1 but performed on the spoke chain's contracts. 1. **Quote Fee:** Call `quoteTransfer` on the Portal Lite contract on Plume to get the fee for delivering a message to Ethereum (`destinationChainId = 1`). 2. **Approve:** Call `approve` on the token contract on Plume, granting an allowance to the Portal Lite (`0x36f...2ecE`). 3. **Bridge:** Call `transferMLikeToken` on the Portal Lite on Plume, with `destinationChainId` set to `1`. # Bridging M And wM Tokens This guide explains how to manually bridge `$M` and wrapped `$M` (`w$M`) tokens between Ethereum (the "Hub") and connected networks (the "Spokes") using a block explorer. **Important:** The bridging system uses two different portal contracts depending on the destination chain. Always use the correct Portal address for your transaction. **Prerequisites:** - A Web3 wallet (e.g., MetaMask). - Your wallet must be funded with the token you wish to bridge. - Your wallet must have the native currency of the source chain for gas fees (e.g., ETH on Ethereum). ## Addresses & Chain IDs ### Portal Contract Addresses *Which portal to use based on your destination chain* | Portal Type | Supported Chains | Contract Address | | --------------------------- | ------------------------------ | -------------------------------------------- | | **Portal (NTT/Wormhole)** | Arbitrum, Optimism | `0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd` | | **Portal Lite (Hyperlane)** | Plume, HyperEVM, Linea, Mantra | `0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE` | ### Token Addresses *Same addresses across all supported chains* | Token | Address | | --------------- | -------------------------------------------- | | **`$M` Token** | `0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b` | | **`w$M` Token** | `0x437cc33344a0B27A429f795ff6B469C72698B291` | ::note Note on Amounts: `$M` and `w$M` tokens use 6 decimal places. When entering an amount in a contract, add 6 zeros. To transfer 10 tokens, enter 10000000. :: ### Chain IDs | Chain | EVM Chain ID | Wormhole Chain ID | Hyperlane | | ---------------- | ------------ | ----------------- | --------- | | **Ethereum** | `1` | `2` | - | | **Arbitrum One** | `42161` | `23` | - | | **Optimism** | `10` | `24` | - | | **Plume** | `98866` | -- | Yes | | **HyperEVM** | `999` | -- | Yes | | **Mantra** | `5888` | -- | Yes | | **Linea** | `59144` | -- | Yes | ## **Part 1: Hub to Spoke (Bridging from Ethereum)** The process always involves two transactions: `approve` and `transferMLikeToken`. ### A. Bridging to `Arbitrum` or `Optimism` (via M0 Portal) This uses the **Portal (NTT/Wormhole)**. The steps are the same for bridging either `w$M` or `$M`. ### B. Bridging to `Plume` or `HyperEVM` (via M0 Portal Lite) This uses the **Portal Lite (Hyperlane)**. The steps are the same for bridging either `w$M` or `$M`. ## Part 2: Spoke to Hub (Bridging to Ethereum) This is the reverse process. You will perform the transactions on the source chain's block explorer (e.g., Arbiscan, Plume Explorer). - **From `Arbitrum` or `Optimism`**: 1. **Approve**: On Arbiscan/Optimistic Etherscan, go to the `$M` or `w$M` token and `approve` the **Portal (NTT)** address: `0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd`. 2. **Transfer**: Go to the Portal contract (`0xD92...28fd`) and call **`transferMLikeToken`**. Use a `destinationChainId` of `2` (Wormhole ID for Ethereum). - **From `Plume` or `HyperEVM`**: 1. **Approve**: On the source explorer, go to the `$M` or `w$M` token and `approve` the **Portal Lite** address: `0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE`. 2. **Transfer**: Go to the Portal Lite contract (`0x36f...2ecE`) and call **`transferMLikeToken`**. Use a `destinationChainId` of `1` (EVM ID for Ethereum). # Design templates M0 provides powerful, battle-tested templates to get you to market faster. These models are audited and have been used by dozens of builders for the most common use cases. Each one offers a different approach to handling the underlying rewards generated by eligible collateral, giving you the flexibility to build the exact product you need. ::tabs :::tabs-item{label="EVM Models"} ::::card --- icon: i-lucide-piggy-bank title: MYieldToOne to: https://docs.m0.org/build/treasury-model --- ## Treasury Model **For US / Regulated Issuance** All accrued rewards flow to a single designated treasury wallet. Simple, direct, and easy to reason about. **Good for:** Loyalty programs, B2B payment products, treasury management tools. Creates an ERC-20 token backed 1:1 by eligible collateral. All rewards are claimed by calling `claimYield()`, which mints new tokens to a single designated recipient. - **Rewards Claiming:** Anyone can call `claimYield()` to mint accumulated rewards to the configured recipient. - **Access Control:** Role-based permissions for managing the recipient address and freezing accounts. - **Source Code:** `MYieldToOne.sol` :::: ::::card --- icon: i-lucide-rocket title: JMIExtension to: https://docs.m0.org/build/treasury-jmi-overview --- ### Multi-Collateral Model **Just Mint It - For Offshore Issuance** JMIExtension inherits all the functionality of MYieldToOne - rewards flow to a single treasury wallet - with one important addition: users can mint your stablecoin extension instantly against whitelisted stablecoins such as USDC and USDT. This makes onboarding significantly faster and simpler for end users. **Good for:** Offshore stablecoin launches where frictionless user onboarding is a priority. - **Instant Minting:** Users mint your stablecoin directly against whitelisted stablecoins (USDC, USDT, DAI). - **Configurable Caps:** Set per-collateral limits to manage risk. - **All MYieldToOne Features:** Inherits rewards claiming, access control, and freezing capabilities. :::: ::: :: ::tabs-item{label="SVM Models"} :::card --- icon: i-lucide-piggy-bank title: NoYield to: https://docs.m0.org/build/svm-noyield-guide-overview --- ## Treasury Model Creates a stablecoin backed 1:1 by eligible collateral on Solana. The admin claims all accumulated rewards via the `claim_fees` instruction. **Good for:** Protocol treasuries and ecosystem funds on Solana. - **Yield Claiming:** Only the admin can call `claim_fees` to mint accumulated rewards to a chosen recipient. - **Token Standard:** Uses Token-2022 (or legacy Token Program for compatibility). - **Source Code:** [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} ::: :: \:: ## Choosing Your Path 1. **Choose your issuance geography:** - **US / Regulated** → Treasury Model (`MYieldToOne` ) - **Offshore** → Multi-Collateral Model (`JMIExtension` - recommended, or `MYieldToOne` ) 2. **Choose your chain**based on your ecosystem: - **EVM chains** for Ethereum, Base, Optimism, and other supported L2s. - **SVM chains** for Solana, Fogo, or other SVM-compatible networks. 3. **Review implementation details** by reading the deep-dive documentation for your chosen implementation. Ready to build? Check out the deployment guides linked above to get started. # Cross chain ## M0 on EVM M0 is designed for a multichain world, enabling M0 extensions to be accessible across various blockchain ecosystems while maintaining Ethereum as the authoritative source for governance. ### M0-Portals These are smart contracts facilitating the secure transfer of M0 stablecoin extensions (and critical M0 metadata like the rewards index and other parameters) between Ethereum and spoke chains. To achieve this, M0 integrates with leading cross-chain messaging protocols. Currently, M0 supports M0-Portals built on: - **Wormhole:** Leveraging its Native Token Transfer (NTT) framework. - **Hyperlane:** Utilizing its [general message passing](https://docs.hyperlane.xyz/docs/protocol/core/mailbox){rel=""nofollow""} for permissionless bridging. - **LayerZero:** Leveraging its Omnichain Fungible Token (OFT) framework. If your desired blockchain is not yet supported through these providers, please reach out to discuss custom integration possibilities. **Deeper Dive**: [M0-Portals (Cross-Chain Architecture)](https://docs.m0.org/protocol/m-portals) ## M0 on Solana The M0 platform extends its reach beyond EVM chains by offering a native deployment on the Solana blockchain. This provides the Solana ecosystem with direct access to secure stablecoin building blocks, unlocking new possibilities for high-performance DeFi, payments, and other applications. This is a native representation, inheriting its core properties of security and rewards accrual while leveraging Solana's speed and low transaction costs. ### How It Works: A High-Level View M0's presence on Solana is facilitated by the **M0 Portal**, which is built on **Wormhole's Native Token Transfer (NTT)** framework. This follows the same hub-and-spoke model used for EVM chains: 1. **Lock on Ethereum**: To move Extensions to Solana, the native token is locked in the `HubPortal` contract on Ethereum. 2. **Message with Wormhole**: A secure message is sent via the Wormhole network to Solana. 3. **Mint on Solana**: The Portal program on Solana receives the message and mints an equivalent amount of native Solana extension (an SPL token). This process ensures that every extension on Solana is fully backed by a corresponding token locked on Ethereum, maintaining the integrity of the total supply. ### Key Features for Solana Users & Developers - **Native Rewards**: Stablecoin extensions on Solana are designed to be rewards-accruing. The earning index from Ethereum is propagated via Wormhole, allowing Solana holders to benefit from the same underlying collateral rewards. - **High Performance**: Leverage Solana's sub-second finality and low fees for transactions. - **Deep Integration**: Extensions have the ability to be a native SPL token or follow token 2022 for more advanced functionality. This means that extensions can be seamlessly integrated into Solana's vibrant DeFi ecosystem, including DEXs, lending protocols, and derivatives platforms. ### Program Addresses You can find the relevant Solana program and token addresses in our central address registry: - [Solana Mainnet & Devnet Addresses](https://docs.m0.org/resources/addresses#solana) # Overview You want your own stablecoin. Your brand, your monetization model, your rules. M0 makes that possible without asking you to rebuild the entire monetary stack. **M0 Stablecoin Extensions** is a framework for custom stablecoins you deploy on top of the M0 platform. They inherit the platform's security, collateral guarantees, and shared liquidity - and you configure the rest: how rewards are distributed, who can hold and transfer, what the token is called. You don't need to build issuance infrastructure. You don't need to manage reserves. You partner with a licensed M0 Issuer who handles that responsibility, while you stay in control of the product. ::steps{level="3"} ### Decide on geographic issuance Before you write a line of code, talk to the M0 team. We'll help you understand which model fits your use case, which issuers you can partner with, and what the path to earner approval looks like. **For US issuance:** Get in touch so we can review your situation and walk through next steps together. **For offshore issuance:** We recommend starting with our Just Mint It (JMI) template - it's purpose-built for ease of onboarding and the fastest path to a live stablecoin. That said, you're free to use any of our other audited extension template contracts depending on your monetization model and use case. Current issuers powered by M0 include Bridge (Stripe), MoonPay, 1Money, and MXON (Offshore). [Get in Touch →](https://www.m0.org/contact-us){rel=""nofollow""} ### Choose your stablecoin extension template M0 offers two core audited, battle-tested stablecoin extension templates. The right one depends on your use case and context. :::card-group ::::card --- title: Treasury (MYieldToOne) to: https://docs.m0.org/build/treasury-model --- All accrued rewards flow to a single designated treasury wallet. Simple, direct, and easy to reason about. :::: ::::card --- title: Multi-Collateral (JMI) to: https://docs.m0.org/build/treasury-jmi-overview --- Inherits Treasury functionality with instant minting against whitelisted stablecoins like USDC and USDT. :::: ::: :: \:: [Models overview and comparison →](https://docs.m0.org/build/choosing-your-model) ### Customizing your stablecoin extension The M0 platform is designed to be flexible. The main areas builders typically configure are: 1. **Purpose & Branding** - Define your stablecoin's use case and whether it's publicly branded or an internal infrastructure component. 2. **Access Control** - Choose whether your extension is held and transferred permissionlessly or restricted to a whitelist. 3. **Rewards Distribution** - Decide how rewards flow: to a treasury, split between multiple parties, to token holders. 4. **Compliance Features** - M0 supports [Predicate](https://predicate.io/){rel=""nofollow""} integration as the preferred compliance partner on all EVM chains. 5. **Multi-Chain Deployment** - Extensions can be deployed across multiple blockchains. 6. **Advanced Rewards Management** *(Optional)* - Whitelist eligible rewards recipients, define multiple tiers, or redirect rewards. ::warning Any customization beyond the standard models requires an independent audit before deployment. Fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""} and we'll advise on next steps. :: ### Choose your target chain(s) M0's contracts are deployed on the following blockchains: **EVM:** Ethereum, Arbitrum, Base, Optimism, Linea, BNB, HyperEVM, Plume, Mantra, Soneium, Plasma, Citrea. **Non-EVM:** Solana (Mainnet + Devnet). M0 supports the following bridge providers: **LayerZero**, **Hyperlane**, and **Wormhole**. ::note Deploying on additional chains beyond your core choice may incur additional costs. If the chain you need isn't on this list, [get in touch](https://www.m0.org/contact-us){rel=""nofollow""} with our BD team to discuss terms and timeline for a custom deployment. :: [Full contract address list →](https://docs.m0.org/resources/addresses) ### Deploy your stablecoin extension Once you've chosen your model and target chains, follow the deployment guide: - [Treasury (MYieldToOne) deployment guide →](https://docs.m0.org/build/treasury-guide) - [Multi-Collateral (JMIExtension) deployment guide →](https://docs.m0.org/build/treasury-jmi-guide) - [Treasury (SVM) deployment guide →](https://docs.m0.org/build/svm-noyield-guide) ::warning Our contracts are audited and battle-tested. If you need custom behavior beyond the standard models, fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""} first. :: ### Get earner approval & enable earning For your extension to accrue rewards, its contract address must be approved as an M0 Earner through on-chain configuration. 1. Deploy your extension contract. 2. Prepare your earner approval proposal. 3. Submit via the [M0 Governance Portal](https://governance.m0.org/proposal/create){rel=""nofollow""}. 4. Approval is configured on-chain. Once configuration is confirmed, call `enableEarning()` on your deployed contract. [Full earner approval guide →](https://docs.m0.org/protocol/roles) ### Enable user access Once earning is live, users can convert into your stablecoin extension (and back) through M0's Onchain Orchestration - no custom swap interface needed. DEX aggregators, wallet providers, and integration partners handle the UX automatically. Your stablecoin extension only needs to implement two functions: `wrap()` and `unwrap()`, which are called exclusively by SwapFacility. \:: ## FAQ for builders ::accordion :::accordion-item{label="Do I need smart contract experience? "} Yes. Deploying an extension requires familiarity with Solidity and EVM tooling. If you're not technical, talk to us and we'll connect you with integration support. ::: :::accordion-item{label="What if I want a model that doesn't exist yet?"} Fill in the [intake form](https://www.m0.org/intake-form){rel=""nofollow""}. Custom models are possible but require a conversation before you start building. ::: :::accordion-item{label="Is the code audited?"} All pre-built models are audited. [View audit reports →](https://docs.m0.org/resources/audits) ::: :::accordion-item{label="What chains are supported?"} Ethereum mainnet, multiple L2s, additional L1s, and Solana. M0 uses [Portals](https://docs.m0.org/protocol/m-portals) for cross-chain interoperability. See the full [address list](https://docs.m0.org/resources/addresses). ::: :::accordion-item{label="How do I get liquidity from day one?"} Shared liquidity is a core benefit of building on M0. When your extension is approved, it's connected to the SwapFacility and has access to the ecosystem's liquidity on Mainnet or Solana from launch. ::: :: ## Related ::card-group :::card --- icon: i-lucide-link title: Cross-chain to: https://docs.m0.org/build/cross-chain --- Deploy and bridge stablecoins across multiple networks. ::: :::card --- icon: i-lucide-droplets title: Accessing Liquidity to: https://docs.m0.org/build/accessing-liquidity --- Connect to liquidity sources in the M0 ecosystem. ::: :: # Design customization M0 empowers builders to tailor a stablecoin precisely to their application's needs, leveraging the security and rewards mechanism of the M0 platform while giving them full control over the stablecoin's behavior and features. A stablecoin extension is its own smart contract that allows one to define unique rules, branding, and value flows, all built upon M0's pre-determined templates and robust infrastructure. ## Key Design Decisions Before writing code, consider these crucial aspects. With M0 you will be able to customize your stablecoin extension to fit any needs below. Your choices will define your stablecoin's functionality and value proposition. ### 1. Purpose & Branding - What will your stablecoin be used for (e.g., payments, in-app currency, rewards, DeFi collateral)? - Will it be publicly branded (e.g., YourAppUSD) or an internal/infrastructure component? ### 2. Access Control - Will your extension be held and transferred permissionlessly (publicly accessible) or restricted to a specific whitelist of users/contracts? ### 3. Rewards Distribution - If your extension contract is approved as an M0 Earner, it will accrue rewards. How will rewards be claimed? - **As revenue to a treasury / business / foundation**? - **Split between multiple parties?** (e.g. distributors, or holders and a treasury) - **As rewards to token holders?** (e.g. via rebasing balances or a claimable mechanism) M0's design gives you [complete flexibility to implement your desired monetization flow](https://docs.m0.org/build/overview). ### 4. Compliance Features - There are compliance requirements in all stablecoin extensions. M0 supports [Predicate](https://predicate.io/){rel=""nofollow""} integration as the preferred partner on all EVM chains. You can build these directly into your extension. ### 5. Multi-Chain Deployment - What blockchains does your stablecoin need to live in? Extensions can be deployed across multiple networks, ensuring your stablecoin token can natively exist and be held on other chains as needed. ### 6. Advanced Rewards Management (Optional) - For more sophisticated use cases, partners can implement advanced controls, such as whitelisting eligible rewards recipients, defining multiple rewards tiers, or redirecting rewards from LP contracts to alternate addresses. **Deeper Dive**: [M0 Extensions](https://docs.m0.org/protocol/extensions) ## Next Steps ::card-group :::card --- icon: i-lucide-rocket title: Design Your Stablecoin to: https://docs.m0.org/build/overview --- Choose a model and deploy your own stablecoin extension. ::: :::card --- icon: i-lucide-landmark title: Integrate Your Stablecoin to: https://docs.m0.org/build/accessing-liquidity --- Source liquidity directly into your use case. ::: :: # Implementation guide ## Getting Started ::note The SVM deployment guide is currently being finalized. For immediate support deploying a NoYield extension on Solana, please [get in touch](https://www.m0.org/contact-us){rel=""nofollow""} with the M0 team. :: ### 1. Clone the Repository ```bash git clone https://github.com/m0-foundation/solana-m-extensions.git cd solana-m-extensions ``` ### 2. Setup Your Environment Follow the setup instructions in the repository's README to install Anchor, Solana CLI, and other dependencies. ### 3. Configure Your Extension Configure your extension parameters including: - **Token name and symbol** for your stablecoin. - **Admin address** that will control rewards claiming and configuration. - **Token program** choice (Token-2022 recommended, legacy Token Program supported). ### 4. Deploy and Test 1. Deploy to Solana Devnet first for testing. 2. Verify all functionality including `claim_fees` . 3. Deploy to Solana Mainnet once validated. ### 5. Enable Earning After deployment, your extension's vault must be registered as an earner. The vault then begins to accrue rewards. The admin can call `claim_fees` at any time to collect accumulated rewards. ## Next Steps - [Gaining Earner Approval](https://docs.m0.org/protocol/roles) - Get your extension approved. - [Contract Addresses](https://docs.m0.org/resources/addresses) - View deployed Solana program addresses. - [Cross-Chain Interoperability](https://docs.m0.org/build/cross-chain) - Bridge your extension across chains. # Overview Deploy a Treasury Model stablecoin on Solana using the `NoYield` extension. All accrued rewards are captured by the admin via the `claim_fees` instruction. If you haven't already, review the [**Choosing Your Model**](https://docs.m0.org/build/choosing-your-model) page for context on how NoYield fits into the M0 extension ecosystem. ## Overview The `NoYield` model on SVM creates a token backed 1:1 by eligible collateral. It is the Solana equivalent of the EVM `MYieldToOne` model - all rewards flow to the protocol admin. - **Rewards Claiming:** Only the admin can call `claim_fees` to mint accumulated rewards to a chosen recipient. - **Token Standard:** Uses Token-2022 (or legacy Token Program for compatibility). - **Source Code:** [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} # Implementation guide Deploy a Treasury Model stablecoin where 100% of rewards are captured by the use case owner. This guide covers the EVM implementation using `MYieldToOne`. If you haven't already, please review the [**Treasury Model Deep Dive**](https://docs.m0.org/build/treasury-model) for conceptual details about this model and its implementations. ::note This guide covers the standard `MYieldToOne` implementation. If you need to accept additional stablecoins (USDC, USDT, DAI), see the [**JMI Implementation Guide**](https://docs.m0.org/build/treasury-jmi-guide) instead. :: ## EVM Implementation: MYieldToOne This section covers deploying the `MYieldToOne` contract on EVM chains (Ethereum, Base, Arbitrum, etc.). ### 1. Setup Your Environment First, you'll need to clone the `m-extensions` repository and set up your development environment: 1. **Clone the repository:** ```bash git clone https://github.com/m0-foundation/m-extensions.git cd m-extensions ``` 2. **Follow the setup instructions:** Refer to the [**README.md**](https://github.com/m0-foundation/m-extensions/blob/main/README.md){rel=""nofollow""} and follow all the commands provided to install dependencies and configure your development environment. 3. **Configure Environment Variables:** Create a `.env`file in the root directory: ```bash # Private key for deployment (include 0x prefix) PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef # RPC URLs MAINNET_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key SEPOLIA_RPC_URL=https://eth-sepolia.alchemyapi.io/v2/your-api-key # Etherscan API key for verification ETHERSCAN_API_KEY=your-etherscan-api-key ``` **Security Warning:** Never commit your `.env` file to version control. Add it to your `.gitignore`: ```bash echo ".env" >> .gitignore ``` ### 2. Create and Initialize Your Contract Your custom extension contract will inherit from `MYieldToOne`. The most important step is the `initialize` function, where you configure your token's name, symbol, and access control roles. Create a new file in the `src/` directory, for example, `MyTreasuryToken.sol`. ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { MYieldToOne } from "./projects/yieldToOne/MYieldToOne.sol"; contract MyTreasuryToken is MYieldToOne { /// @custom:oz-upgrades-unsafe-allow constructor constructor(address mToken_, address swapFacility_) MYieldToOne(mToken_, swapFacility_) { _disableInitializers(); } function initialize( string memory name, string memory symbol, address yieldRecipient_, address admin, address freezeManager, address yieldRecipientManager ) public initializer { MYieldToOne.initialize( name, // "My Treasury USD" symbol, // "tUSD" yieldRecipient_, // Treasury wallet address admin, // Admin multisig address freezeManager, // Freeze manager address (can be same as admin) yieldRecipientManager // Rewards recipient manager address ); } } ``` #### Key Parameters Explained: - **`name`**: The full name of your token (e.g., "My Treasury USD") - **`symbol`**: The token symbol (e.g., "tUSD") - `yieldRecipient_`: The wallet that will receive all rewards (your treasury) - **`admin`**: Address with `DEFAULT_ADMIN_ROLE` (should be a multisig) - **`freezeManager`**: Address with `FREEZE_MANAGER_ROLE` - **`yieldRecipientManager`**: Address with `YIELD_RECIPIENT_MANAGER_ROLE` **Note:** The `mToken` and `swapFacility` addresses are set in the constructor as immutable variables, not in the initializer. ### 3. Write Deployment Script Create a deployment script in the `script/` directory: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Script } from "forge-std/Script.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { MyTreasuryToken } from "../src/MyTreasuryToken.sol"; contract DeployMyTreasuryToken is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); vm.startBroadcast(deployerPrivateKey); // Deploying a Transparent Proxy, which requires a proxy admin. address proxy = Upgrades.deployTransparentProxy( "MyTreasuryToken.sol", // Contract file name deployer, // The admin for the proxy contract itself abi.encodeCall( // The initializer call data MyTreasuryToken.initialize, ( "My Treasury USD", // name "tUSD", // symbol 0x..., // rewards recipient 0x..., // admin (DEFAULT_ADMIN_ROLE for the logic) 0x..., // freezeManager 0x... // rewards recipient manager ) ) ); vm.stopBroadcast(); console.log("MyTreasuryToken deployed at:", proxy); } } ``` ### 4. Testing Your Contract Before deployment, create comprehensive tests: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { MyTreasuryToken } from "../src/MyTreasuryToken.sol"; contract MyTreasuryTokenTest is Test { MyTreasuryToken token; address admin = makeAddr("admin"); address treasury = makeAddr("treasury"); address user = makeAddr("user"); function setUp() public { token = new MyTreasuryToken( address(mockMToken), // $M Token address address(mockSwapFacility) // SwapFacility address ); token.initialize( "My Treasury USD", "tUSD", treasury, // Rewards recipient admin, // Admin admin, // Freeze manager admin // Rewards recipient manager ); } function testYieldClaim() public { // Test rewards claiming functionality // Mock some rewards accrual // Call claimYield() // Assert rewards goes to treasury } function testFreezing() public { // Test freezing functionality vm.prank(admin); token.freeze(user); assertTrue(token.isFrozen(user)); } function testYieldRecipientChange() public { // Test changing rewards recipient address newTreasury = makeAddr("newTreasury"); vm.prank(admin); token.setYieldRecipient(newTreasury); assertEq(token.yieldRecipient(), newTreasury); } } ``` ### 5. Gain M0 Earner Approval For your contract to accrue rewards, its deployed address must be approved as an M0 Earner. - **Calculate Your Address:** Determine your contract's final deployment address. You can do this deterministically using `CREATE2` or by deploying it to a testnet first. - **Submit a Proposal:** Create and submit a proposal to M0 to add your contract's address to the `earners` list. For more details on this process, see the [Gaining Earner Approval](https://docs.m0.org/protocol/roles) guide. ### 6. Security & Audit Even though you are building on an audited template, any modifications or new logic should be thoroughly tested and independently audited. Security is paramount. - Write comprehensive tests for your new contract in the `test/` directory. - Run static analysis tools like Slither. - Engage with a reputable security firm for a full audit if you've made significant modifications. ### 7. Deploy & Launch #### Deploy to Testnet First ```bash # Load environment variables source .env # Deploy to Sepolia forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $SEPOLIA_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Deploy to Mainnet ```bash # Load environment variables source .env # Deploy to Mainnet forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Alternative: Hardware Wallet Deployment For production deployments, consider using a hardware wallet: ```bash forge script script/DeployMyTreasuryToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --ledger \ --broadcast ``` #### Enable Earning After your contract is deployed and earner approval is confirmed, call the `enableEarning()` function on your deployed contract: ```solidity // Call this function to start rewards accrual myTreasuryToken.enableEarning(); ``` This will start the rewards accrual process. ### 8. Integration & Usage #### For Treasury Management 1. **Claiming Rewards:**Anyone can call the rewards claim function when surplus exists: ```solidity uint256 yieldAmount = myTreasuryToken.claimYield(); ``` 2. **Managing Rewards Recipient:**The rewards recipient manager can update where rewards go: ```solidity // Only YIELD_RECIPIENT_MANAGER_ROLE can call this myTreasuryToken.setYieldRecipient(newTreasuryAddress); ``` 3. **Freezing (if needed):** ```solidity // Only FREEZE_MANAGER_ROLE can call this myTreasuryToken.freeze(suspiciousAddress); myTreasuryToken.unfreeze(rehabilitatedAddress); // Batch operations address[] memory accounts = new address[](3); accounts[0] = addr1; accounts[1] = addr2; accounts[2] = addr3; myTreasuryToken.freezeAccounts(accounts); ``` #### Frontend Integration Display real-time rewards information to your users: ```javascript // Get current claimable rewards const currentYield = await myTreasuryToken.yield(); // Get total supply const totalSupply = await myTreasuryToken.totalSupply(); // Get user balance const userBalance = await myTreasuryToken.balanceOf(userAddress); // Check if address is frozen const isFrozen = await myTreasuryToken.isFrozen(userAddress); // Get rewards recipient const yieldRecipient = await myTreasuryToken.yieldRecipient(); ``` ### 9. Monitoring & Maintenance #### Key Metrics to Track - **Total Supply**: How much of your token is in circulation. - **Rewards Rate**: Current rewards being generated. - **Treasury Balance**: Amount accumulated in the rewards recipient address. - **Underlying Balance**: Total value held by your contract. #### Operational Considerations - Monitor the `yield()` function regularly to see claimable amounts. - Set up automated rewards claiming if desired. - Keep track of frozen addresses for compliance. - Monitor gas costs for user operations. **Congratulations!** You now have a fully functional treasury-focused stablecoin that automatically accumulates rewards for your use case while providing users with a stable, 1:1 backed token experience. ## SVM Implementation For the Solana/SVM implementation, see the dedicated [**NoYield (SVM) Deployment Guide**](https://docs.m0.org/build/svm-noyield-guide). # Implementation guide ::note The JMI extension is currently available on EVM chains only (Ethereum, Base, Arbitrum, etc.). :: This guide provides step-by-step instructions for deploying a JMI (Just Mint It) extension, where multiple collateral types are accepted and 100% of the rewards are captured by a designated recipient. If you haven't already, please review the [**JMI Deep Dive**](https://docs.m0.org/build/treasury-jmi-overview) for conceptual details about this model. ::note For single-collateral deployments, consider the simpler [**MYieldToOne implementation**](https://docs.m0.org/build/treasury-guide) instead. :: ## EVM Implementation This section covers deploying the `JMIExtension` contract on EVM chains (Ethereum, Base, Arbitrum, etc.). ### 1. Setup Your Environment First, clone the `m-extensions` repository and set up your development environment: 1. **Clone the repository:** ```bash git clone https://github.com/m0-foundation/m-extensions.git cd m-extensions ``` 2. **Follow the setup instructions:** Refer to the [**README.md**](https://github.com/m0-foundation/m-extensions/blob/main/README.md){rel=""nofollow""} and follow all commands to install dependencies and configure your development environment. 3. **Configure Environment Variables:** Create a `.env`file in the root directory: ```bash # Private key for deployment (include 0x prefix) PRIVATE_KEY=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef # RPC URLs MAINNET_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key SEPOLIA_RPC_URL=https://eth-sepolia.alchemyapi.io/v2/your-api-key # Etherscan API key for verification ETHERSCAN_API_KEY=your-etherscan-api-key ``` **Security Warning:** Never commit your `.env` file to version control: ```bash echo ".env" >> .gitignore ``` ### 2. Create Your Contract Your custom JMI extension will inherit from `JMIExtension`. Create a new file in the `src/` directory, for example, `MyJMIToken.sol`: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { JMIExtension } from "./projects/jmi/JMIExtension.sol"; contract MyJMIToken is JMIExtension { /// @custom:oz-upgrades-unsafe-allow constructor constructor(address mToken_, address swapFacility_) JMIExtension(mToken_, swapFacility_) { _disableInitializers(); } function initialize( string memory name, string memory symbol, address yieldRecipient_, address admin, address assetCapManager, address freezeManager, address pauser, address yieldRecipientManager ) public override initializer { JMIExtension.initialize( name, // "My JMI USD" symbol, // "jUSD" yieldRecipient_, // Treasury wallet address admin, // Admin multisig address assetCapManager, // Asset cap manager address freezeManager, // Freeze manager address pauser, // Pauser address yieldRecipientManager // Rewards recipient manager address ); } } ``` #### Key Parameters Explained - **`name`**: The full name of your token (e.g., "My JMI USD") - **`symbol`**: The token symbol (e.g., "jUSD") - `yieldRecipient_`: The wallet receiving all rewards (your treasury) - **`admin`**: Address with `DEFAULT_ADMIN_ROLE` (should be a multisig) - **`assetCapManager`**: Address with `ASSET_CAP_MANAGER_ROLE` (manages collateral caps) - **`freezeManager`**: Address with `FREEZE_MANAGER_ROLE` (can freeze addresses) - **`pauser`**: Address with `PAUSER_ROLE` (emergency stop) - **`yieldRecipientManager`**: Address with `YIELD_RECIPIENT_MANAGER_ROLE` **Note:** The `mToken` and `swapFacility` addresses are set in the constructor as immutable variables. ### 3. Write Deployment Script Create a deployment script in the `script/` directory: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Script, console } from "forge-std/Script.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { MyJMIToken } from "../src/MyJMIToken.sol"; contract DeployMyJMIToken is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); address deployer = vm.addr(deployerPrivateKey); // Configuration - update these addresses for your deployment address mToken = 0x...; // M Token address for your chain address swapFacility = 0x...; // SwapFacility address for your chain vm.startBroadcast(deployerPrivateKey); address proxy = Upgrades.deployTransparentProxy( "MyJMIToken.sol", deployer, abi.encodeCall( MyJMIToken.initialize, ( "My JMI USD", // name "jUSD", // symbol 0x..., // rewards recipient 0x..., // admin 0x..., // assetCapManager 0x..., // freezeManager 0x..., // pauser 0x... // rewards recipient manager ) ), Upgrades.DeployOptions({ constructorData: abi.encode(mToken, swapFacility) }) ); vm.stopBroadcast(); console.log("MyJMIToken deployed at:", proxy); } } ``` ### 4. Testing Your Contract Create comprehensive tests before deployment: ```solidity // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { MyJMIToken } from "../src/MyJMIToken.sol"; import { IERC20 } from "openzeppelin-contracts/token/ERC20/IERC20.sol"; contract MyJMITokenTest is Test { MyJMIToken token; address admin = makeAddr("admin"); address assetCapManager = makeAddr("assetCapManager"); address treasury = makeAddr("treasury"); address user = makeAddr("user"); address mockUSDC; address mockDAI; function setUp() public { // Deploy mock tokens // Deploy your JMI token // Initialize with test addresses } function testSetAssetCap() public { uint256 usdcCap = 1_000_000e6; // 1M USDC vm.prank(assetCapManager); token.setAssetCap(mockUSDC, usdcCap); assertEq(token.assetCap(mockUSDC), usdcCap); assertTrue(token.isAllowedAsset(mockUSDC)); } function testWrapWithUSDC() public { // Setup: Set cap and mint USDC to swap facility uint256 amount = 1000e6; // Test wrapping USDC through SwapFacility // Assert JMI tokens minted // Assert totalAssets increased } function testUnwrapLimitedToMBacking() public { // Setup: Wrap some M and some USDC // Try to unwrap more than M backing // Expect revert with InsufficientMBacking } function testReplaceAssetWithM() public { // Setup: Have USDC in the contract // Call replaceAssetWithM to swap M for USDC // Assert totalAssets decreased // Assert USDC sent to recipient } function testFreezing() public { vm.prank(admin); // Assuming admin has freeze manager role token.freeze(user); assertTrue(token.isFrozen(user)); } function testYieldClaim() public { // Setup: Wrap M and let rewards accrue // Call claimYield // Assert rewards went to treasury } } ``` ### 5. Configure Supported Collateral After deployment, the asset cap manager must configure which collateral assets are accepted: ```solidity // Set caps for each supported stablecoin // Cap is in the asset's native decimals // USDC (6 decimals) - 10M cap myJMIToken.setAssetCap(USDC_ADDRESS, 10_000_000e6); // DAI (18 decimals) - 5M cap myJMIToken.setAssetCap(DAI_ADDRESS, 5_000_000e18); // USDT (6 decimals) - 5M cap myJMIToken.setAssetCap(USDT_ADDRESS, 5_000_000e6); ``` **Important:** Only set caps for stablecoins that maintain a reliable 1:1 peg to the dollar. The JMI model assumes 1:1 peg for all accepted collateral. ### 6. Gain M0 Earner Approval For your contract to accrue rewards on the supply it holds, its deployed address must be approved as an M0 Earner. - **Calculate Your Address:** Determine your contract's final deployment address using `CREATE2` or by deploying to a testnet first. - **Submit a Proposal:** Create and submit a proposal to add your contract's address to the `earners` list in the `TTGRegistrar`. For more details, see the [Gaining Earner Approval](https://docs.m0.org/protocol/roles) guide. ### 7. Security & Audit Even though you are building on an audited template, any modifications should be thoroughly tested and independently audited: - Write comprehensive tests covering all scenarios - Run static analysis tools like Slither - Engage a reputable security firm for a full audit if you've made significant modifications ### 8. Deploy & Launch #### Deploy to Testnet First ```bash source .env forge script script/DeployMyJMIToken.s.sol \ --rpc-url $SEPOLIA_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Deploy to Mainnet ```bash source .env forge script script/DeployMyJMIToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --broadcast ``` #### Hardware Wallet Deployment For production deployments: ```bash forge script script/DeployMyJMIToken.s.sol \ --rpc-url $MAINNET_RPC_URL \ --verify \ --etherscan-api-key $ETHERSCAN_API_KEY \ --ledger \ --broadcast ``` #### Enable Earning After deployment and earner approval: ```solidity myJMIToken.enableEarning(); ``` ### 9. Integration & Usage #### Managing Asset Caps ```solidity // Check current cap uint256 currentCap = myJMIToken.assetCap(USDC_ADDRESS); // Update cap (only ASSET_CAP_MANAGER_ROLE) myJMIToken.setAssetCap(USDC_ADDRESS, newCap); // Disable an asset (set cap to 0) myJMIToken.setAssetCap(USDC_ADDRESS, 0); ``` #### Checking Backing Status ```solidity // Total JMI supply uint256 totalSupply = myJMIToken.totalSupply(); // Non-M asset backing uint256 totalAssets = myJMIToken.totalAssets(); // M backing (what can be unwrapped) uint256 mBacking = totalSupply - totalAssets; // Specific asset backing uint256 usdcBacking = myJMIToken.assetBalanceOf(USDC_ADDRESS); ``` #### Claiming Rewards ```solidity // Check claimable rewards uint256 pendingYield = myJMIToken.yield(); // Claim rewards (anyone can call, goes to yieldRecipient) uint256 claimedAmount = myJMIToken.claimYield(); ``` #### Managing Rewards Recipient ```solidity // Only YIELD_RECIPIENT_MANAGER_ROLE myJMIToken.setYieldRecipient(newTreasuryAddress); ``` #### Freezing Addresses ```solidity // Only FREEZE_MANAGER_ROLE myJMIToken.freeze(suspiciousAddress); myJMIToken.unfreeze(clearedAddress); // Batch operations address[] memory toFreeze = new address[](3); toFreeze[0] = addr1; toFreeze[1] = addr2; toFreeze[2] = addr3; myJMIToken.freezeAccounts(toFreeze); ``` #### Emergency Pause ```solidity // Only PAUSER_ROLE myJMIToken.pause(); // Halts wrap, unwrap, transfer, replaceAssetWithM myJMIToken.unpause(); // Resumes operations ``` #### Frontend Integration ```javascript // Get backing breakdown const totalSupply = await myJMIToken.totalSupply(); const totalAssets = await myJMIToken.totalAssets(); const mBacking = totalSupply - totalAssets; // Check specific asset backing const usdcBacking = await myJMIToken.assetBalanceOf(USDC_ADDRESS); const daisBacking = await myJMIToken.assetBalanceOf(DAI_ADDRESS); // Check if wrap is allowed const canWrap = await myJMIToken.isAllowedToWrap(USDC_ADDRESS, amount); // Check if unwrap is allowed const canUnwrap = await myJMIToken.isAllowedToUnwrap(amount); // Check if asset replacement is allowed (amount in asset decimals) const canReplace = await myJMIToken.isAllowedToReplaceAssetWithM( USDC_ADDRESS, assetAmount, ); // Get rewards info const pendingYield = await myJMIToken.yield(); const yieldRecipient = await myJMIToken.yieldRecipient(); // Check account status const isFrozen = await myJMIToken.isFrozen(userAddress); const isPaused = await myJMIToken.paused(); ``` ### 10. Monitoring & Maintenance #### Key Metrics to Track | Metric | Description | | ------------------ | ----------------------------------- | | Total Supply | Total JMI tokens in circulation | | M Backing | `totalSupply - totalAssets` | | Total Non-M Assets | Sum of all collateral backings | | Per-Asset Backing | Individual collateral levels | | Asset Utilization | `assetBalance / assetCap` per asset | | Rewards Rate | Current rewards accrual rate | | Pending Rewards | Unclaimed rewards amount | #### Operational Considerations 1. **Monitor Asset Caps:**Regularly review if caps need adjustment based on: - Stablecoin peg stability - Market conditions - Protocol risk tolerance 2. **Rebalancing:** Use `replaceAssetWithM`to rebalance backing when needed: - Arbitrageurs can swap approved collateral for approved stablecoin collateral. - This naturally moves the backing toward approved collateral over time. 3. **Rewards Claiming:** Set up automated rewards claiming if desired. 4. **Emergency Procedures:** - Document pause/unpause procedures. - Have freeze manager ready for suspicious activity. - Maintain secure communication channels for emergencies. 5. **Collateral Monitoring:** - Track peg stability of accepted stablecoins. - Be prepared to reduce/remove caps for depegged assets. **Congratulations!** You now have a fully functional multi-collateral stablecoin that accepts various stablecoins while automatically accumulating rewards for your protocol. # Overview ::note The JMI extension is currently available on EVM chains only (Ethereum, Base, Arbitrum, etc.). SVM support is planned for a future release. :: The `JMIExtension` (Just Mint It) is an enhanced Treasury Model implementation that accepts multiple collateral types while still directing 100% of rewards to a single recipient. **When to Use JMI instead of standard MYieldToOne:** - Projects requiring flexibility to accept multiple stablecoins as collateral types (USDC, USDT, DAI). - Protocols wanting a non-rebasing stablecoin backed by a diversified collateral pool. - Ecosystems that need to manage risk exposure to different stablecoin assets through configurable caps. - Treasury management scenarios where rewards are centralized but collateral sources are diversified. **Source Code:** [`JMIExtension.sol`](https://github.com/m0-foundation/m-extensions/blob/main/src/projects/jmi/JMIExtension.sol){rel=""nofollow""} ## Key Differentiators from MYieldToOne | Feature | MYieldToOne (Standard) | JMIExtension (JMI) | | --------------------- | ---------------------- | ----------------------------------------------------------------------- | | Collateral | Single-collateral | Multi-collateral (liquid stablecoins such as USDC, USDT also supported) | | Asset Caps | N/A | Configurable per asset | | Unwrap Logic | Always backed 1:1 | Limited to base backing portion | | Additional Operations | None | `replaceAssetWithM` | ## Architecture Overview `JMIExtension` inherits from `MYieldToOne`, which itself inherits from `MExtension`, `Freezable`, and `Pausable`. This inheritance chain provides: - **ERC-20 Token Logic** via `MExtension` - **Rewards Distribution** via `MYieldToOne` (100% to single recipient) - **Compliance Controls** via `Freezable` (address-level restrictions) - **Emergency Controls** via `Pausable` (contract-wide halt) - **Multi-Collateral Logic** via `JMIExtension` (asset caps, multiple collateral types) ```text JMIExtension ├── IJMIExtension (interface) ├── JMIExtensionLayout (storage) └── MYieldToOne ├── IMYieldToOne (interface) ├── MYieldToOneStorageLayout (storage) ├── MExtension (core ERC-20 + wrap/unwrap) ├── Freezable (address freezing) └── Pausable (emergency stop) ``` ## How It Works 1. **Multi-Collateral Wrapping:** Users can wrap an approved stablecoin (USDC, DAI, etc.) through the SwapFacility. The contract tracks each asset's balance separately. 2. **Asset Caps:** Each stablecoin collateral component has a maximum cap. This limits risk exposure to any single stablecoin. 3. **Rewards Accrual:** Only the non-stablecoin collateral portion of the reserves earns rewards. 4. **Rewards Distribution:** The `claimYield()` function mints new JMI tokens to the designated `yieldRecipient`, equal to the rewards surplus. 5. **Selective Unwrapping:** Users can only unwrap to the base asset. The maximum unwrap amount is limited to the non-stablecoin collateral portion of reserves (total supply minus stablecoin asset backing). 6. **Asset Replacement:** The `replaceAssetWithM` function allows swapping approved collateral for approved stablecoins held by the contract, enabling arbitrageurs to rebalance the backing. ## Backing Model The total supply of JMI tokens is backed by two components: ```text Total Supply = M Backing + Total Non-M Assets ``` Where: - **M Backing** = `totalSupply() - totalAssets()` - **Total Non-M Assets** = Sum of all approved stablecoin collateral (in extension decimals) The claimable rewards are calculated as: ```text Rewards = mBalanceOf(contract) - M Backing ``` The JMI backing model assumes a 1:1 peg between all deposited assets. This means 1 USDC deposited equals 1 JMI token minted, and 1 DAI deposited equals 1 JMI token minted (accounting for decimal conversions). ## Roles (Access Control) JMIExtension uses OpenZeppelin's `AccessControl` with these roles: | Role | Permissions | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | **`DEFAULT_ADMIN_ROLE`** | Super-user. Can grant and revoke any role. Should be a secure multi-sig or governance contract. | | **`ASSET_CAP_MANAGER_ROLE`** | Manages risk by setting caps on each approved stablecoin collateral asset via `setAssetCap()`. | | **`YIELD_RECIPIENT_MANAGER_ROLE`** | Controls where rewards are distributed via `setYieldRecipient()`. | | **`FREEZE_MANAGER_ROLE`** | Can freeze/unfreeze addresses from interacting with the token. | | **`PAUSER_ROLE`** | Can pause/unpause the entire contract in emergencies. | ## Core Functions ### Wrapping Functions | Function | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | `wrap(asset, recipient, amount)` | Mints JMI tokens by depositing an approved stablecoin collateral asset. Called via SwapFacility. | | `wrap(recipient, amount)` | Mints JMI tokens by depositing approved collateral (inherited from MExtension). Called via SwapFacility. | ### Unwrapping Functions | Function | Description | | --------------------------- | ---------------------------------------------------------------- | | `unwrap(recipient, amount)` | Burns JMI tokens and sends approved collateral to the recipient. | ### Asset Management Functions | Function | Access | Description | | --------------------------------------------- | ------------------------ | ------------------------------------------------------------------------ | | `setAssetCap(asset, cap)` | `ASSET_CAP_MANAGER_ROLE` | Sets the maximum balance allowed for a collateral asset. | | `replaceAssetWithM(asset, recipient, amount)` | SwapFacility only | Swaps approved collateral for approved stablecoins held by the contract. | ### Rewards Functions (Inherited) | Function | Description | | ---------------------------- | ---------------------------------------------------------- | | `yield()` | Returns the current claimable rewards amount. | | `claimYield()` | Claims rewards by minting new JMI tokens to the recipient. | | `setYieldRecipient(address)` | Updates the rewards recipient address. | ### View Functions | Function | Returns | Description | | --------------------------------------------- | --------- | --------------------------------------------------------- | | `assetBalanceOf(asset)` | `uint256` | Tracked balance of a collateral asset | | `assetCap(asset)` | `uint256` | Cap for a collateral asset (0 = not allowed) | | `assetDecimals(asset)` | `uint8` | Cached decimals for a collateral asset | | `totalAssets()` | `uint256` | Total approved stablecoin backing (in extension decimals) | | `isAllowedAsset(asset)` | `bool` | True if asset is approved collateral or has non-zero cap | | `isAllowedToWrap(asset, amount)` | `bool` | True if wrap would succeed | | `isAllowedToUnwrap(amount)` | `bool` | True if unwrap would succeed | | `isAllowedToReplaceAssetWithM(asset, amount)` | `bool` | True if asset balance >= amount | ### Freezing & Pause Functions (Inherited) | Function | Access | Description | | ------------------------------ | --------------------- | --------------------------- | | `freeze(account)` | `FREEZE_MANAGER_ROLE` | Freezes an address | | `unfreeze(account)` | `FREEZE_MANAGER_ROLE` | Unfreezes an address | | `freezeAccounts(accounts[])` | `FREEZE_MANAGER_ROLE` | Batch freeze | | `unfreezeAccounts(accounts[])` | `FREEZE_MANAGER_ROLE` | Batch unfreeze | | `isFrozen(account)` | Public | Check if address is frozen | | `pause()` | `PAUSER_ROLE` | Pauses the contract | | `unpause()` | `PAUSER_ROLE` | Unpauses the contract | | `paused()` | Public | Check if contract is paused | ## Events | Event | Parameters | Description | | -------------------- | ---------------------------------------------- | ------------------------- | | `AssetCapSet` | `asset`, `cap` | Asset cap was updated | | `AssetReplacedWithM` | `asset`, `assetAmount`, `recipient`, `mAmount` | Collateral swapped | | `YieldClaimed` | `amount` | Rewards claimed | | `YieldRecipientSet` | `yieldRecipient` | Rewards recipient changed | | `Transfer` | `from`, `to`, `amount` | ERC-20 transfer | | `Approval` | `owner`, `spender`, `amount` | ERC-20 approval | | `Frozen` | `account`, `timestamp` | Account was frozen | | `Unfrozen` | `account`, `timestamp` | Account was unfrozen | | `Paused` | `account` | Contract was paused | | `Unpaused` | `account` | Contract was unpaused | ## Errors | Error | Description | | ------------------------------------------------------ | --------------------------------------------------------- | | `InvalidAsset(asset)` | Asset is `address(0)` or `$M` (for multi-asset functions) | | `AssetCapReached(asset)` | Deposit would exceed asset cap | | `InsufficientAssetBacking(asset, required, available)` | Not enough collateral for replacement | | `InsufficientAssetReceived(asset, expected, received)` | Fee-on-transfer token detected | | `InsufficientMBacking(required, available)` | Not enough approved collateral backing for unwrap | | `ZeroAssetCapManager()` | Asset cap manager is zero address | | `ZeroAdmin()` | Admin is zero address | | `ZeroYieldRecipient()` | Rewards recipient is zero address | | `ZeroYieldRecipientManager()` | Rewards recipient manager is zero address | | `ZeroFreezeManager()` | Freeze manager is zero address | | `ZeroPauser()` | Pauser is zero address | | `AccountFrozen(account)` | Operation blocked due to frozen account | | `EnforcedPause()` | Operation blocked due to paused state | ## Important Constraints - **Fee-on-Transfer Tokens:** Not supported. The contract reverts if fewer tokens are received than expected. - **Only 1:1 Pegged Stablecoins:** The model assumes all accepted collateral is pegged 1:1 to the dollar. - **Unwrap Limited to Approved Collateral Backing:** Users cannot unwrap more JMI than the contract's approved collateral backing supports. ## Security Considerations 1. **Fee-on-Transfer Protection:** The contract explicitly checks received amounts match expected amounts, reverting on discrepancies. 2. **Inflation Attack Prevention:** The `replaceAssetWithM` function uses the tracked `assetBalanceOf` rather than actual token balance, preventing manipulation via direct token transfers. 3. **Access Control:** All sensitive functions are protected by role-based access control. 4. **Emergency Controls:** The `PAUSER_ROLE` can halt operations in case of security incidents. 5. **Frozen Accounts:** The `FREEZE_MANAGER_ROLE` can block compromised addresses from interacting with the token. 6. **Upgradability:** The contract uses OpenZeppelin's transparent proxy pattern, allowing for future upgrades with proper governance. **Ready to build?** [**Follow the JMI implementation guide**](https://docs.m0.org/build/treasury-jmi-guide) # Overview **Core Concept:** All accrued rewards are captured by the use case owner. Token holders observe a stable, non-rebasing token with 1:1 backing. **When to Use:** - Simple business models where all rewards go to one entity. - Corporate treasuries requiring centralized revenue management. - Protocol-owned stablecoins where rewards fund protocol development. - Ecosystem development funds or grants programs. ## EVM Implementation Options The Treasury Model offers two EVM implementations to fit different collateral strategies: | Feature | MYieldToOne (Standard) | JMIExtension (Multi-Collateral) | | ------------------------- | ---------------------- | ----------------------------------------------------------------------- | | **Collateral** | Single-collateral | Multi-collateral (liquid stablecoins such as USDC, USDT also supported) | | **Asset Caps** | N/A | Configurable per asset | | **Unwrap Logic** | Always 1:1 backed | Limited to base backing portion | | **Additional Operations** | None | `replaceAssetWithM` for rebalancing | | **Best For** | Simple treasury setups | Diversified collateral pools | **Choose `MYieldToOne`** for single-collateral deployments with the simplest implementation. **Choose** `JMIExtension` if you need flexibility to accept multiple stablecoins while still directing all rewards to a single recipient. [**Learn more about JMI**](https://docs.m0.org/build/treasury-jmi-overview) ## Default: MYieldToOne The `MYieldToOne` template is the simplest and most direct way to build on M0. It creates an upgradeable ERC-20 token backed 1:1 by eligible collateral, while directing **100% of the accrued rewards** to a single, designated recipient address. This model is perfect for builders who want the stability and liquidity of an M0-powered token within their ecosystem, but prefer to centralize the rewards for strategic purposes like funding operational expenses, protocol development, ecosystem grants. **Source Code:** [`MYieldToOne.sol`](https://github.com/m0-foundation/m-extensions/blob/main/src/projects/yieldToOne/MYieldToOne.sol){rel=""nofollow""} ### Architecture and Mechanism The beauty of `MYieldToOne` lies in its simplicity. It separates the token holding experience from the rewards generation, while incorporating freezing capabilities for compliance. #### Roles (Access Control) `MYieldToOne` inherits from both `MExtension` and `Freezable`, using OpenZeppelin's `AccessControl` to manage permissions with these distinct roles: - **`DEFAULT_ADMIN_ROLE`**: The super-user. This role can grant and revoke any other role. It should be held by a secure multi-sig or governance contract. - `YIELD_RECIPIENT_MANAGER_ROLE`: This role has one job: to manage where the rewards go. It can call `setYieldRecipient()` to change the beneficiary address. - **`FREEZE_MANAGER_ROLE`**: This role can block specific addresses from wrapping, unwrapping, or transferring the token, adding a layer of compliance through the `freeze()` and `unfreeze()` functions. #### How It Works 1. **Wrapping:** A user wraps through the SwapFacility and receives an equal amount of your new extension token (e.g., wrap 100, receive 100 `YourToken`). The `MYieldToOne` contract now holds the underlying balance. 2. **Accruing Rewards:** Your deployed `MYieldToOne` contract must call `enableEarning()` to start earning rewards. It begins to accrue rewards from the underlying eligible collateral. 3. **Creating a Surplus:** As rewards accrue, the contract's balance becomes greater than the total supply of your extension token. For example, the contract might hold 100.05 while the `totalSupply()` of `YourToken` is still 100. 4. **Claiming Rewards:** This surplus is the claimable rewards. Anyone can call the `claimYield()` function to realize this revenue. 5. **Distribution:** When `claimYield()` is called, the contract mints new `YourToken` equivalent to the surplus amount (0.05 in our example) and sends them directly to the designated `yieldRecipient` address. The `totalSupply` now becomes 100.05. For end-users holding your token, their balance remains stable and unchanged. The rewards are handled completely behind the scenes. ### Key Interface & Functions While `MYieldToOne` is a full ERC-20 token, its unique logic revolves around a few key functions. #### Storage Layout The state is kept minimal and efficient. ```solidity struct MYieldToOneStorageStruct { uint256 totalSupply; address yieldRecipient; mapping(address account => uint256 balance) balanceOf; } ``` #### Core Rewards Functions - `yield() external view returns (uint256)` This view function calculates the amount of claimable rewards at any moment. It returns the difference between the contract's current balance and the `totalSupply` of the extension token: `mBalance > totalSupply ? mBalance - totalSupply : 0`. - `claimYield() external returns (uint256)` This is the function that triggers the rewards distribution. It calculates the rewards, mints that amount of new tokens, and transfers them to the `yieldRecipient`. Returns 0 if there are no rewards to claim. #### Management Functions - **`setYieldRecipient(address yieldRecipient) external`** Callable only by the `YIELD_RECIPIENT_MANAGER_ROLE`, this function updates the address that receives the rewards. It automatically claims any existing rewards for the previous recipient before making the change. #### Freezing Functions (Inherited from Freezable) - **`freeze(address account) external`** Callable by the `FREEZE_MANAGER_ROLE`, this function prevents an address from interacting with the token. Reverts if the account is already frozen. - **`unfreeze(address account) external`** Removes an address from the frozen list. Reverts if the account is not currently frozen. - **`isFrozen(address account) external view returns (bool)`** Checks if an address is currently frozen. - **`freezeAccounts(address[] calldata accounts) external`** Batch version for freezing multiple accounts. - **`unfreezeAccounts(address[] calldata accounts) external`** Batch version for unfreezing multiple accounts. #### Standard MExtension Functions As a child of `MExtension.sol`, `MYieldToOne` automatically inherits all the essential functionalities for interacting with the M0 ecosystem: - `wrap(address recipient, uint256 amount)` - Called by SwapFacility only. - `unwrap(address recipient, uint256 amount)` - Called by SwapFacility only. - `enableEarning()` - Starts earning rewards. - `disableEarning()` - Stops earning rewards. #### Hook Functions The contract implements several internal hook functions that enforce freezing: - `_beforeApprove()` - Checks both account and spender aren't frozen. - `_beforeWrap()` - Checks both depositor and recipient aren't frozen. - `_beforeUnwrap()` - Checks the account isn't frozen. - `_beforeTransfer()` - Checks sender, recipient, and msg.sender aren't frozen. #### Constants - **`YIELD_RECIPIENT_MANAGER_ROLE`**: `keccak256("YIELD_RECIPIENT_MANAGER_ROLE")` - **`FREEZE_MANAGER_ROLE`**: `keccak256("FREEZE_MANAGER_ROLE")` (inherited from Freezable) **Ready to build with MYieldToOne?** [**Follow the implementation guide**](https://docs.m0.org/build/treasury-guide) **Need multi-collateral support?** [**See the JMI implementation guide**](https://docs.m0.org/build/treasury-jmi-guide) ## SVM Implementation: NoYield The `NoYield` extension template is the most direct way to build a treasury-focused stablecoin on Solana. It creates an upgradeable, non-rebasing token backed 1:1 by eligible collateral, while directing **100% of the accrued rewards** to the extension's administrator. The NoYield variant supports both the Token-2022 standard and the legacy token program. This model is perfect for builders who want the stability of an M0-powered token within their ecosystem but prefer to centralize rewards for strategic purposes, such as funding operational expenses, protocol development, ecosystem grants. **Source Code:** The `NoYield` model is compiled from the `m_ext` program using the `no-yield` Rust feature flag. The source code is available in the [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} repository. ### Architecture and Mechanism The `NoYield` model's strength is its simplicity. It completely separates token holding from rewards generation, which is managed centrally by the platform. #### Roles (Access Control) - **`admin`**: The root administrator, typically a secure multi-sig or governance contract. This role can: - Configure the extension via `initialize`. - Claim all accrued rewards using the `claim_fees` instruction. - Manage a whitelist of `wrap_authorities`. - **`wrap_authority`**: An address authorized by the `admin` to perform wrap and unwrap operations. This is useful for delegating wrapping capabilities to specific programs or services, like the swap facility. #### How It Works 1. **Wrapping:** A user or program with `wrap_authority` wraps and receives an equal amount of the new extension token. The `NoYield` contract's vault (`m_vault`) now holds the underlying balance. 2. **Rewards Accrual:** The contract's vault is registered as an earner in the base program. It begins to accrue rewards through Solana's rebasing mechanism. 3. **Creating a Surplus:** As rewards accrue, the contract's balance in its vault becomes greater than the total supply of the extension token. For example, the contract might hold 100.05 while the `totalSupply` of the extension token remains 100. This surplus represents the claimable rewards. 4. **Claiming Rewards:** The `admin` calls the `claim_fees` function. This is the core mechanism for capturing the protocol's revenue. 5. **Distribution:** The `claim_fees` instruction calculates the surplus, mints an equivalent amount of new extension tokens, and sends them directly to a recipient token account controlled by the `admin`. The `totalSupply` of the extension token is now updated to reflect the newly minted fee tokens. For end-users, their token balance remains stable and unchanged. The rewards are handled entirely behind the scenes by the protocol's administrator. ### Key Interface & Functions The `NoYield` model is a feature-complete Token-2022 token. Its unique logic is centered around a few key instructions defined in the generic Extension program. #### Storage Layout The extension's state is managed by the `ExtGlobal` account, which is a PDA seeded with `b"ext_global"`. ```rust // State structure from m_ext program pub struct ExtGlobal { pub admin: Pubkey, 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, } pub struct YieldConfig { pub variant: YieldVariant, // Set to YieldVariant::NoYield pub last_m_index: u64, pub last_ext_index: u64, } ``` #### Core Rewards Function - `claim_fees()`: The primary instruction for capturing rewards. It calculates the total rewards accrued in the `m_vault` since the last claim, mints that amount of new extension tokens, and transfers them to an admin-controlled `recipient_ext_token_account`. This instruction can only be called by the `admin`. #### Management Functions - **`initialize(wrap_authorities: Vec)`**: Sets up the extension's global state with the NoYield configuration. - **`add_wrap_authority(new_wrap_authority: Pubkey)`**: Adds a new address to the list of authorized wrappers. - **`remove_wrap_authority(wrap_authority: Pubkey)`**: Removes an address from the list of authorized wrappers. - **`transfer_admin(new_admin: Pubkey)`**: Transfers admin control to a new address. #### Standard Extension Functions The contract inherits essential functionalities for interacting with the M0 ecosystem: - **`wrap(amount: u64)`**: Wraps into the extension token. Requires the caller to be a whitelisted `wrap_authority`. - **`unwrap(amount: u64)`**: Unwraps the extension token. Requires the caller to be a whitelisted `wrap_authority`. #### Error Handling The contract includes robust error handling to ensure secure operation: - `Unauthorized`: Thrown if a non-admin tries to call a privileged instruction. - `InvalidAmount`: Thrown for invalid wrap or unwrap amounts. - `InsufficientCollateral`: Ensures the contract never mints more extension tokens than its underlying balance. **Ready to build?** [**Follow the implementation guide to deploy your NoYield extension.**](https://docs.m0.org/build/treasury-guide) # Overview Stablecoin issuance is becoming a strategic line of business for many financial institutions. The question is no longer whether to participate - it's whether you build the infrastructure yourself or use the best stack available. M0's answer is **Stablecoin Core**. --- ## What it means to be an issuer powered by M0 As an issuer utilizing the M0 infrastructure, you pledge eligible collateral - specifically, permissible investments in line with local regulations - and mint application-specific stablecoins. As an issuer, you can either utilize the M0 stack to issue your own stablecoin, or optionally partner with builders who want their own application-specific stablecoins. You manage the reserve side of the business. M0 gives you the tech to issue and distribute digital money on-chain. --- ## Why issue using M0 technology **You get the best issuance tech stack in the market.** Mint and distribute any number of stablecoins across most major blockchains. Configure partner-specific, real-time rewards distribution programs. Monitor all data associated with issued stablecoins and reconcile with off-chain ledgers. **You go live fast.** Start issuing in a matter of weeks - not the 18-month build cycle that proprietary infrastructure typically requires. **You reach the right builders.** PayPal, MetaMask, Exodus, Kast, and others already demand M0-standard stablecoins. When you become an M0 issuer, you get immediate access to builders who are actively looking for issuance partners. **You earn from the relationship.** Real-time rewards distribution means your clients get near-instant rewards from the collateral backing their stablecoins. You manage the program parameters. You control the revenue sharing logic. **You maintain optionality.** M0 Stablecoin Core is designed to handle multiple clients on multiple chains from a single deployment. You're not locked into one product or one market. --- ## The product: Stablecoin Core Stablecoin Core is M0's top-level issuance product. It's the software stack that powers your issuance operation. What's included: - Mint and burn mechanics across supported chains - Configurable real-time rewards distribution - Onchain data monitoring and off-chain reconciliation tools - Partner-level revenue sharing program management - Multi-currency support (expand to new currencies without rebuilding) A Stablecoin Core is specific to an issuer and to a currency. You can deploy multiple Stablecoin Cores - one per currency you want to issue in. You control all parameters of each Core. --- ## Optional: Onchain Orchestration Once you're issuing, you may want to distribute your stablecoin into your applications, or you may want to help your builder clients distribute their stablecoin into real use cases. M0's Onchain Orchestration service handles this. With Onchain Orchestration, you can: - Swap USDC or USDT for your issued stablecoin (and vice versa) - Handle intra-issuer and cross-issuer stablecoin conversions - Move stablecoins cross-chain - Deliver liquidity to builder clients without building custom plumbing This is available as an additional license on top of Stablecoin Core. [Onchain Orchestration →](https://docs.m0.org/build/accessing-liquidity) --- ## Who this is for M0 is built for institutions that already operate - or are ready to operate - in the digital asset space with the appropriate licenses and charters: - **SIaaS providers** (Stablecoin Issuance as a Service) - companies like Bridge (Stripe), MoonPay, and Anchorage who want to offer issuance to their clients at scale. - **Crypto-native infrastructure providers** - companies with adjacent licenses (rails, custody) expanding into issuance. - **Vertically integrated institutions** - banks, fintechs, and financial networks that want to issue a stablecoin for their own distribution. --- ## How to get started 1. **Talk to us.** Every issuer relationship starts with a conversation. We'll assess fit, walk through the technical requirements, and outline the onboarding process for your specific situation. 2. **Complete the technical onboarding.** Once aligned, the M0 team will guide you through Stablecoin Core setup and integration with your backoffice systems. 3. **Go live.** Start issuing stablecoins to your builder clients. [Get in Touch →](https://www.m0.org/contact-us){rel=""nofollow""} --- ## Related ::card-group :::card --- icon: i-lucide-network title: Protocol overview to: https://docs.m0.org/protocol/m-token --- Understand the technical architecture behind M0. ::: :::card --- icon: i-lucide-refresh-cw title: Minting & Burning to: https://docs.m0.org/protocol/minting-burning --- How the MinterGateway manages supply creation and redemption. ::: :::card --- icon: i-lucide-users title: Roles to: https://docs.m0.org/protocol/roles --- Understand Minters, Validators, and Earners in the protocol. ::: :: # Distribution Vault Also called `TTGVault` or `Vault`. ## Overview The Distribution Vault is a core component of M0's economic infrastructure designed to collect, account for, and distribute various forms of protocol revenue and yield to Zero token holders. It serves as the primary mechanism for value capture within the protocol's Two Token Governance (TTG) system, turning protocol operations into direct economic benefits for governance participants. ## Purpose and Role in the Ecosystem The Distribution Vault functions as: 1. **Revenue Collector**: Accumulates various forms of protocol revenue from multiple sources 2. **Yield Distributor**: Enables proportional distribution of accumulated tokens to Zero token holders 3. **Value Capture Mechanism**: Creates a direct incentive for governance participation 4. **Economic Bridge**: Links protocol activity with governance token value ## Architecture Overview The Distribution Vault is referenced throughout the system as `TTGVault` or simply `vault`. Its address is established during deployment and made accessible to other core contracts via the `Registrar` or specific deployer contracts. ![Adopted Guidance Change Process Flow](https://docs.m0.org/images/technical-documentations/distribution-vault/distribution-flow.png) ## Revenue Sources The Distribution Vault accumulates tokens from four primary sources: ### 1. Excess Yield (from `MinterGateway`) This represents a continuous source of revenue coming from the interest rate spread between minters and earners. **Mechanism:** - The `MinterGateway.updateIndex()` function calculates `excessOwedM()` - the difference between total owed `$M` (based on minter interest rate) and actual total `$M` supply (growing at the earner interest rate) - When positive, this excess amount is minted directly to the Distribution Vault **Origin of Excess:** - **Interest Rate Spread**: The inherent difference between the higher interest rate charged to minters vs. the lower rate paid to earners (capped at 98% of the "safe rate") - **Rounding Differences**: Accumulated from principal-to-present value conversions within the continuous indexing math - **Penalties**: Charges imposed on minters for missed collateral updates or undercollateralization **Code Implementation:** ```solidity // In MinterGateway.sol function updateIndex() public override(IContinuousIndexing, ContinuousIndexing) returns (uint128 index_) { uint240 excessOwedM_ = excessOwedM(); if (excessOwedM_ > 0) IMToken(mToken).mint(ttgVault, excessOwedM_); // Mint $M to TTG Vault index_ = super.updateIndex(); // Update minter index and rate IMToken(mToken).updateIndex(); // Update earning index and rate } ``` ### 2. Power Token Auction Proceeds (from Power Token) The Power token contract auctions off newly inflated tokens in non-voting epochs, with proceeds directed to the vault. **Mechanism:** - Power token employs a Dutch auction mechanism where price decreases over time - When users purchase Power token via the `buy()` function, the calculated cost in `cashToken` is sent to the Distribution Vault **Code Implementation:** ```solidity // In Power token.sol function buy( uint256 minAmount_, uint256 maxAmount_, address destination_, uint16 expiryEpoch_ ) external returns (uint240 amount_, uint256 cost_) { // ... calculations for amount_ and cost_ ... emit Buy(msg.sender, amount_, cost_ = getCost(amount_)); _mint(destination_, amount_); if (!ERC20Helper.transferFrom(cashToken(), msg.sender, vault, cost_)) revert TransferFromFailed(); } ``` ### 3. Forfeited Standard Governor Proposal Fees (from `StandardGovernor`) Fees submitted with proposals in the `StandardGovernor` flow to the Distribution Vault if the proposal fails. **Mechanism:** - When creating a proposal via `StandardGovernor.propose()`, proposers pay a fee in the system's `cashToken` - If the proposal succeeds, the fee is returned to the proposer - If the proposal is defeated or expires, the fee is sent to the Distribution Vault via `sendProposalFeeToVault()` **Code Implementation:** ```solidity // In StandardGovernor.sol function sendProposalFeeToVault(uint256 proposalId_) external { ProposalState state_ = state(proposalId_); if (state_ != ProposalState.Expired && state_ != ProposalState.Defeated) revert FeeNotDestinedForVault(state_); uint256 proposalFee_ = _proposalFees[proposalId_].fee; if (proposalFee_ == 0) revert NoFeeToSend(); address cashToken_ = _proposalFees[proposalId_].cashToken; delete _proposalFees[proposalId_]; emit ProposalFeeSentToVault(proposalId_, cashToken_, proposalFee_); _transfer(cashToken_, vault, proposalFee_); } ``` ### 4. Direct/Arbitrary Transfers The Distribution Vault can receive tokens via direct transfers from any account or contract. These tokens need to be explicitly "registered" for distribution. **Mechanism:** - Tokens can be transferred directly to the vault contract address at any time using a standard ERC20 `transfer` or `transferFrom`. - To make these externally transferred tokens available for claiming, the external `distribute(token)` function must be called for the specific `token_` address. - The `distribute(token)` function calculates the increase in the vault's balance of `token_` since the last time distribute was called for that token (using the internal `_lastTokenBalances` tracking). It then registers this newly available amount (`amount_`) within the `currentEpoch_` (the epoch when `distribute()` is called). - Crucially, Zero token holders who held tokens *during* this `currentEpoch_` become eligible to claim a share of this distributed `amount_`. The claimable share for each eligible holder is calculated pro-rata based on their fractional Zero token ownership as snapshotted at the *end* of that specific `currentEpoch_`. **Code Implementation:** ```solidity // In DistributionVault.sol function distribute(address token_) external returns (uint256 amount_) { uint256 currentEpoch_ = clock(); amount_ = getDistributable(token_); emit Distribution(token_, currentEpoch_, amount_); unchecked { distributionOfAt[token_][currentEpoch_] += amount_; // Add to the distribution for the current epoch _lastTokenBalances[token_] += amount_; // Track this contract's latest balance } } function getDistributable(address token_) public view returns (uint256) { return IERC20(token_).balanceOf(address(this)) - _lastTokenBalances[token_]; } ``` ## Distribution Mechanism The Distribution Vault implements a sophisticated epoch-based, pro-rata distribution system that ensures fair allocation of accumulated tokens to Zero token holders. ### Core Mechanics 1. **Epoch-Based Accounting**: Each token distribution is recorded in the current epoch (a time period, typically 15 days) 2. **Historical Balances**: Claims are calculated based on Zero token holders' balances during past epochs 3. **Pro-Rata Distribution**: Tokens are distributed proportionally to Zero token holdings 4. **Claimable Design**: Zero token holders must claim their share of distributed tokens ### Distribution Process 1. **Accumulation**: Tokens flow into the vault through various mechanisms 2. **Registration**: The `distribute(token)` function is called, recording the new token amount for the current epoch 3. **Claiming**: Zero token holders call `claim()` or `claimBySig()` to receive their share for specific epochs 4. **Calculation**: The claimable amount is computed based on the holder's proportional ownership during each claimed epoch ### Key Functions ```solidity // Allows claiming tokens for a specific address function claim( address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_ ) external returns (uint256 claimed_) // Allows claiming with a signature (helpful for gas-less transactions) function claimBySig( address account_, address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_, uint256 deadline_, bytes memory signature_ ) external returns (uint256 claimed_) // Calculates the claimable amount for a given address, token, and epoch range function getClaimable( address token_, address account_, uint256 startEpoch_, uint256 endEpoch_ ) public view returns (uint256 claimable_) ``` ## Technical Implementation Details ### Contract Structure The Distribution Vault inherits from StatefulERC712, which provides EIP-712 signature verification functionality: ```solidity contract DistributionVault is IDistributionVault, StatefulERC712 { // ... implementation ... } ``` ### Key State Variables ```solidity // The scale to apply when accumulating an account's claimable token to minimize precision loss uint256 internal constant _GRANULARITY = 1e9; // EIP-712 typehash for claim function bytes32 public constant CLAIM_TYPEHASH = 0x4b4633c3c305de33d5d9cf70f2712f26961648cd68d020c2556a9e43be58051d; // Address of the Zero Token contract address public immutable zeroToken; // Mapping of last recorded balance per token mapping(address token => uint256 balance) internal _lastTokenBalances; // Mapping of distributions per token, epoch, and amount mapping(address token => mapping(uint256 epoch => uint256 amount)) public distributionOfAt; // Mapping of claimed status per token, epoch, and account mapping(address token => mapping(uint256 epoch => mapping(address account => bool claimed))) public hasClaimed; ``` ### Signature-Based Claims The Distribution Vault supports signature-based claims, allowing users to authorize others to claim on their behalf: ```solidity function claimBySig( address account_, address token_, uint256 startEpoch_, uint256 endEpoch_, address destination_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_ ) external returns (uint256) { // ... signature verification logic ... // ... deadline verification ... return _claim(account_, token_, startEpoch_, endEpoch_, destination_); } ``` ## Economic Implications The Distribution Vault creates several important economic effects: 1. **Zero Token Value Accrual**: By directing protocol revenue to Zero token holders, it increases the token's economic value 2. **Governance Incentive Alignment**: Creates a direct financial incentive for governance participation 3. **Sustainable Protocol Economics**: Ensures that excess yield and protocol fees flow back to governance participants 4. **Diversified Revenue Streams**: Collects different token types from multiple sources, creating a robust revenue model ## Integration with the Wrapped `$M` Token (Extended) The Distribution Vault also interacts with the Wrapped `$M` (wM) system: 1. **Excess Yield Destination**: The wM contract can send excess yield to the Distribution Vault via its `claimExcess()` function 2. **Economic Loop Completion**: This creates a complete economic loop where even wrapped token operations can generate value for governance participants ## Conclusion The Distribution Vault stands as a critical infrastructure component in the M0 protocol, serving as the central hub for value capture and redistribution. Through its sophisticated pro-rata distribution mechanism, it ensures that protocol operations consistently generate value for governance participants, creating strong economic incentives for system participation and alignment. By collecting revenue from multiple sources and making it claimable by Zero token holders, the vault creates a direct financial link between protocol usage and governance value, strengthening the economic sustainability of the entire ecosystem. One can check straight on the [dashboard](http://dashboard.m0.org){rel=""nofollow""} the number of `$M` accumulated by the vault and not claimed so far. # M0 Extensions ## What Are M0 Extensions? M0 Extensions are the **application layer** of the M0 Protocol. They are custom ERC-20 stablecoins that developers build on the M0 platform, inheriting its security and yield properties while adding their own unique features, branding, and business logic. Think of M0 as the secure, collateralized foundation -- like digital infrastructure. Extensions are the applications, products, and user experiences built on top of this foundation. ### The Extension Architecture ```text Your Custom Stablecoin (Extension) | wraps/unwraps M0 (Foundation) | backed by US Treasury Collateral ``` **Key Relationship:** Extensions are backed 1:1 by eligible collateral, meaning: - 1 Extension Token = 1 unit of value (always redeemable) - Extensions inherit the M0 platform's stability and regulatory clarity - Extension contracts can earn yield (if approved by governance) - Developers control how that yield flows to users, treasuries, or other destinations ### Why Build an Extension? #### Full Customization - **Yield Distribution**: Route yield to users, treasury, or split however you want - **Access Controls**: Implement KYC, geographic restrictions, or institutional-only access - **Fee Mechanisms**: Add transaction fees, management fees, or revenue sharing - **Branding**: Create a fully branded stablecoin (YourAppUSD) or keep it generic #### Inherited Benefits - **Regulatory Clarity**: Built on M0's compliant, audited infrastructure - **Shared Liquidity**: Tap into the M0 ecosystem's shared liquidity layer - **Cross-Chain Ready**: Deploy on any chain where M0 operates - **Battle-Tested Security**: Leverage M0's proven collateral and governance systems #### Business Value - **Control**: Unlike integrating someone else's stablecoin, you own the user relationship - **Revenue**: Capture value through yield management and optional fees - **Flexibility**: Evolve your stablecoin's features as your needs change ### Common Extension Types #### Treasury Extensions Route 100% of yield to a single treasury address while providing users with a stable, non-rebasing token experience. **Use Cases**: Protocol treasuries, ecosystem development funds, corporate reserves #### User Yield Extensions Share yield with token holders while taking a small protocol fee for sustainability. **Use Cases**: DeFi protocols, consumer apps, yield savings accounts #### Institutional Extensions Provide granular, per-account control with custom fee arrangements and whitelisting. **Use Cases**: Prime brokerages, fintech platforms, institutional treasury management ### Who Should Build Extensions? **Recommended for:** - **Application Developers**: Games, payments, DeFi protocols wanting branded stablecoins - **Ecosystem Builders**: L1/L2 chains needing native stablecoins for their ecosystems - **Fintech Companies**: Businesses requiring custom compliance or yield distribution - **Treasury Managers**: Organizations needing yield-bearing accounts with specific controls **Consider Wrapped `$M` instead if:** - You just need standard DeFi integration (AMM pools, lending markets) - You prefer using an existing, established token rather than deploying your own ### Real-World Examples - [**Wrapped `$M`**](https://docs.m0.org/protocol/wrapped-m): The "reference implementation" showing standard DeFi compatibility - [**Noble USD**](https://dollar.noble.xyz/){rel=""nofollow""}: Cross-chain stablecoin serving the Cosmos ecosystem ### Technical Overview Extensions are smart contracts that: 1. **Inherit from MExtension.sol**: A base contract providing core wrap/unwrap functionality 2. **Implement Custom Logic**: Add your unique features, access controls, and yield distribution 3. **Gain Earner Approval**: Get governance approval to earn yield 4. **Deploy Anywhere**: Launch on Ethereum, L2s, or any supported chain **Key Functions Every Extension Must Implement:** - `wrap(recipient, amount)`: Convert to your extension token - `unwrap(recipient, amount)`: Convert your extension token back - Custom yield claiming/distribution logic (your choice how this works) ### Getting Started Ready to build your own stablecoin? The [Build section](https://docs.m0.org/build/overview) provides everything you need: - **Model Selection Guide**: Choose the right template for your use case - **Step-by-Step Deployment**: Deploy and configure your extension - **Earner Approval Process**: Get governance approval for yield earning - **Integration Examples**: Real code examples and best practices ## SwapFacility Deep Dive ### Architecture Overview SwapFacility serves as the universal router for the M0 Extension ecosystem, enabling atomic conversions between any approved M0 Extensions. The contract maintains minimal state while relying on the TTG governance system for extension approval and access control. #### Core Dependencies - **M Token Base**: The foundational rebasing token that backs all extensions - **TTG Registrar**: Governance-controlled registry that maintains the approved earners list - **ReentrancyLock**: Advanced protection using transient storage for atomic operations ### Swap Operations #### Extension-to-Extension Swaps ```solidity function swap(address extensionIn, address extensionOut, uint256 amount, address recipient) external ``` **Process Flow:** 1. Validates both extensions are approved earners via `_revertIfNotApprovedExtension()` 2. Transfers extension tokens from user to SwapFacility 3. Calls `extensionIn.unwrap()` to convert to $M 4. Measures actual `$M` received (accounts for rounding differences in v1 extensions) 5. Approves and calls `extensionOut.wrap()` to mint target extension tokens #### Direct `$M` Token Operations ```solidity function swapInM(address extensionOut, uint256 amount, address recipient) external function swapOutM(address extensionIn, uint256 amount, address recipient) external ``` **Access Control Differences:** - `swapInM`: Anyone can convert `$M` to extensions - `swapOutM`: Requires `M_SWAPPER_ROLE` or permission for specific extension ### Permission System #### Two-Tier Extension Model 1. **Standard Extensions**: Open for general extension-to-extension swaps 2. **Permissioned Extensions**: Require special authorization for `$M` conversions ```solidity function setPermissionedExtension(address extension, bool permissioned) external function setPermissionedMSwapper(address extension, address swapper, bool allowed) external ``` #### Role-Based Access - `DEFAULT_ADMIN_ROLE`: Contract administration and upgrades - `M_SWAPPER_ROLE`: Global permission for `swapOutM` operations on standard extensions ### Security Features #### Advanced Reentrancy Protection The contract inherits from `ReentrancyLock` which implements transient storage-based locking: ```solidity modifier isNotLocked() { if (Locker.get() != address(0)) revert ContractLocked(); address caller_ = isTrustedRouter(msg.sender) ? IMsgSender(msg.sender).msgSender() : msg.sender; Locker.set(caller_); _; Locker.set(address(0)); } ``` #### Trusted Router System For complex integrations (like UniswapV3SwapAdapter), trusted routers can specify the original caller: - Direct calls: `msg.sender` is stored as the locker - Trusted router calls: `IMsgSender(msg.sender).msgSender()` retrieves the original user Extensions can call `swapFacility.msgSender()` to get the true transaction initiator. #### Governance Integration All extension approvals are checked against the TTG Registrar: ```solidity function _isApprovedEarner(address extension) private view returns (bool) { return IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, extension); } ``` ### Gas Optimization #### Permit Integration All swap functions include permit variants supporting both EIP-2612 and EIP-712 signatures: ```solidity function swapWithPermit(..., uint256 deadline, uint8 v, bytes32 r, bytes32 s) external function swapWithPermit(..., uint256 deadline, bytes calldata signature) external ``` #### Balance Tracking Precision For compatibility with Wrapped `$M` v1, the contract measures actual balance changes: ```solidity uint256 mBalanceBefore = _mBalanceOf(address(this)); IMExtension(extensionIn).unwrap(address(this), amount); amount = _mBalanceOf(address(this)) - mBalanceBefore; ``` ### Technical Implementation #### Immutable Architecture ```solidity address public immutable mToken; address public immutable registrar; ``` Core dependencies are immutable for security, while operational parameters remain upgradeable. #### State Management The contract maintains minimal state: - `permissionedExtensions`: Extensions requiring special authorization - `permissionedMSwappers`: Authorized swappers for specific permissioned extensions #### Event System ```solidity event Swapped(address indexed extensionIn, address indexed extensionOut, uint256 amount, address indexed recipient); event SwappedInM(address indexed extensionOut, uint256 amount, address indexed recipient); event SwappedOutM(address indexed extensionIn, uint256 amount, address indexed recipient); ``` ### Economic Guarantees SwapFacility maintains perfect 1:1 value relationships by design: - No slippage or trading fees - Atomic execution prevents arbitrage - All conversions preserve dollar parity - Extensions remain fully backed by eligible collateral The contract creates implicit liquidity between all M0 Extensions without requiring separate AMM pools, enabling seamless cross-extension value transfer. # Limit Order Protocol The Limit Order Protocol is the onchain settlement layer for M0's Liquidity Delivery Network. It provides a trustless, intent-based protocol for exchanging tokens across chains with support for partial fills, gasless orders, and secure cancellation. ## Overview The Limit Order Protocol allows users to submit same-chain or cross-chain limit orders to exchange one token for another. While the primary use case is stablecoin orchestration - swapping between USDC/USDT and M0 extensions - the protocol is asset-agnostic. ### Key Design Principles - **Trustless execution** - No trust assumptions on users or solvers; only bridge contracts and messaging protocols are trusted parties - **Partial fills** - Orders can be filled incrementally, allowing solvers to cycle inventory for large orders - **Deterministic pricing** - Users specify exact `amountIn` and `amountOut` - **Cross-chain native** - Orders are created on the source chain but can be filled on any configured destination - **Secure order cancellations** - Refunds respect finality buffers to prevent race conditions between cancellations and fills ## Architecture The system consists of: - **`OrderBook` contracts** deployed on each supported chain - **Portal/Messenger** for cross-chain communication via [Portal V2](https://docs.m0.org/protocol/portal-v2) - **Solvers** that monitor and fill orders ### Chain Interactions For **same-chain orders**: 1. User opens order on Chain A 2. Solver fills order on Chain A 3. Tokens are exchanged atomically For **cross-chain orders**: 1. User opens order on Chain A (origin), locking input tokens 2. Solver fills order on Chain B (destination), delivering output tokens to recipient 3. Fill report is sent back to Chain A via the messaging layer 4. Solver receives input tokens on Chain A ## Order Lifecycle ### 1. Order Creation Users create orders by calling `openOrder()` on the `OrderBook` contract on the origin chain. The order specifies: | Parameter | Description | | -------------- | ----------------------------------------------------------- | | `destChainId` | Destination chain where tokens will be delivered | | `tokenIn` | Input token address on origin chain | | `tokenOut` | Output token address on destination chain | | `amountIn` | Amount of input token to exchange | | `amountOut` | Expected amount of output token | | `recipient` | Address to receive tokens on destination chain | | `fillDeadline` | Timestamp by which order must be filled | | `solver` | (Optional) Exclusive solver address, or zero for any solver | ```solidity struct OrderParams { uint32 destChainId; uint32 fillDeadline; address tokenIn; bytes32 tokenOut; // bytes32 supports non-EVM destinations uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; } ``` When an order is created: - Input tokens are transferred from the user to the `OrderBook` - A unique `orderId` is generated from the order parameters - The order is stored with status `Created` ### 2. Order Filling Solvers fill orders by calling `fillOrder()` on the destination chain's `OrderBook`: ```solidity function fillOrder( bytes32 orderId_, OrderData calldata orderData_, FillParams calldata fillerParams_ ) external; ``` **Partial fills** are supported - solvers can fill any portion of an order: - The solver specifies `amountOutToFill` in their fill parameters - A proportional amount of `tokenIn` is released to the solver - Multiple solvers can fill the same order until it's complete **Same-chain fills** release input tokens immediately to the solver. **Cross-chain fills** trigger a fill report sent back to the origin chain: - The messenger delivers the fill report - Input tokens are released to the solver's specified `originRecipient` ### 3. Order Completion An order reaches `Completed` status when: - The full `amountOut` has been filled, OR - The user claims a refund after cancellation or deadline expiry ### 4. Cancellation and Refunds Users can cancel orders before the fill deadline by calling `requestCancelOrder()`: ```solidity function requestCancelOrder(bytes32 orderId_) external; ``` For gasless cancellation, users can sign an EIP-712 message and have a relayer submit it: ```solidity function requestCancelOrderFor(bytes32 orderId_, bytes calldata signature_) external; ``` ::warning **Cross-chain refund delays** For cross-chain orders, refunds cannot be claimed immediately. A **finality buffer** must pass to ensure no in-flight fills arrive after cancellation. :: | Scenario | Refund Available | | --------------------------- | ------------------------------------------ | | Same-chain order cancelled | Immediately | | Cross-chain order cancelled | After `cancelRequestedAt + finalityBuffer` | | Order deadline expired | After `fillDeadline + finalityBuffer` | The finality buffer is configured per destination chain and accounts for messaging latency and potential reorgs. ::note Orders in `CancelRequested` status can still receive fills until the refund is claimed. This protects solvers who submitted fills before seeing the cancellation request. :: ### Edge Cases **Late fill reports:** If a cross-chain fill is not reported within the finality buffer, the user can claim a refund after `fillDeadline + finalityBuffer`. Late `reportFill()` calls will fail if the refund was already claimed - the solver bears the loss in this scenario. This can occur when messaging networks experience delays, fill transactions are stuck in the destination chain mempool, or bridge relays are slower than expected. **Concurrent cancel and fill:** If a user requests cancellation while a fill is in-flight, the fill can still succeed until the refund is claimed. `CancelRequested` status does **not** block fills. Fill reports received before the refund claim are processed normally. ## Gasless Orders Users can create orders without paying gas using EIP-712 signatures: ```solidity function openOrderFor( GaslessOrderParams calldata orderParams_, bytes calldata orderSignature_ ) external returns (bytes32); ``` A relayer (often the solver) submits the order on behalf of the user. The signature includes all order parameters, the user's nonce (to prevent replay attacks), and the origin chain ID (to prevent cross-chain replay). Permit signatures can also be combined with gasless orders to allow token approval in the same transaction: ```solidity // With split signature (v, r, s) function openOrderForWithPermit( GaslessOrderParams calldata orderParams_, bytes calldata orderSignature_, uint256 deadline_, uint8 v_, bytes32 r_, bytes32 s_ ) external returns (bytes32); // With packed signature function openOrderForWithPermit( GaslessOrderParams calldata orderParams_, bytes calldata orderSignature_, uint256 deadline_, bytes memory permitSignature_ ) external returns (bytes32); ``` ## Order Identification Each order has a unique ID derived from hashing its parameters: ```solidity orderId = keccak256(abi.encodePacked( version, sender, nonce, originChainId, destChainId, fillDeadline, tokenIn, tokenOut, amountIn, amountOut, recipient, solver )) ``` This ensures orders are globally unique across all chains, solvers can verify order authenticity without trusting the origin chain, and the same order parameters always produce the same ID. ## Solver Integration Solvers monitor `OrderBook` events to discover fillable orders: ```solidity event OrderOpened( bytes32 indexed orderId, address sender, address tokenIn, uint128 amountIn, uint32 indexed destChainId, bytes32 tokenOut, uint128 amountOut, bytes32 indexed solver ); ``` ### Exclusive Solvers If an order specifies a `solver` address, only that address can fill the order. Setting `solver` to zero allows any solver to fill (permissionless racing). ### Fill Strategy Solvers determine their own fill strategy - fill entire orders when inventory allows, partially fill large orders to manage risk, or prioritize orders by profitability or deadline urgency. ### Receiving Tokens For cross-chain fills, solvers specify an `originRecipient` in their fill parameters. This address receives the input tokens on the origin chain after the fill report is processed. ## Security Considerations ### Finality Buffers Each destination chain has a configured finality buffer that determines how long to wait before processing refunds and protection against reorgs causing double-spends. Finality buffer changes are time-delayed to protect existing orders: - **Increases** take effect immediately (more conservative) - **Decreases** take effect after the old buffer duration passes ### Token Compatibility ::warning **Non-standard token risks** The `OrderBook` has known edge cases with non-standard tokens. Review this section carefully before whitelisting tokens. :: | Token Type | Behavior | Severity | Recommendation | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------ | ---------------------------------------------------------------------- | | **Pausable tokens** (USDC/USDT) | If paused during cross-chain fill, user may receive destination tokens AND claim origin refund (double-spend) | **CRITICAL** | Avoid for cross-chain orders or implement pause monitoring | | **Rebase tokens** | If token rebases downward after order creation, `reportFill()` and `claimRefund()` may revert permanently, locking funds | **HIGH** | Do not use | | **Yield-bearing tokens** | Yield accrues to `OrderBook` contract, not recoverable by user or solver | **MEDIUM** | Use short order lifetimes; configure fee recovery via `MEarnerManager` | | **Fee-on-transfer tokens** | `fillOrder()` reverts via `safeTransferExact`; `claimRefund()` may lose fee amount | **MEDIUM** | Do not use | ### Solver Safety ::warning **Solver fund loss risks** The following behaviors can cause fund loss for solvers. :: **Invalid recipient address:** Passing `address(0)` as `originRecipient` in `FillParams` will silently burn tokens. The contract does not validate this address. Always verify `originRecipient` is a valid, non-zero address before submitting fills. **Dust fills with decimal mismatch:** When `tokenIn` has fewer decimals than `tokenOut`, very small fills can round to zero `amountIn`. The pro-rata formula is: ```text amountInToRelease = (amountIn * amountOutToFill) / amountOut ``` Solvers should validate minimum fill amounts to avoid zero-value releases. ### Rounding Behavior Pro-rata calculations for partial fills always round down (floor): - Users receive at least their proportional share - Solvers may receive slightly less than the theoretical maximum - Very small fills relative to decimal differences may round to zero ### Messenger Trust The only trusted component is the messenger contract that relays fill reports between chains. M0 uses its [Portal V2](https://docs.m0.org/protocol/portal-v2) infrastructure for secure cross-chain messaging. ## Contract Reference The Limit Order Protocol is implemented in the [`OrderBook.sol`](https://github.com/m0-foundation/liquidity-delivery/blob/main/evm/src/OrderBook.sol){rel=""nofollow""} contract. ### Key Functions | Function | Description | | -------------------------- | ----------------------------------------------------------- | | `openOrder()` | Create a new limit order | | `openOrderWithPermit()` | Create order with EIP-2612 permit (2 overloads) | | `openOrderFor()` | Create gasless order on behalf of user | | `openOrderForWithPermit()` | Create gasless order with permit (2 overloads) | | `fillOrder()` | Fill an order (full or partial, 2 overloads) | | `requestCancelOrder()` | Request order cancellation | | `requestCancelOrderFor()` | Request gasless order cancellation | | `claimRefund()` | Claim refund for cancelled/expired order | | `reportFill()` | Report a cross-chain fill (messenger only) | | `setDestinationConfig()` | Configure destination chain (requires `DEFAULT_ADMIN_ROLE`) | ### View Functions | Function | Description | | -------------------------------- | ------------------------------------------- | | `getOrder()` | Get order details by ID | | `getOrderId()` | Compute order ID from `OrderData` | | `getFilledAmounts()` | Get filled amounts for an order | | `getSenderNonce()` | Get next nonce for gasless orders | | `isDestinationSupported()` | Check if chain is supported | | `getDestinationFinalityBuffer()` | Get finality buffer for chain | | `getDestinationConfig()` | Get full destination configuration | | `getGaslessOrderDigest()` | Get EIP-712 digest for gasless order | | `getCancelRequestDigest()` | Get EIP-712 digest for gasless cancellation | ## Data Structures ### OrderStatus ```solidity enum OrderStatus { DoesNotExist, // Order has never been created Created, // Order is active and fillable CancelRequested, // User requested cancellation (fills still accepted) Completed // Order fully filled or refund claimed } ``` ### Order Complete data about an order originated on this chain (stored on origin chain only): ```solidity struct Order { OrderStatus status; uint16 version; address sender; uint64 nonce; uint32 destChainId; uint32 fillDeadline; uint32 cancelRequestedAt; address tokenIn; bytes32 tokenOut; uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; } ``` ### OrderData Used to identify and fill orders on the destination chain: ```solidity struct OrderData { uint16 version; bytes32 sender; uint64 nonce; uint32 originChainId; uint32 destChainId; uint64 fillDeadline; bytes32 tokenIn; bytes32 tokenOut; uint128 amountIn; uint128 amountOut; bytes32 recipient; bytes32 solver; } ``` ### FillParams ```solidity struct FillParams { uint128 amountOutToFill; bytes32 originRecipient; } ``` ### FillReport ```solidity struct FillReport { bytes32 orderId; uint128 amountInToRelease; uint128 amountOutFilled; bytes32 originRecipient; bytes32 tokenIn; } ``` ### FilledAmounts ```solidity struct FilledAmounts { uint128 amountInReleased; uint128 amountOutFilled; } ``` ## Events ```solidity // Order lifecycle event OrderOpened( bytes32 indexed orderId, address sender, address tokenIn, uint128 amountIn, uint32 indexed destChainId, bytes32 tokenOut, uint128 amountOut, bytes32 indexed solver ); event OrderFilled( bytes32 indexed orderId, address indexed solver, uint128 amountInToRelease, uint128 amountOutFilled ); event OrderCompleted(bytes32 orderId); // Cancellation event CancelRequested(bytes32 indexed orderId, uint32 cancelRequestedAt); event RefundClaimed( bytes32 indexed orderId, address indexed sender, uint128 amountInRefunded ); // Configuration event DestinationConfigUpdated( uint32 indexed destChainId, bool newIsSupported, uint32 newFinalityBuffer, uint64 newFinalityBufferEffectiveTimestamp ); ``` ## Error Codes | Error | Description | | ------------------------- | -------------------------------------------------------- | | `AmountInZero` | Order input amount cannot be zero | | `AmountOutZero` | Order output amount cannot be zero | | `FillAmountZero` | Fill amount must be greater than zero | | `FinalityPending` | Finality buffer has not elapsed; cannot claim refund yet | | `InvalidDeadline` | Fill deadline is in the past or invalid | | `InvalidDestinationChain` | Destination chain is not supported | | `InvalidFinalityBuffer` | Finality buffer value is invalid | | `InvalidNonce` | Nonce doesn't match sender's expected nonce | | `InvalidOrderStatus` | Order is not in a valid status for this operation | | `InvalidOrderVersion` | Order version doesn't match contract version | | `InvalidOriginChain` | Origin chain ID doesn't match this chain | | `InvalidRecipient` | Recipient address is invalid | | `InvalidReport` | Fill report data is invalid | | `NotAuthorized` | Caller is not authorized for this operation | | `OrderExpired` | Order fill deadline has passed | | `OrderAlreadyFilled` | Order has already been completely filled | | `OrderIdMismatch` | Computed order ID doesn't match provided ID | ## Related - [Accessing Liquidity](https://docs.m0.org/build/accessing-liquidity) - Overview of M0's liquidity infrastructure - [Portal V2](https://docs.m0.org/protocol/portal-v2) - Cross-chain bridge and messaging system - [M Portals (V1)](https://docs.m0.org/protocol/m-portals) - Previous portal architecture reference - [Orchestration API](https://docs.m0.org/api-reference/orchestration/overview) - API for requesting quotes and transaction payloads - [Source Code](https://github.com/m0-foundation/liquidity-delivery){rel=""nofollow""} - Smart contract repository ```text ``` # M0 Portals ## Overview ### Native Multichain `$M` The core objective of M0's multichain strategy is to make the native `$M` token accessible across various blockchains beyond Ethereum mainnet. This aims to provide users with a consistent experience, particularly regarding yield-earning capabilities, regardless of the chain they operate on, while maintaining Ethereum as the authoritative source for `$M` issuance and governance. **M Portals** are the set of smart contracts that facilitate this cross-chain functionality. They are responsible for bridging the `$M` token itself, as well as propagating essential system information like the yield index and governance parameters between Ethereum and connected Spoke chains. To cater to different ecosystem needs and technical requirements, M0 offers two distinct implementations of the Portal system. ### M Portal Implementations: Standard vs. Lite M0 provides two portal versions built on different underlying technologies. While they share the same core goal of bridging `$M` and its metadata, they have key differences in architecture, features, and chain support. 1. **M Portal (Standard):** The original, feature-rich implementation built on the **Wormhole Native Token Transfer (NTT)** framework. It is designed for maximum compatibility, supporting both EVM and non-EVM chains (like Solana) and allowing for spoke-to-spoke transfers. 2. **M Portal Lite:** A streamlined, gas-efficient, and more modular implementation built on the **Hyperlane** messaging protocol. It is designed specifically for **EVM-only environments**, follows a stricter hub-and-spoke model (no spoke-to-spoke transfers), and introduces enhanced security mechanisms and a developer-friendly upgradeable design. ### At a Glance: M Portal vs. M Portal Lite | Feature | M Portal (Standard) | M Portal Lite | | --------------------- | ---------------------------------- | ----------------------------------------------- | | **Bridge Technology** | Wormhole NTT | Hyperlane | | **Chain Support** | EVM & non-EVM (e.g., Solana) | EVM-only | | **Communication** | Hub-Spoke & Spoke-Spoke | Strictly Hub-Spoke | | **Contract Design** | Monolithic (inherits `NttManager`) | Modular & Extendable (uses `IBridge` interface) | | **Security Model** | Hub balance = Total locked | Hub tracks `bridgedPrincipal` per spoke chain | | **Chain IDs** | Wormhole `uint16` IDs | Standard EVM `uint256` IDs | ### The Hub-and-Spoke Model Both M Portal implementations employ a Hub-and-Spoke architecture for multichain deployment: ![Hub & Spoke](https://docs.m0.org/images/technical-documentations/m-portals/hub.png) - **Hub Chain (Ethereum):** - The single source of truth for the native `$M` token (where it's initially minted and governed). - Hosts the `HubPortal` contract. - Token Mechanism: **Lock-and-Release**. When bridging *from* the Hub, `$M` tokens are locked. When bridging *back* to the Hub, these locked tokens are released. - Acts as the source for propagating the `$M` Earning Index and TTG Registrar values. - *(M Portal Lite Specific):* The `HubPortal` also tracks the principal amount of `$M` bridged to each spoke chain to ensure it never releases more tokens than were locked for that specific spoke. - **Spoke Chains (e.g., Optimism, Arbitrum):** - Host representations of the `$M` token. - Host `SpokePortal` contracts. - Token Mechanism: **Mint-and-Burn**. When bridging *to* a Spoke chain, a corresponding amount of `$M` representation is minted. When bridging *from* a Spoke chain, the `$M` representation is burned. - Receive and apply the `$M` Earning Index and TTG Registrar values propagated from the Hub. This model ensures that the canonical `$M` supply originates and is controlled solely on Ethereum, while Spoke chains manage representations backed by the locked tokens on the Hub. ## Key Components ### Common Components - **User:** The EOA or contract initiating or receiving cross-chain transfers. - **`$M` Token:** The native ERC-20 compliant token contract deployed on Ethereum. - **Wrapped `$M` / Extensions:** ERC-20 wrappers for `$M` that must implement `wrap` and `unwrap` functions. - **`HubPortal` (Ethereum):** Manages locking/releasing of native `$M` and propagates metadata. - **`SpokePortal` (Spoke Chains):** Manages minting/burning of the Spoke chain's `$M` representation and receives metadata. ### M Portal (Wormhole) Specific Components - **Wormhole NTT Framework:** The underlying technology. `HubPortal` and `SpokePortal` inherit from its `NttManager` contract. - **Wormhole Transceiver:** A Wormhole contract on each chain that `NttManager` uses to send/receive low-level messages. - **Wormhole Core / Guardians / Relayers:** The fundamental Wormhole infrastructure that validates and delivers messages (VAAs). - **`MerkleTreeBuilder.sol`:** A contract used on the Hub to generate Merkle roots for propagating large datasets (like earner lists) efficiently to non-EVM chains like Solana. ### M Portal Lite (Hyperlane) Specific Components - **`IBridge.sol`:** A generic interface that decouples the Portal logic from the underlying bridge technology. - **`HyperlaneBridge.sol`:** The implementation of `IBridge` that interacts with the Hyperlane protocol (`IMailbox`) to send and receive messages. - **Hyperlane Mailbox / Relayers:** The core Hyperlane infrastructure for message dispatch, security, and delivery. - **Upgradeable Contracts:** `HubPortal` and `SpokePortal` are built using the OpenZeppelin Upgrades pattern, allowing for proxy-based upgrades. ## Core Bridging Process The transfer of `$M` (or its extensions like Wrapped `$M`) between the Hub and Spoke chains follows a similar logical flow in both implementations, orchestrated by the Portals and their underlying messaging bridge. ### Transfer: Hub Chain to Spoke Chain ![Hub & Spoke Architecture](https://docs.m0.org/images/technical-documentations/m-portals/hub.png) 1. **User Interaction:** The user initiates a transfer by calling a function on the `HubPortal`contract on Ethereum, specifying the destination chain and amount. Beforehand, users can call a function to get the estimated delivery fee for the cross-chain transaction. - **M Portal (Standard):** The function is `quoteDeliveryPrice`, inherited from the Wormhole NTT framework. - **M Portal Lite:** The function is `quoteTransfer`. After getting the fee and approving the `HubPortal` to spend their `$M` or Wrapped `$M`, the user makes the final transfer call. 2. **Token Handling:** - If the user transferred Wrapped `$M`, the `HubPortal` first unwraps it to the native `$M` token. - The native `$M` tokens are then **locked** within the `HubPortal` contract. - *(M Portal Lite Specific):* The `HubPortal` increments its internal `bridgedPrincipal` counter for the destination spoke chain. 3. **Message Creation & Sending:** - The `HubPortal` constructs a cross-chain message containing transfer details (amount, recipient, destination chain) and additional metadata (like the current `$M` Index). - It interacts with its configured bridge to send this message. - **M Portal (Wormhole):** Interacts with the Wormhole Transceiver via the `NttManager` logic. - **M Portal Lite (Hyperlane):** Interacts with the `HyperlaneBridge` contract, which dispatches via the Hyperlane Mailbox. 4. **Cross-Chain Relaying (Offchain):** The message is validated by the underlying bridge's security mechanism (Wormhole Guardians or Hyperlane ISMs). Once validated, the message is delivered to the destination Spoke chain. 5. **Message Reception & Processing:** The bridge contract on the Spoke chain receives the message and passes it to the `SpokePortal`. 6. **Token Minting & Delivery:** - The `SpokePortal` processes the message. - It **mints** the corresponding amount of the `$M` token representation on the Spoke chain. - If the original transfer was for Wrapped `$M`, the `SpokePortal` wraps the newly minted `$M`. - The final token is transferred to the recipient address on the Spoke chain. ### Transfer: Spoke Chain to Hub Chain This is largely the reverse process. 1. **User Interaction:** User calls the `SpokePortal` on a Spoke chain to send tokens back to the Hub. 2. **Token Handling:** - If transferring Wrapped `$M`, it's unwrapped to the native `$M` representation. - The native `$M` tokens are **burned** by the `SpokePortal`. 3. **Message Creation & Sending:** The `SpokePortal` sends a cross-chain message back to the Hub Chain (Ethereum). 4. **Cross-Chain Relaying:** The message is relayed back to Ethereum. 5. **Message Reception & Processing:** The bridge on Ethereum receives the message and passes it to the `HubPortal`. 6. **Token Releasing & Delivery:** - The `HubPortal` processes the message and **releases** (unlocks) the equivalent amount of native `$M` tokens. - *(M Portal Lite Specific):* The `HubPortal` first verifies that the requested release amount does not exceed the `bridgedPrincipal` for that source spoke chain. It then decrements the counter. - The released tokens (wrapped if necessary) are delivered to the recipient on Ethereum. ### Transfer: Spoke Chain to Spoke Chain - **M Portal (Wormhole):** **Supported.** This process involves burning tokens on the source Spoke and minting them on the destination Spoke, with a message relayed via Wormhole. - **M Portal Lite (Hyperlane):** **Not Supported.** The system is strictly Hub-and-Spoke. To move assets between spokes, a user must first bridge back to the Hub (Ethereum) and then out to the new Spoke chain. ## Metadata Propagation Beyond token value, M Portals are crucial for synchronizing the M0 protocol state across chains. The purpose and data are the same for both implementations, but the transport mechanism differs. - **Purpose:** To ensure yield calculations (\`\`$M` Earning Index`) and governance parameters (`TTG Registrar` values) are consistent between the Hub and all Spoke chains. - **Propagated Data:** - **`$M` Earning Index:** A continuously accruing value representing the aggregate yield earned within the M0 system on Ethereum. Propagating this allows the `$M` representation on Spoke chains to reflect the correct yield. - **TTG Registrar Values:** Key-value pairs stored in the `Registrar` contract on Ethereum, set by M0 governance (TTG). Examples include the `EARNERS_LIST` which dictates which addresses are eligible for yield. Propagation ensures Spoke chains respect Hub governance decisions. - **Propagation Mechanisms:** - **Implicit (with token transfers):** Every token transfer message from the `HubPortal` automatically includes the current \`\`$M` Earning Index`in its payload. The receiving`SpokePortal` uses this to update its local state. - **Explicit (on-demand):** Anyone can permissionlessly call functions on the `HubPortal`to send the latest metadata to a specific Spoke chain. - `sendMTokenIndex(uint16 destinationChainId)`: Sends the current `$M` Index to a specific Spoke chain. - `sendRegistrarKey(uint16 destinationChainId, bytes32 key)`: Sends a specific key from the Hub `Registrar`. - `sendRegistrarListStatus(uint16 destinationChainId, bytes32 listKey, address entry)`: Sends the inclusion status of an address within a list. - **Frequency:** Explicit updates are often triggered periodically by automated bots to ensure timely synchronization even without user transfer activity. ## Underlying Technology ### Wormhole NTT (M Portal) M Portal (Standard) is built *using* the Wormhole Native Token Transfer (NTT) framework. - **Integration:** `HubPortal` and `SpokePortal` inherit from Wormhole's `NttManager` abstract contract. This provides the base functionality for interacting with Wormhole Transceivers, encoding/decoding messages, and handling standard NTT flows. - **Customization:**M0 extends this base functionality to: - Implement the specific Lock/Release (Hub) and Mint/Burn (Spoke) logic. - Handle the automatic unwrap/wrap mechanism for `$M` extensions. - Inject the M0-specific metadata (like \`\`$M` Earning Index`) into the `additionalPayload` field of standard NTT messages. - Define and handle custom message types for explicit metadata propagation. - **Abstraction:** Using the NTT framework allows M Portals to leverage Wormhole's established messaging infrastructure while focusing on M0-specific logic. ### Hyperlane (M Portal Lite) M Portal Lite uses Hyperlane as its messaging layer and features a more modular design. - **Integration:** The `Portal` contracts do **not** inherit from a bridge-specific contract. Instead, they hold an address of a contract that implements the generic `IBridge` interface. The `HyperlaneBridge.sol` contract is the concrete implementation that connects the Portals to the Hyperlane protocol. - **Decoupling:** This design decouples the token bridging logic from the cross-chain messaging logic. The Portals are only aware of the `IBridge` interface, making the system potentially adaptable to other messaging bridges in the future. - **Message Flow:** 1. A `Portal` contract calls `sendMessage()` on the configured `HyperlaneBridge`. 2. `HyperlaneBridge` formats the message and dispatches it through the Hyperlane `IMailbox` contract. 3. On the destination chain, the Hyperlane network delivers the message to the destination `HyperlaneBridge` by calling its `handle()` function. 4. The destination `HyperlaneBridge` authenticates the message (verifying it comes from the Mailbox and a known peer) and then calls `receiveMessage()` on its local `Portal` contract, delivering the payload. ## Developer Integration and Security - **Audits:** The M Portal contracts have undergone security audits. Refer to linked audit reports for findings and details. - **Upgradeability:** - **M Portal Lite:** Designed as an upgradeable system using the UUPS proxy pattern (via OpenZeppelin Upgrades). - **M Portal (Standard):** The contracts are also designed to be upgradeable. - **Security Dependencies:** - **M Portal (Wormhole):** Security relies significantly on the integrity of the Wormhole Guardians and the NTT framework contracts. - **M Portal Lite (Hyperlane):** Security relies on the Hyperlane protocol, including its configured Interchain Security Modules (ISMs) and the correctness of the Hyperlane Relayer network. - **Key Security Feature (Lite):** The `bridgedPrincipal` tracking in the `HubPortal` of M Portal Lite provides an additional layer of accounting security, ensuring that the Hub cannot be drained of more tokens than were originally locked for a given spoke chain. - **Code Repositories:** - M Portal (Wormhole): [github.com/m0-foundation/m-portal](https://github.com/m0-foundation/m-portal){rel=""nofollow""} - M Portal Lite (Hyperlane): [github.com/m0-foundation/m-portal-lite](https://github.com/m0-foundation/m-portal-lite){rel=""nofollow""} - **External Documentation:** - Wormhole NTT Documentation: [docs.wormhole.com/wormhole/ntt/overview](https://docs.wormhole.com/wormhole/ntt/overview){rel=""nofollow""} - Hyperlane Documentation: [docs.hyperlane.xyz](https://docs.hyperlane.xyz/){rel=""nofollow""} - **Audit Reports:** - M Portal (Wormhole): [github.com/m0-foundation/m-portal/tree/main/audits](https://github.com/m0-foundation/m-portal/tree/main/audits){rel=""nofollow""} - M Portal Lite (Hyperlane): [github.com/m0-foundation/m-portal-lite/tree/main/audits](https://github.com/m0-foundation/m-portal-lite/tree/main/audits){rel=""nofollow""} # M Token ## Overview *M is for Money.* ![M](https://docs.m0.org/images/technical-documentations/m/M.png){.doc-logo} `$M` is an immutable ERC20-compliant token at the heart of the M0 Ecosystem. It serves as the foundational building block for all M0 Extensions. Each unit is backed 1:1 by approved collateral. It implements an innovative dual-balance accounting system: 1. **Non-earning Balances**: Standard ERC20-style token balances that remain static over time - non-rebasing amounts do not increase without external transactions. 2. **Earning Balances**: Token balances that automatically increase in quantity over time through continuous compounding. Holders with earning balances receive additional tokens without requiring any transactions. This rebasing mechanism means users see their token count grow as interest accrues continuously, with their viewable balance updating in real-time (per block). This design separates standard token functionality from interest-bearing capabilities. The M0 Two-Token Governance (TTG) system determines which accounts are eligible to become Earners through an approval process recorded in the [`TTGRegistrar`](https://docs.m0.org/protocol/roles). `$M` interacts closely with several **key components** of the M0 Protocol: - **[`MinterGateway`](https://docs.m0.org/protocol/minting-burning)**: Controls the total supply through secure mint and burn operations. - **[`Rate Models`](https://docs.m0.org/protocol/rate-models)**: External smart contracts that dynamically calculate the interest rates for both Minters and Earners. - **[`TTGRegistrar`](https://docs.m0.org/protocol/roles)**: The governance-controlled parameter store that, among other things, maintains the list of approved Earners and the addresses of the active Rate Models. - **[`DistributionVault`](https://docs.m0.org/protocol/distribution-vault)**: Receives excess yield, which arises when the interest charged to Minters exceeds the yield distributed to Earners. ## Key Features and User Interactions - **Governance-Approved Yield Access**: Selected accounts can earn continuous yield after Two Token Governance (TTG) approval. - **Automatic Yield**: Earning balances grow continuously without requiring any user action. - **Transparency**: All interest calculations occur onchain with fully transparent mechanics. - **Efficiency**: A single token contract handles both stable (non-earning) and yield-generating (earning) use cases. - **Precision**: Advanced mathematical techniques are employed to ensure accurate interest calculations. - **Safety Mechanisms**: Protections are in place to ensure accounts removed from the `APPROVED_EARNERS` list by governance cannot continue earning. ### User Interactions #### Basic Token Operations `$M` implements all standard ERC20 functions and includes additional features: - It has **6 decimal places** of precision for all operations. - Supports standard transfers (`transfer`, `transferFrom`), approvals (`approve`), and balance inquiries (`balanceOf`). - Includes support for EIP-2612 `permit` for gasless approvals. - Includes support for EIP-3009 for transfers with authorization (`transferWithAuthorization`, `receiveWithAuthorization`). #### Earning Status Management Users can interact with the earning functionality through several key functions on the `MToken` contract: - `startEarning()`: Converts an account's regular (non-earning) balance to an earning balance. This function can only be called by an account for itself, and only if that account has been approved as an Earner by M0 Governance. - `stopEarning()`: Converts an account's earning balance back to a regular (non-earning) balance. The account retains all interest accrued up to that point. This can be called by the account owner. - `stopEarning(address account_)`: A safety function that allows anyone to stop the earning status for a specified `account_` if that account is no longer on the governance-approved `APPROVED_EARNERS` list. This ensures that accounts cannot continue to earn yield if their approval is revoked. #### Balance Querying Several methods are available to check different aspects of balances: - `balanceOf(address account)`: Returns the current balance of the account. For earning accounts, this includes all accrued interest up to the current block. - `principalBalanceOf(address account)`: Returns the underlying principal amount for earning accounts. For non-earning accounts, this will be 0. - `isEarning(address account)`: Checks if an account is currently in earning mode. - `totalEarningSupply()`: Returns the total amount of tokens (present value including accrued interest) currently held in earning balances. - `totalNonEarningSupply()`: Returns the total amount of tokens held in non-earning balances. The `totalSupply()` is the sum of `totalEarningSupply()` and `totalNonEarningSupply()`. ## Technical Mechanics ### Dual Balance System The dual balance system is one of the core innovations of the `$M` token, enabling it to function both as a standard stable token and a yield-bearing asset. #### Two Balance Types | Non-Earning | Earning (allowed by governance) | | ---------------------------------------------------- | ------------------------------------------------------------------------------- | | Function exactly like regular ERC20 tokens | Automatically increase in value over time through continuous compounding | | Value remains constant unless explicitly transferred | Stored internally as "principal amounts" that get multiplied by a growing index | | Stored as actual token amounts in the contract | Interest accrues without requiring any transactions or claim process | ### Balance Conversions in `$M` token The `MToken` contract implements a dual accounting system that allows users and developers to leverage both non-earning and earning states. Understanding how the state changes work is crucial. #### Non-earning to Earning Conversion Process When an approved user calls `startEarning()` to convert from non-earning to earning status: 1. The contract first checks if the user is approved as an earner in the M0 Governance system (via the `TTGRegistrar`). 2. The user's current token balance (let's call it `amount`) is read from storage. 3. This `amount` is converted to a smaller "principal amount" using the following calculation: [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1.9781em;vertical-align:-0.686em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[am]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.2921em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.686em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br where `currentIndex` started at 1.0 (represented as 1e12) and has been growing continuously based on the `EARNER_RATE_MODEL`. 4. The conversion rounds **down** in favor of the protocol (a tiny fraction may be left, contributing to protocol reserves). 5. The user's raw balance in storage (within the `_balances` mapping, specifically `MBalance.rawBalance`) is updated to this `principalAmount`. Their `MBalance.isEarning` flag is set to `true`. 6. Global accounting variables are updated: - `totalNonEarningSupply` is decreased by the original `amount`. - `principalOfTotalEarningSupply` is increased by the `principalAmount`. This `principalAmount` will continually grow in value as the `currentIndex` increases. The user doesn't see their token count change directly in the `rawBalance` storage, but when they check their balance through `balanceOf()`, the contract multiplies their `principalAmount` by the `currentIndex` to show their true balance including all earned interest. #### Earning to Non-earning Conversion Process When a user calls `stopEarning()` to convert from earning to non-earning status: 1. The contract reads the user's current `principalAmount` from storage. 2. This principal is converted back to a "present amount" (actual token value) by multiplying: [[[]{.katex-mathml}[[[]{.strut style="height:0.8778em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[ese]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[A]{.mord.mathnormal}[m]{.mord.mathnormal}[o]{.mord.mathnormal}[u]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} 3. The resulting `presentAmount` includes all interest earned up to that moment. The calculation of `presentAmount` from `principalAmount` is rounded **down** (`_getPresentAmountRoundedDown`). 4. The user's `rawBalance` in storage is updated to this `presentAmount`. Their `MBalance.isEarning` flag is set to `false`. 5. Global accounting variables are updated: - `totalNonEarningSupply` is increased by the `presentAmount`. - `principalOfTotalEarningSupply` is decreased by the `principalAmount`. After this conversion, the user's balance no longer earns interest but now includes all interest accrued up to the conversion point. Their balance will remain static until they perform another transaction or start earning again (if still approved). #### Example to Illustrate Consider a user with 1,000 `$M` tokens in non-earning state, and the `currentIndex` is 1.05 (representing a 5% increase since inception): 1. When they call `startEarning()`: - `principalAmount = 1000 / 1.05 ≈ 952.380952` (actual value depends on 6 decimals for `$M` and 12 decimals for index, then rounded down). Let's say it's `952.380952 * 10^6` as raw principal. - Their storage value for `rawBalance` becomes this principal. - `balanceOf()` returns `(principalAmount * 1.05 * 10^12) / 10^{12} ≈ 1000 * 10^6`. 2. After time passes and the `currentIndex`grows to 1.08: - Their stored `rawBalance` (principal) is still `952.380952 * 10^6`. - `balanceOf()` now returns `(principalAmount * 1.08 * 10^12) / 10^{12} ≈ 1028.571428 * 10^6`. 3. When they call `stopEarning()`: - `presentAmount = (principalAmount * 1.08 * 10^{12}) / 10^{12}` (rounded down). - Their storage value for `rawBalance` becomes this `presentAmount`. - `balanceOf()` now returns exactly this `presentAmount`. ### Transfer Mechanics Transfers in `MToken` handle accounts with different earning statuses: 1. **In-kind Transfers**: Between two accounts with the same earning status. - Between two earning accounts: The transfer involves principal amounts. The amount to be transferred is converted to its principal equivalent (rounded **up** if sender, effectively taking slightly more principal for the given token amount) and then adjusted in the sender's and receiver's principal balances. - Between two non-earning accounts: The transfer is a standard token amount transfer. 2. **Out-of-kind Transfers**: Between accounts with different earning statuses. - From earning to non-earning: The sender's earning balance (principal) is reduced (principal equivalent rounded **up**), and the receiver's non-earning balance (token amount) is increased by the specified token amount. - From non-earning to earning: The sender's non-earning balance (token amount) is reduced, and the receiver's earning balance is increased by the principal equivalent of the token amount (rounded **down**). ### Strategic Rounding The protocol employs consistent rounding rules that slightly favor the protocol to create a small buffer, enhancing system stability and protecting against potential exploitation. These small rounding differences accumulate as protocol reserves. | Operation | From | To | Rounding Rule for Amount Conversion to Principal | Protocol Favored | Internal Function Involved (Illustrative) | | :------------------- | :--------- | :--------- | :--------------------------------------------------------------------------------------------- | :--------------- | :------------------------------------------ | | **Conversions** | | | | | | | Start Earning | Non-Earner | Earner | Principal from present amount rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` | | Stop Earning | Earner | Non-Earner | Present amount from principal rounded **DOWN** (for the final balance calculation) | Yes | `_getPresentAmountRoundedDown` | | **Transfers** | | | | | | | Earner to Earner | Earner | Earner | Amount to principal for sender: rounded **UP**; for receiver: (principal transferred directly) | Sender: Yes | `_getPrincipalAmountRoundedUp` (sender) | | Earner to Non-Earner | Earner | Non-Earner | Amount to principal for sender: rounded **UP** | Yes | `_getPrincipalAmountRoundedUp` (sender) | | Non-Earner to Earner | Non-Earner | Earner | Amount to principal for receiver: rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` (receiver) | | **Mint/Burn** | | | | | | | Mint to Earner | - | Earner | Amount to principal rounded **DOWN** | Yes | `_getPrincipalAmountRoundedDown` | | Burn from Earner | Earner | - | Amount to principal rounded **UP** | Yes | `_getPrincipalAmountRoundedUp` | *(Note: The table simplifies complex interactions. Refer to the [`MToken.sol`](https://docs.m0.org/protocol/m-token) contract for precise implementation details, especially `_transferOutOfKind`, `_transferAmountInKind`, `_addEarningAmount`, `_subtractEarningAmount`, `_addNonEarningAmount`, `_subtractNonEarningAmount`.)* ### Global Index & Continuous Compounding At the heart of the `$M` token's interest mechanism for earning balances is a global index that efficiently tracks the growth of all earning balances through continuous compound interest. #### Index Mechanism - **Single Global Index**: A single shared growth factor (`latestIndex`) applies to all earning balances, making the system highly gas-efficient. - **Mathematical Relationship**: For an earning account, its `balanceOf()` is effectively `principalAmount * currentIndex`. - **Starting Point**: The index starts at 1.0 (represented as `1e12` internally due to 12 decimal places of precision for the index) and only increases over time. - **Index Storage**: The `MToken` contract inherits from `ContinuousIndexing`, which stores `latestIndex` (uint128) and `latestUpdateTimestamp` (uint40). #### Mathematical Implementation The `MToken` contract implements true continuous compounding: - **Continuous Compounding Formula**: The index is updated using the formula `newIndex = oldIndex * e^(rate * timeElapsed / SECONDS_PER_YEAR)`. - **Interest Accumulation**: Interest compounds with every second that passes, with the `currentIndex()` function calculating the up-to-date index on-demand. - **Exponential Approximation**: The `e^x` function is implemented onchain using a Pade approximant R(4,4) for gas efficiency and precision: [[[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[e]{.mord.mathnormal}[(]{.mopen}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.9043em;vertical-align:-1.1514em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-2.2115em;"}[[]{.pstrut style="height:3.0179em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.2479em;"}[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-3.7529em;"}]{.vlist style="height:1.7529em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.1514em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} - **Rate Conversion**: Rates obtained from the `EARNER_RATE_MODEL` (in basis points) are converted to a scaled format suitable for the exponential calculations. - **Time Scaling**: Rates are scaled by the time elapsed (in seconds) divided by `SECONDS_PER_YEAR` (31,536,000). #### Principal vs. Present Value The contract manages two key value concepts for earning balances: - **Principal Amount**: The base `rawBalance` (uint240, but effectively uint112 for principal part) stored for earning accounts. This is the amount that earns interest. - **Present Amount**: The current value of an earning balance, including all accrued interest, calculated as `principalAmount * currentIndex`. This is what `balanceOf()` returns. Conversion functions handle translations: - `_getPresentAmount(uint112 principalAmount_)` or `_getPresentAmountRoundedDown(uint112 principalAmount, uint128 index_)`: Multiplies principal by the current index. - `_getPrincipalAmountRoundedDown(uint240 presentAmount_)`: Divides present amount by the current index, rounded down. Used when adding to earning accounts. - `_getPrincipalAmountRoundedUp(uint240 presentAmount_)`: Divides present amount by the current index, rounded up. Used when subtracting from earning accounts. #### Index Updates The global earning index (`latestIndex`) in `MToken` is updated by calling `updateIndex()`. This function is typically called: - By the `MinterGateway` during its own `updateIndex()` calls (e.g., during `mintM`, `burnM`, `updateCollateral`, `deactivateMinter`). This ensures synchronization between Minter interest payments and Earner yield accrual. - Internally within `MToken` before operations that change an account's earning status or modify `principalOfTotalEarningSupply`, such as: - `_startEarning()` - `_stopEarning()` - Minting directly to an earning account via `_mint()`. - Burning directly from an earning account via `_burn()`. - Transfers that involve an earning account (`_transferOutOfKind`, `_transferAmountInKind` for earning-to-earning). Each `MToken.updateIndex()` call: 1. Fetches the current earner rate from the `EARNER_RATE_MODEL` (address obtained from `TTGRegistrar`). 2. Calculates the time elapsed since `latestUpdateTimestamp`. 3. Computes the new index: `newIndex = latestIndex * e^(rate * timeElapsed / SECONDS_PER_YEAR)`. 4. Updates `latestIndex` and `latestUpdateTimestamp` in storage. #### Precision and Efficiency - **Fixed-Point Arithmetic**: The index uses 12 decimal places of precision (scaled by `1e12`). `$M` balances have 6 decimals. - **Conservative Rounding**: All rounding strategies are designed to favor protocol safety, with small residuals contributing to protocol reserves. - **Gas Optimization**: The single global index is updated only when necessary and is shared across all earning accounts. - **Overflow Protection**: The index is capped at `type(uint128).max`. Principal amounts are also managed to prevent overflow in calculations. ## Security and Governance Interactions The security and behavior of the `$M` token are deeply intertwined with the M0 Protocol's governance and other core contracts. ### Supply Control - The total supply of `$M` is exclusively controlled by the `MinterGateway` contract. - `MToken.mint(address recipient, uint240 amount)` can only be called by the `minterGateway` address set in `MToken`. - `MToken.burn(address account, uint240 amount)` can only be called by the `minterGateway` address. This ensures that `$M` tokens are only created or destroyed as per the protocol's minting (collateral-backed) and burning (debt-repayment) rules managed by `MinterGateway`. ### Earning Mechanism Governance - **Earner Approval**: Whether an account can switch to an earning balance (`startEarning()`) is determined by M0 Governance. The `MToken` contract checks if an account is an approved earner by querying the `TTGRegistrar` (specifically, the `APPROVED_EARNERS` list or if `EARNERS_LIST_IGNORED` is true). ```solidity // Simplified check from _isApprovedEarner(address account_) function _isApprovedEarner(address account_) internal view returns (bool) { return ttgRegistrarReader.getBool("earners_list_ignored") || ttgRegistrarReader.listContains("earners", account_); } ``` - **Rate Model Control**: The interest rate for earners is determined by the `EARNER_RATE_MODEL`. The address of this model is a governance-controlled parameter stored in the `TTGRegistrar` and read by `MToken` during `updateIndex()`. ### Safety Controls for Earners - The `stopEarning(address account_)` function allows anyone to call it for an `account_` that is *no longer* on the `APPROVED_EARNERS` list. This is a crucial safety mechanism to prevent an account from continuing to accrue yield if its earner status is revoked by governance. ```solidity function stopEarning(address account_) external { if (_isApprovedEarner(account_)) revert IsApprovedEarner(); // Can only be called if account is NOT an approved earner _stopEarning(account_); } ``` ### Index Management and Synchronization - The `MToken.updateIndex()` function is critical for reflecting the correct yield. As mentioned, it's called by `MinterGateway` to ensure synchronization with Minter-side interest accruals and by `MToken` itself during critical state changes. This synchronization is vital for the protocol's economic balance. ### Rounding and Numerical Stability - The consistent use of protocol-favoring rounding rules in conversions and transfers contributes to the robustness of the `$M` token system. These small, accumulated amounts act as a buffer and eventually contribute to protocol reserves (e.g., claimable by the `DistributionVault`). - Explicit overflow checks and the use of libraries like `UIntMath` and `ContinuousIndexingMath` ensure numerical precision and prevent errors during balance and index calculations. ### Immutability The `MToken` contract itself is designed to be immutable, meaning its core logic cannot be upgraded directly. Changes to its behavior (like interest rates or earner eligibility) are managed externally through governance-controlled parameters in the `TTGRegistrar` and updatable Rate Model contracts. # M Token Specification ## Contract overview The `MToken` contract is an immutable ERC20-compliant token with a dual-balance accounting system. It implements continuous compounding for earning accounts via a global index mechanism. - **Decimals:** 6 - **Index precision:** 12 decimal places (scaled by `1e12`) - **Inherits:** `ContinuousIndexing`, ERC20, EIP-2612, EIP-3009 --- ## Key functions ### ERC20 Standard | Function | Description | | -------------------------------------------------------- | -------------------------------------------------------------- | | `transfer(address to, uint256 amount)` | Transfer tokens to an address | | `transferFrom(address from, address to, uint256 amount)` | Transfer tokens on behalf of another address | | `approve(address spender, uint256 amount)` | Approve a spender allowance | | `balanceOf(address account)` | Returns current balance including accrued interest for earners | | `totalSupply()` | Returns `totalEarningSupply() + totalNonEarningSupply()` | ### EIP-2612 Permit | Function | Description | | -------------------------------------------------------------------------------------------------------- | ------------------------------ | | `permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` | Gasless approval via signature | ### EIP-3009 Transfer with Authorization | Function | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)` | Transfer via signed authorization | | `receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)` | Receive via signed authorization | ### Earning Management | Function | Description | | ------------------------------- | ------------------------------------------------------------------ | | `startEarning()` | Convert caller's balance to earning mode (must be approved earner) | | `stopEarning()` | Convert caller's balance back to non-earning mode | | `stopEarning(address account_)` | Force-stop earning for an account no longer on the approved list | ### Balance Queries | Function | Returns | | ------------------------------------- | ------------------------------------------------------------- | | `balanceOf(address account)` | Current balance (including accrued interest for earners) | | `principalBalanceOf(address account)` | Underlying principal for earning accounts (0 for non-earners) | | `isEarning(address account)` | Whether the account is in earning mode | | `totalEarningSupply()` | Total tokens in earning balances (present value) | | `totalNonEarningSupply()` | Total tokens in non-earning balances | ### Index Management | Function | Description | | ----------------------- | ------------------------------------------------------------------------- | | `updateIndex()` | Update the global earning index; called by `MinterGateway` and internally | | `currentIndex()` | Returns the current index value (on-demand calculation) | | `latestIndex` | Last stored index value (uint128) | | `latestUpdateTimestamp` | Timestamp of last index update (uint40) | ### Supply Control (MinterGateway only) | Function | Description | | ----------------------------------------- | --------------------------- | | `mint(address recipient, uint240 amount)` | Mint tokens to an address | | `burn(address account, uint240 amount)` | Burn tokens from an address | --- ## Events | Event | Description | | ------------------------------------------------------------------------- | ---------------------------- | | `Transfer(address indexed from, address indexed to, uint256 value)` | Standard ERC20 transfer | | `Approval(address indexed owner, address indexed spender, uint256 value)` | Standard ERC20 approval | | `StartedEarning(address indexed account)` | Account entered earning mode | | `StoppedEarning(address indexed account)` | Account exited earning mode | --- ## Errors | Error | Condition | | ----------------------------------------------------------------------- | --------------------------------------------------------- | | `NotApprovedEarner()` | Account is not on the approved earners list | | `IsApprovedEarner()` | Attempted to force-stop an account that is still approved | | `NotMinterGateway()` | Caller is not the `MinterGateway` | | `InsufficientBalance(address account, uint256 balance, uint256 amount)` | Transfer or burn exceeds balance | --- ## Continuous compounding formula The global index grows via continuous compounding: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[[newIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[oldIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.938em;"}[[e]{.mord.mathnormal}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[rate]{.mord.mtight}]{.mord.text.mtight}[×]{.mbin.mtight}[[timeElapsed]{.mord.mtight}]{.mord.text.mtight}[/]{.mord.mtight}[[SECONDS\_PER\_YEAR]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.113em;margin-right:0.05em;"}]{.vlist style="height:0.938em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The `e^x` function uses a Padé approximant R(4,4): [[[]{.katex-mathml}[[[]{.strut style="height:1em;vertical-align:-0.25em;"}[e]{.mord.mathnormal}[(]{.mopen}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.9043em;vertical-align:-1.1514em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.786em;margin-right:0.0714em;"}]{.vlist style="height:0.7463em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.9164em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-2.2115em;"}[[]{.pstrut style="height:3.0179em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.2479em;"}[[]{.pstrut style="height:3.0179em;"}[[1]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[2]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:0.6954em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[28]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[3]{.mord.mtight}[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[2]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[84]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[3]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[[1680]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.655em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[[x]{.mord.mathnormal.mtight}[[[[[[]{.pstrut style="height:2.5em;"}[[4]{.mord.mtight}]{.sizing.reset-size3.size1.mtight}]{style="top:-2.931em;margin-right:0.0714em;"}]{.vlist style="height:0.8913em;"}]{.vlist-r}]{.vlist-t}]{.msupsub}]{.mord.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-3.394em;"}]{.vlist style="height:1.0179em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.345em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.mord}]{style="top:-3.7529em;"}]{.vlist style="height:1.7529em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.1514em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} --- ## Rounding rules | Operation | Rounding | Protocol favored | | ---------------------------------------- | -------- | ---------------- | | Start earning (present → principal) | Down | Yes | | Stop earning (principal → present) | Down | Yes | | Earner-to-earner transfer (sender) | Up | Yes | | Non-earner-to-earner transfer (receiver) | Down | Yes | | Mint to earner | Down | Yes | | Burn from earner | Up | Yes | --- ## Related - [M Token overview](https://docs.m0.org/protocol/m-token) - Full conceptual documentation - [Rate Models](https://docs.m0.org/protocol/rate-models) - How interest rates are determined - [Minting & Burning](https://docs.m0.org/protocol/minting-burning) - Supply control via `MinterGateway` # Minting & Burning (MinterGateway) ## Overview *Where money is born.* The `MinterGateway` contract is the central component within the M0 Protocol responsible for orchestrating the entire lifecycle of `$M`. It governs the processes for minting and burning, manages the offchain collateral system that backs the supply, and tracks the debt obligations of Minters. This contract acts as the primary interface for Minters and Validators, ensuring that issuance is always securely collateralized and transparently managed according to protocol rules defined by M0 Governance. Its key responsibilities include: - Managing Minter activation, deactivation, and operational states. - Overseeing the collateral update process, including verification of offchain assets by Validators. - Enforcing the structured minting and burning procedures. - Calculating and applying interest, debt, and penalties for Minters. - Facilitating the collateral retrieval process for Minters. - Synchronizing its interest index with the `MToken` contract to maintain system-wide consistency. ## Minting and Burning Process The creation (minting) and destruction (burning) of `$M` tokens are strictly controlled by the `MinterGateway`. ### Minting `$M` The minting process is designed with security checkpoints involving a proposal, a delay, and execution: 1. **Update Collateral (Prerequisite)**: Before proposing a mint, a Minter must have an up-to-date and sufficient verified collateral value on record. 2. **Propose Mint**: An active Minter initiates a mint request by calling `proposeMint()`: This creates a mint proposal with a unique `mintId_`. The proposal specifies the amount of `$M` to be minted and the recipient address. This step allows Validators to review pending mints. ```solidity function proposeMint(uint256 amount, address destination) external returns (uint48 mintId_); ``` 3. **Delay Period**: A mandatory delay period (`MINT_DELAY`), set by governance, must pass after a mint is proposed before it can be executed. This window allows Validators to intervene (e.g., cancel the mint or freeze the Minter) if they detect any issues. 4. **Execute Mint**: After the `MINT_DELAY` has elapsed, and before the proposal expires (within `MINT_TTL` - Time To Live, also set by governance), the Minter can execute the mint by calling `mintM()`: This function mints the specified amount of `$M` to the destination address, provided the proposal is still valid, hasn't been canceled, the Minter isn't frozen, and the mint doesn't violate collateralization requirements. ```solidity function mintM(uint256 mintId) external; ``` The `MINT_DELAY` and `MINT_TTL` mechanisms create a secure time window for oversight, preventing immediate, unreviewed mints and ensuring proposals don't remain executable indefinitely. ### Burning `$M` `$M` tokens can be burned to reduce a Minter's outstanding debt. Any `$M` holder can initiate a burn against a specific Minter's debt: ```solidity function burnM(address minter, uint256 amount) external; ``` - **For Active Minters**: Burning `$M` reduces the principal of their interest-accruing debt (`activeOwedM`). - **For Deactivated Minters**: Burning `$M` reduces their fixed `inactiveOwedM`. This serves as an informal liquidation mechanism, allowing the market to help settle the obligations of a Minter who has exited the system. The `_repayForDeactivatedMinter` internal function handles this logic. ## Collateral System `$M` is backed by offchain collateral, typically real-world assets like U.S. Treasury Bills held in Special Purpose Vehicles (SPVs). The `MinterGateway` manages the onchain representation and verification of this collateral. ### Offchain Backing & Onchain Representation Minters are responsible for maintaining sufficient eligible collateral offchain. The value of this collateral is reported onchain through the `MinterGateway`. ### Validator Verification Trusted, independent Validators play a crucial role in verifying the Minters' offchain collateral. They periodically assess the collateral and provide cryptographic signatures attesting to its value. ### Update Process Minters must regularly update their onchain collateral value to reflect the current state of their offchain reserves. This is done by calling the `updateCollateral` function: ```solidity function updateCollateral( uint256 collateral, uint256[] calldata retrievalIds, bytes32 metadataHash, address[] calldata validators, uint256[] calldata timestamps, bytes[] calldata signatures ) external returns (uint40 minTimestamp_); ``` Key aspects of the collateral update process: - **Frequency**: Minters must update their collateral within a governance-defined interval (e.g., `UPDATE_COLLATERAL_INTERVAL`, typically daily). Failure to do so results in their onchain collateral value being considered zero until a new valid update is provided. - **Validator Signatures**: A collateral update requires signatures from multiple approved Validators, meeting a minimum threshold set by governance (e.g., `UPDATE_COLLATERAL_THRESHOLD`). - Signatures must be provided with validator addresses in ascending order to prevent duplicates. - Each signature includes a timestamp which must be newer than the last signature from that specific validator for that Minter and must not be in the future. This prevents replay attacks. - Signatures use EIP-712 typed data specific to each validator. - **Collateral Expiration**: If a Minter fails to update their collateral within the `UPDATE_COLLATERAL_INTERVAL`, their effective onchain collateral value is treated as zero for all protocol calculations, potentially leading to penalties or an inability to mint. - **Collateralization Caps**: Minters must maintain a collateralization ratio above the governance-set `MINT_RATIO`. This ratio dictates the maximum amount of `$M` a Minter can have outstanding relative to their verified collateral. - **Earliest Update Timing**: A new collateral update's effective timestamp must be greater than: - The Minter's last update timestamp. - The timestamp of the Minter's latest proposed collateral retrieval. - `block.timestamp - UPDATE_COLLATERAL_INTERVAL` to ensure updates are reasonably current. ## Collateral Retrieval Minters can retrieve excess collateral from their backing SPV, provided the retrieval does not compromise their ability to back their outstanding `$M` debt. This process is managed onchain through the `MinterGateway` and requires Validator oversight. 1. **Propose Retrieval**: The Minter initiates a request to retrieve a specific amount of collateral by calling `proposeRetrieval()`: ```solidity function proposeRetrieval(uint256 collateral) external returns (uint48 retrievalId_); ``` :brThis creates a pending retrieval proposal with a unique, sequential `retrievalId_`. The proposed retrieval amount is tracked in the `_pendingCollateralRetrievals` mapping and is immediately deducted from the Minter's effective collateral value for all ongoing calculations, ensuring the Minter remains adequately collateralized. The system prevents proposals that would lead to undercollateralization. 2. **Validator Inclusion & Resolution**: For a pending retrieval to be finalized, it must be explicitly included and approved by Validators in a subsequent `updateCollateral` transaction. - When Validators sign off on a Minter's collateral update, their signature digest includes the list of `retrievalIds` they are approving for resolution. - During the `updateCollateral` call, the `MinterGateway` processes these approved `retrievalIds`. The corresponding amounts are then considered formally retrieved, and their values are no longer deducted from the Minter's available collateral. This multi-step process ensures that collateral can only be effectively retrieved with explicit, signed approval from Validators as part of a standard, verified collateral update, maintaining the integrity of `$M`'s backing. Multiple retrieval proposals can be resolved within a single `updateCollateral` transaction. ## Debt, Interest, and Penalties for Minters Minters incur debt for the `$M` tokens they mint and are charged interest on this debt. The `MinterGateway` manages this system. ### Debt and Interest - **Debt Accrual**: When a Minter mints `$M`, they incur an equivalent amount of debt. - **Continuous Indexing**: Minter debt, similar to `MToken` earning balances, is tracked using a continuous indexing system. The debt is stored as a principal amount that grows over time as interest accrues. - **Interest Rate Model**: The interest rate charged to Minters (`MINTER_RATE_MODEL`) is determined by an external smart contract, the address of which is set by M0 Governance and stored in the `TTGRegistrar`. - **`activeOwedM`**: This term represents the current total value of an active Minter's obligations, including all accrued interest. It is calculated by applying the current minter index to their principal debt. ### Total Owed `$M` Accounting The `MinterGateway` tracks various categories of Minter debt: - **Active Owed `$M`**: The sum of interest-accruing debt from all active Minters. - **Inactive Owed `$M`**: The sum of fixed, non-interest-accruing debt from all deactivated Minters. - **Total Owed `$M`**: The aggregate of `activeOwedM` and `inactiveOwedM`. - **Excess Owed `$M`**: The difference between `totalOwedM` (interest generated from Minters) and the total supply of `$M` (which grows at the earner rate). This excess, if positive, represents protocol revenue and is minted directly to the [DistributionVault](https://docs.m0.org/protocol/distribution-vault) (TTGVault) during index updates. ### Penalties The protocol imposes penalties on Minters for failing to adhere to specific operational requirements, ensuring system discipline: 1. **Missed Collateral Updates Penalty**: If a Minter fails to update their collateral within the `UPDATE_COLLATERAL_INTERVAL`, a penalty is applied. - Calculation: `PENALTY_RATE * principalOfActiveOwedM * missedIntervals`. - This penalty is charged once per missed interval. 2. **Undercollateralization Penalty**: If a Minter's `activeOwedM` exceeds the maximum allowed by their verified collateral and the `MINT_RATIO`, they are penalized. - Calculation: `PENALTY_RATE * principalOfExcessOwedM * (timeSpan / UPDATE_COLLATERAL_INTERVAL)`. - This penalty is proportional to the amount and duration of the undercollateralization. Penalties are added to the Minter's debt. ## Minter Lifecycle and Management Minters are permissioned entities that interact with the `MinterGateway` to issue and manage `$M`. Their lifecycle within the protocol is characterized by several distinct statuses: 1. **Approved**: Minters are initially whitelisted by the M0 Two-Token Governance (TTG) system. At this stage, they are recognized by the protocol but cannot yet perform minting operations. 2. **Active**: An approved Minter must explicitly activate their status by calling `activateMinter()` on the `MinterGateway`. ```solidity function activateMinter(address minter_) external; ``` This function can only be called by the Minter themselves and transitions them to an operational state, allowing them to propose collateral updates and mint `$M`. The active status is stored locally within `MinterGateway` for gas efficiency. 3. **Frozen**: An active Minter can be temporarily restricted from minting `$M` by Validators (via `freezeMinter()`). During this state, the Minter can still burn `$M` to reduce their debt and update their collateral but cannot propose new mints. The freeze duration is determined by governance. 4. **Deactivated**: A Minter can be deactivated if they are removed from the governance-approved list. Deactivation is a permanent state, initiated by a call to `deactivateMinter()`. ```solidity function deactivateMinter(address minter_) external returns (uint240 inactiveOwedM_); ``` This function can be called if the Minter is no longer approved by TTG. Upon deactivation: - The Minter's outstanding debt, including accrued interest, is converted into a fixed `inactiveOwedM` amount which no longer accrues interest. - The Minter can no longer mint `$M` or propose collateral retrievals. - Their collateral remains to back their `inactiveOwedM`. - A deactivated Minter cannot be reactivated. This permanence optimizes gas by eliminating further status checks against the `TTGRegistrar`. ## Validator Roles and Security Controls Validators are independent, trusted entities that form a critical security layer for the minting and burning processes within the `MinterGateway`. Their responsibilities and powers include: ### Core Responsibilities - **Collateral Verification**: Validators verify Minters' offchain collateral and provide cryptographic signatures for `updateCollateral` transactions. These signatures attest to the value and existence of the assets backing `$M`. - **Timestamping**: Signatures from Validators must include a secure timestamp to prevent replay attacks and ensure the freshness of collateral data. - **Enabling Offchain-Onchain Bridge**: Validators act as the crucial link ensuring the onchain representation of collateral accurately reflects the state of offchain assets. ### Validator Signature Verification The signature verification system includes several security features: - **Multi-signature Requirement**: Requires multiple validator signatures for collateral updates. - **Ordered Validator Addresses**: Validator addresses must be provided in ascending order to prevent duplicates. - **Timestamp Validation**: Each signature includes a timestamp that must be: - Newer than the last signature from that validator for that minter. - Not in the future. - Not zero. - **Replay Protection**: The system tracks the last used timestamp for each validator-minter pair. - **Signature Format**: Uses EIP-712 typed data signatures that are validator-specific. ### Security Controls & Emergency Powers Validators are empowered with specific functions to intervene and protect the protocol: 1. **Cancel Mint Proposal**: If Validators deem a pending mint proposal to be suspicious, invalid, or potentially harmful to the protocol, they can cancel it before execution: ```solidity function cancelMint(address minter, uint256 mintId) external onlyApprovedValidator; ``` :brThis action deletes the mint proposal, preventing the Minter from executing it. 2. **Freeze Minter**: Validators can temporarily suspend a Minter's ability to mint new `$M` tokens: ```solidity function freezeMinter(address minter) external onlyApprovedValidator returns (uint40 frozenUntil_); ``` :brThis "freezes" the Minter for a duration specified by the governance parameter `MINTER_FREEZE_TIME`. A frozen Minter can still burn `$M` and update collateral but cannot propose or execute new mints until the freeze period expires. These controls allow Validators to act as a rapid response mechanism against potential threats or misbehavior related to `$M` issuance. ## Timing Constraints The system enforces several timing-related constraints: - **Collateral Update Frequency**: Minters must update at least once per `UPDATE_COLLATERAL_INTERVAL`. - **Mint Delay**: Proposals must wait `MINT_DELAY` seconds before execution. - **Mint TTL**: Proposals expire after `MINT_TTL` seconds. - **Earliest Collateral Update**: The timestamp for a new update must be greater than: - The last update timestamp. - The latest proposed retrieval timestamp. - `block.timestamp - UPDATE_COLLATERAL_INTERVAL`. ## Security Mechanisms The MinterGateway implements several safety features: - **Mint Delay & TTL (Time-To-Live)**: Creates a time window for validators to review and potentially cancel mints. - **Collateralization Caps**: Enforces maximum mint-to-collateral ratios (set by governance). - **Multi-signature Requirement**: Requires multiple validator signatures for collateral updates. - **Strictly Ordered Validations**: Validator addresses must be provided in ascending order. - **Timestamp Verification**: Prevents reuse of signatures and ensures freshness of collateral data. - **Excess Yield Distribution**: Any yield generated by minters beyond what's distributed to earners goes to the TTG Vault (also called the [DistributionVault](https://docs.m0.org/protocol/distribution-vault)). ## Emergency Measures For extreme circumstances, the protocol includes additional safeguards: ### Mint Proposal Cancellation Validators can cancel suspicious mint proposals before they're executed, allowing them to quickly intervene if they detect potentially harmful minting activity. ```solidity function cancelMint(address minter_, uint256 mintId_) external onlyApprovedValidator { uint48 id_ = _mintProposals[minter_].id; if (id_ != mintId_ || id_ == 0) revert InvalidMintProposal(); delete _mintProposals[minter_]; emit MintCanceled(id_, minter_, msg.sender); } ``` ### Validator Freeze Validators can freeze minters to stop them from minting tokens for a governance-defined period: ```solidity function freezeMinter(address minter_) external onlyApprovedValidator returns (uint40 frozenUntil_) { unchecked { _minterStates[minter_].frozenUntilTimestamp = frozenUntil_ = uint40(block.timestamp) + minterFreezeTime(); } emit MinterFrozen(minter_, frozenUntil_); } ``` ### Minter Deactivation Governance can completely deactivate minters who have been removed from the approved list: ```solidity function deactivateMinter(address minter_) external onlyActiveMinter(minter_) returns (uint240 inactiveOwedM_) { if (isMinterApproved(minter_)) revert StillApprovedMinter(); // Implementation details for deactivation process } ``` Once deactivated, a minter cannot be reactivated, providing a permanent resolution for problematic minters. ### Open Redemption Anyone can burn `$M` tokens to repay a deactivated minter's debt: ```solidity function _repayForDeactivatedMinter(address minter_, uint240 maxAmount_) internal returns (uint240 amount_) { amount_ = UIntMath.min240(inactiveOwedMOf(minter_), maxAmount_); unchecked { _rawOwedM[minter_] -= amount_; totalInactiveOwedM -= amount_; } } ``` This creates an informal liquidation mechanism for deactivated minters. ## Index Synchronization with `$M` Token The `MinterGateway` and the `MToken` contract both utilize a continuous indexing mechanism to track the accrual of interest -- for Minter debt in `MinterGateway` and for earner balances in `MToken`. It is crucial that these indices are synchronized. ### Synchronization Mechanism The `MinterGateway` ensures that whenever its own interest index for Minter obligations is updated, the `MToken`'s earning index is also updated simultaneously. This is achieved within the `MinterGateway.updateIndex()` function: ```solidity function updateIndex() public override(IContinuousIndexing, ContinuousIndexing) returns (uint128 index_) { uint240 excessOwedM_ = excessOwedM(); if (excessOwedM_ > 0) IMToken(mToken).mint(ttgVault, excessOwedM_); // Mint excess to TTG Vault // First, update the MinterGateway's own index and rate (for minter debt) index_ = super.updateIndex(); // Then, explicitly trigger an update of the MToken's index and rate (for earner yield) IMToken(mToken).updateIndex(); // Emit an event if this MinterGateway instance is also the MToken's MinterGateway // (This is relevant if MToken directly calls this contract for its own updates) if (address(this) == IMToken(mToken).minterGateway()) { emit MTokenIndexUpdated(IMToken(mToken).currentIndex(), IMToken(mToken).currentRate()); } } ``` This synchronized update ensures that the calculation of interest paid by Minters and yield distributed to Earners remains consistent and balanced across the protocol, reflecting the latest rates and system state. ### Triggers for Index Updates The `updateIndex()` function in `MinterGateway` (which in turn calls `MToken.updateIndex()`) is invoked during several key operations, ensuring indices are frequently updated: - When a Minter updates their collateral via `updateCollateral()`. - When `$M` tokens are minted via `mintM()`. - When `$M` tokens are burned via `burnM()`. - When a Minter is deactivated via `deactivateMinter()`. - When `MinterGateway.updateIndex()` is explicitly called by an external party. This frequent synchronization is vital for the accurate accounting of debts, yields, and the overall economic stability of the M0 Protocol. # Portal V2 Portal V2 is M0's cross-chain infrastructure for bridging tokens and propagating protocol metadata across multiple blockchains. It replaces the previous dual-implementation model (Portal Standard + Portal Lite) with a unified, modular architecture that supports pluggable bridge adapters. ## Overview Portal V2 serves two core functions in the M0 ecosystem: 1. **Token Bridging** - Enables seamless transfer of M0 extension tokens between L1 chains like Ethereum, Solana, and BNB, and supported L2 chains like Arbitrum, Base, and others 2. **Metadata Propagation** - Synchronizes critical protocol data (earning index, governance registrar values, earner lists) from L1 to all connected L2s. Also supports notification messages when a cross-chain order is filled or cancelled ### Why V2? The previous portal system offered two separate implementations: **Portal Standard** (built on Wormhole NTT) and **Portal Lite** (built on Hyperlane). Each was tightly coupled to its underlying bridge provider. Portal V2 introduces a **bridge adapter abstraction layer** that decouples the portal logic from any specific messaging protocol. This enables: - **Multiple bridge providers** - Both Hyperlane and Wormhole are supported simultaneously, with the ability to add more - **Per-chain bridge selection** - Each destination chain can have a different default bridge adapter - **Unified codebase** - A single Portal implementation replaces two separate codebases - **Configurable cross-spoke transfers** - Spoke-to-spoke transfers can be enabled on a per-chain basis (isolated by default) For background on the previous portal architecture, see [M Portals (V1)](https://docs.m0.org/protocol/m-portals). ## Architecture Portal V2 follows a **hub-and-spoke model** with Ethereum as the hub chain. ### Hub Chain (Ethereum) The `HubPortal` contract is deployed on Ethereum - the single source of truth for native issuance and governance. It: - **Locks** tokens when bridging to spoke chains - **Releases** locked tokens when bridged back - **Tracks bridged principal** per spoke chain (for isolated spokes) to ensure it never releases more than was locked - **Broadcasts** the earning index, registrar key/value pairs, and earner list updates to spoke chains - **Controls earning** - can enable/disable earning for the portal, affecting which index is propagated ### Spoke Chains (L1s & L2s) `SpokePortal` contracts are deployed on each supported chain (Optimism, Arbitrum, Base, Solana, and others). They: - **Mint** token representations when tokens are bridged from the hub - **Burn** representations when tokens are bridged back to the hub - **Apply** index and registrar updates received from the hub - **Optionally support** cross-spoke transfers when enabled by the hub ### Bridge Adapter Layer Bridge adapters are modular contracts that handle the actual cross-chain message delivery. Each adapter: - Implements a standard `IBridgeAdapter` interface - Maps internal chain IDs to provider-specific chain IDs - Handles quoting, sending, and receiving messages Current adapters: | Adapter | Protocol | Delivery Model | | ------------------------ | --------- | ------------------------------------------------ | | `HyperlaneBridgeAdapter` | Hyperlane | Mailbox dispatch with ISM validation | | `WormholeBridgeAdapter` | Wormhole | Core Bridge publish with Executor-based delivery | | `LayerZeroBridgeAdapter` | LayerZero | Endpoint dispatch with DVN validation | Operators can configure a **default adapter** per destination chain, and users can optionally specify an explicit adapter when sending tokens. ## Token Transfer Flow ### Hub to Spoke 1. User calls `sendToken()` on the `HubPortal`, specifying amount, destination chain, and recipient 2. If sending an extension token, the portal unwraps it first 3. Tokens are **locked** in the `HubPortal`; bridged principal is incremented for the destination spoke 4. A `TokenTransfer` payload is encoded with the amount, current index, and recipient 5. The payload is sent via the selected bridge adapter 6. On the spoke chain, the bridge adapter delivers the message to the `SpokePortal` 7. The `SpokePortal` mints tokens to the recipient (wrapping to an extension if specified) ### Spoke to Hub 1. User calls `sendToken()` on the `SpokePortal` 2. Tokens are **burned** on the spoke chain 3. Message is sent back to the Hub via the bridge adapter 4. The `HubPortal` verifies that the release amount does not exceed the bridged principal for that spoke 5. Tokens are **released** (unlocked) and delivered to the recipient on Ethereum ### Spoke to Spoke Cross-spoke transfers are **disabled by default**. Each spoke starts in isolated mode, where it can only communicate with the hub. To enable spoke-to-spoke transfers: 1. The hub operator calls `enableCrossSpokeTokenTransfer()` on the `HubPortal` for the spoke chain 2. The spoke operator calls `enableCrossSpokeTokenTransfer()` on the `SpokePortal` for each peer spoke Once enabled, tokens are burned on the source spoke and minted on the destination spoke, with a message relayed through the bridge adapter. ::note Once cross-spoke transfers are enabled for a spoke, the hub stops tracking per-spoke bridged principal for that chain, since tokens can now flow freely between spokes. :: ## Metadata Propagation Beyond token transfers, Portal V2 propagates critical protocol state from Ethereum to all spoke chains. ### Earning Index The `HubPortal` broadcasts the current earning index to spoke chains via `sendMTokenIndex()`. Spoke chains use this index to correctly calculate yield accrual. - When **earning is enabled** on the `HubPortal`, the live index from the token contract is used - When **earning is disabled**, a frozen index (captured at disable time) is propagated instead - Earning can only be enabled once - after being disabled, it cannot be re-enabled ### Registrar Data Governance parameters from M0's [TTG (Two Token Governance)](https://docs.m0.org/protocol/roles) system are propagated to spoke chains: - **Key-value pairs** via `sendRegistrarKey()` - configuration values that govern protocol behavior - **List membership** via `sendRegistrarListStatus()` - whether accounts are on specific governance lists (e.g., approved earners) ### Earner Merkle Roots (SVM) For Solana spoke chains, the `HubPortal` can send earner Merkle roots via `sendEarnersMerkleRoot()`, enabling Solana-based holders to prove their earner status. ## OrderBook Integration Portal V2 integrates with M0's [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) to relay cross-chain fill and cancel reports: - **Fill Reports** - When a solver fills an order on a destination chain, the `OrderBook` calls `sendFillReport()` on the portal to relay the fill details back to the origin chain - **Cancel Reports** - When an order is cancelled on the destination chain, `sendCancelReport()` relays the cancellation back to the origin chain for refund processing These functions are restricted to the `OrderBook` contract address. ## Message Types All cross-chain messages share a common header and are differentiated by payload type: | Type | Name | Direction | Purpose | | ---- | ------------------ | -------------------------- | ------------------------------------ | | 0 | `TokenTransfer` | Hub ↔ Spoke, Spoke ↔ Spoke | Bridge tokens between chains | | 1 | `Index` | Hub → Spoke | Broadcast earning index update | | 2 | `RegistrarKey` | Hub → Spoke | Propagate governance key-value pair | | 3 | `RegistrarList` | Hub → Spoke | Propagate governance list membership | | 4 | `FillReport` | Hub ↔ Spoke, Spoke ↔ Spoke | Relay OrderBook fill details | | 5 | `EarnerMerkleRoot` | Hub → Spoke (SVM) | Broadcast SVM earners merkle root | | 6 | `CancelReport` | Hub ↔ Spoke, Spoke ↔ Spoke | Relay OrderBook cancel details | ## Roles and Permissions Portal V2 uses role-based access control: | Role | Capabilities | | -------------------------------- | ---------------------------------------------------------------------------- | | **Admin** (`DEFAULT_ADMIN_ROLE`) | Authorize contract upgrades | | **Pauser** (`PAUSER_ROLE`) | Pause/unpause send and receive operations independently | | **Operator** (`OPERATOR_ROLE`) | Configure bridge adapters, bridging paths, gas limits, cross-spoke transfers | Additionally: - Only the **OrderBook** contract can call `sendFillReport()` and `sendCancelReport()` - Only registered **bridge adapters** can call `receiveMessage()` ## Security Features - **Reentrancy protection** - Uses transient storage-based locking to prevent recursive calls - **Message deduplication** - Each message has a unique ID; processed messages are tracked to prevent replay - **Independent pause controls** - Send and receive can be paused separately, allowing partial operation during incidents - **Per-spoke principal tracking** - For isolated spokes, the `HubPortal` ensures it never releases more tokens than were locked for that specific spoke - **Earning freeze** - The hub can freeze the earning index to manage token supply during transitions - **Wrap failure fallback** - If wrapping to an extension fails on the destination, the recipient receives the underlying token instead of the transaction reverting ## Migration from V1 Portal V2 includes migration logic for transitioning from the previous portal system: 1. Storage from V1 is cleaned using dedicated `StorageCleaner` contracts 2. The proxy is upgraded to the V2 implementation 3. `migrateBridgedPrincipal()` is called for each existing spoke to set the correct locked amounts 4. `completeMigration()` finalizes the process, disabling further migration operations ## Related - [Accessing Liquidity](https://docs.m0.org/build/accessing-liquidity) - Overview of M0's liquidity infrastructure - [Limit Order Protocol](https://docs.m0.org/protocol/limit-order-protocol) - Onchain settlement layer for cross-chain swaps - [M Portals (V1)](https://docs.m0.org/protocol/m-portals) - Previous portal architecture reference - [Cross-Chain](https://docs.m0.org/build/cross-chain) - Guide to deploying and bridging stablecoins across networks - [Source Code](https://github.com/m0-foundation/m-portal-v2){rel=""nofollow""} - Smart contract repository # Rate Models & Yield ## The Interest Mechanism `$M` token operates within a balanced interest system in the M0 Protocol: 1. **Minter Interest Accrual**: Minters accrue obligations at the minter rate on their outstanding `$M` balances. 2. **Earner Interest Accrual**: Simultaneously, approved earning accounts accrue additional tokens at the earner rate. 3. **Rate Synchronization**: The EarnerRateModel ensures the earner rate is calibrated so total earnings never exceed total minter obligations (see Rate Model section below for further details). 4. **Continuous Mechanism**: Both sides update through independent but synchronized continuous indexing systems. This creates a mathematically balanced system where earner yields are safely constrained by the protocol's income from minters. ![Virtual Flow](https://docs.m0.org/images/technical-documentations/m/virtual-flow.png) `$M` token implements sophisticated interest rate models that ensure financial soundness while distributing yield between minters, earners, and the protocol. ## Rate Model Architecture The protocol uses two primary rate models: 1. **MinterRateModel**: Determines the interest rate minters pay on borrowed `$M`token - Simple model that reads rates directly from the TTG Registrar - Capped at 400% APR (40,000 basis points) for system safety - Governance-controlled through the `base_minter_rate` parameter 2. **EarnerRateModel**: Calculates the interest rate paid to `$M`token holders who opt in to earning - Uses a mathematical formula that ensures financial soundness - Rates are capped at 98% of the calculated safe rate - Governed through the `max_earner_rate` parameter ### Minter Rate Calculation The `MinterRateModel` has a straightforward implementation: [[[]{.katex-mathml}[[[]{.strut style="height:0.6833em;"}[min]{.mord.mathnormal}[t]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[min]{.mord.mathnormal}[(]{.mopen}[[ TTGRegistrar(]{.mord}[[[BASE-MINTER-RATE]{.mord}]{.mord.text}]{.mord}[)]{.mord}]{.mord.text}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[[MAX-MINTER-RATE]{.mord}]{.mord.text}[)]{.mclose}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} ```solidity function rate() external view returns (uint256) { return min(TTGRegistrar.get("base_minter_rate"), MAX_MINTER_RATE); } ``` - The rate is governance-controlled through the TTG Registrar - Has a hard cap of 400% APR to prevent system abuse - Changes to this rate affect all minters uniformly ### Earner Rate Calculation The EarnerRateModel employs a more complex approach to ensure system solvency: [[[]{.katex-mathml}[[[]{.strut style="height:0.6833em;"}[e]{.mord.mathnormal}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[min]{.mord.mathnormal}[(]{.mopen}[ma]{.mord.mathnormal}[x]{.mord.mathnormal}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[,]{.mpunct}[]{.mspace style="margin-right:0.1667em;"}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[e]{.mord.mathnormal}[tE]{.mord.mathnormal style="margin-right:0.0576em;"}[x]{.mord.mathnormal}[t]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[)]{.mclose}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}[[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[g]{.mord.mathnormal style="margin-right:0.0359em;"}[e]{.mord.mathnormal}[tE]{.mord.mathnormal style="margin-right:0.0576em;"}[x]{.mord.mathnormal}[t]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[a]{.mord.mathnormal}[S]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[s]{.mord.mathnormal}[a]{.mord.mathnormal}[f]{.mord.mathnormal style="margin-right:0.1076em;"}[e]{.mord.mathnormal}[E]{.mord.mathnormal style="margin-right:0.0576em;"}[a]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[n]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[R]{.mord.mathnormal style="margin-right:0.0077em;"}[a]{.mord.mathnormal}[t]{.mord.mathnormal}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[u]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[t]{.mord.mathnormal}[i]{.mord.mathnormal}[pl]{.mord.mathnormal style="margin-right:0.0197em;"}[i]{.mord.mathnormal}[er]{.mord.mathnormal style="margin-right:0.0278em;"}]{.mord}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} ```solidity function rate() external view returns (uint256) { return min( maxRate(), getExtraSafeEarnerRate( totalActiveOwedM, totalEarningSupply, minterRate ) ); } ``` Where: - `getExtraSafeEarnerRate` applies the rate multiplier to the safe earner rate - `RATE_MULTIPLIER` is 9,800 (98% in basis points) - `ONE` is 10,000 (100% in basis points) - `maxRate()` returns the governance-set maximum earner rate #### Safe Rate Derivation The safe earner rate is calculated using two different approaches based on the relationship between total active owed $M and total earning supply: 1. **When totalActiveOwedM ≤ totalEarningSupply**: - Uses an instantaneous cashflow approach - `safeRate = totalActiveOwedM * minterRate / totalEarningSupply` - Ensures interest paid to earners never exceeds interest collected from minters 2. **When totalActiveOwedM > totalEarningSupply**: - Uses a time-integrated approach over a 30-day confidence interval - Accounts for compound interest effects - Applies complex mathematical formula to ensure long-term solvency - Allows earner rate to temporarily exceed minter rate while maintaining system safety #### Extra Safety Factor The EarnerRateModel applies an additional safety factor to the calculated safe rate: ```solidity function getExtraSafeEarnerRate( uint240 totalActiveOwedM_, uint240 totalEarningSupply_, uint32 minterRate_ ) public pure returns (uint32) { uint256 safeEarnerRate_ = getSafeEarnerRate(totalActiveOwedM_, totalEarningSupply_, minterRate_); uint256 extraSafeEarnerRate_ = (safeEarnerRate_ * RATE_MULTIPLIER) / ONE; return (extraSafeEarnerRate_ > type(uint32).max) ? type(uint32).max : uint32(extraSafeEarnerRate_); } ``` This ensures that earners receive 98% of the theoretically safe rate, creating an additional buffer for protocol safety. ## Confidence Interval Approach The EarnerRateModel uses a 30-day confidence interval to determine safe rates: - Calculates the amount of interest minters will generate over 30 days - Derives maximum earner rate that ensures total interest paid doesn't exceed this amount - Accounts for the compounding effects of continuous interest - Assumes the `updateIndex()` function will be called at least once every 30 days ## Balance Between Minters and Earners The system maintains a mathematical relationship between interest flows: ```text totalInterestFromMinters = totalActiveOwedM * (e^(minterRate*dt) - 1) totalInterestToEarners = totalEarningSupply * (e^(earnerRate*dt) - 1) ``` The rate models ensure: `totalInterestToEarners ≤ 98% of totalInterestFromMinters` ## Edge Cases Handling The EarnerRateModel handles several edge cases with special logic: - When `totalActiveOwedM = 0` or `minterRate = 0`: The earner rate is set to 0 - When `totalEarningSupply = 0`: The earner rate is set to the maximum possible value (`type(uint32).max`) - When calculations might overflow: The implementation includes safeguards against numerical issues ## Yield Distribution The system distributes yield in the following manner: 1. **Minters pay interest** based on their outstanding debt at the minter rate 2. **Earners receive up to 98%** of the available interest (subject to constraints) 3. **Excess yield** (at least 2% plus any additional buffer) goes to the TTG Vault 4. The TTG Vault funds protocol operations and can distribute to governance token holders ## Rate Update Mechanism Interest rates don't change automatically when governance parameters are updated: 1. Rate changes in the TTG Registrar take effect only when `updateIndex()` is called 2. Both `$M` token and MinterGateway must update their respective indices 3. Updates occur automatically during key operations such as minting, burning, and transfers 4. Rate models recalculate rates during each update based on current system state ## Mathematical Precision The implementation includes several technical considerations: - Rates are stored in basis points (1/100 of a percent) for precision - Calculations use fixed-point math with 12 decimal places (1e12 scaling) - Exponential calculations use Pade approximants for gas-efficient computation - Conservative rounding strategies favor protocol safety ## Visuals ![Safe Earner Rate](https://docs.m0.org/images/technical-documentations/rates/rates.png) # Roles ## Introduction The M0 Protocol operates through a carefully designed system of actors, each with distinct roles and responsibilities crucial for the protocol's operation, security, and decentralized governance. Understanding these roles is key to understanding how `$M` is created, secured, utilized, and how the protocol evolves. The protocol's core functionality is implemented in several key smart contracts that these roles interact with: - **`MToken`**: The ERC20-compliant token contract for `$M`, which includes the unique earning capabilities. - **`MinterGateway`**: The interface for minting and burning, managing Minter collateral, and tracking debt obligations. - **`TTGRegistrar`**: The central configuration store managed by Two-Token Governance (TTG), holding approved addresses for roles and key protocol parameters. Below is a high-level overview of the primary roles within the M0 ecosystem, categorized by their main area of interaction. ## Overview of Key Protocol Roles The M0 ecosystem comprises several key actors, each playing a vital part: **1. `$M` Supply & Security:** - **Minters**: - **Description**: Minters are permissioned institutions responsible for creating (minting) new `$M`. They are the primary on/off-ramp to and from fiat. - **Function**: They provide eligible offchain collateral (such as U.S. backed reserves held in Special Purpose Vehicles) and register its value onchain via the MinterGateway. Based on this collateral and governance-set ratios, they can mint. They also pay interest on their minted supply, which forms the basis for the yield distributed to Earners and the protocol. - **Validators**: - **Description**: Validators are independent, trusted entities acting as a crucial security and verification layer for the protocol. - **Function**: Their primary role is to verify the offchain collateral reported by Minters, ensuring that all supply remains fully and transparently backed. They provide cryptographic attestations (signatures) for Minters' collateral updates to the MinterGateway. Validators also possess emergency powers, such as freezing a Minter or canceling a suspicious mint proposal, to safeguard protocol integrity. **2. `$M` Usage & Yield:** To earn yield, a user has to get the earner status. Below is an overview of the Earner status: - **Description**: Earners are addresses (users, smart contracts, or M0 Extensions) that have been approved by M0 Governance (or delegated admins for certain extensions like wM) to accrue yield on their `$M` balances. - **Function**: Once approved and activated via MToken.startEarning(), their balance (or the balance of an M0-powered extension they hold) automatically increases over time due to continuous compounding, based on the prevailing earner rate. This mechanism allows `$M` to function as a yield-bearing component for authorized participants. **3. Protocol Governance:** - **Power Token (POWER) Holders**: - **Description**: POWER Holders are the primary participants in the operational governance of the M0 Protocol. - **Function**: POWER tokens are used to vote on proposals in the `StandardGovernor` (e.g., approving `Minters`, `Validators`, `Earners`; setting operational parameters like interest rates or collateral ratios) and the `EmergencyGovernor` (for urgent protocol adjustments). Active and complete participation in `StandardGovernor` voting is incentivized through POWER token inflation and ZERO token rewards. - **Zero Token (ZERO) Holders**: - **Description**: ZERO Holders possess meta-governance authority, effectively acting as the highest level of governance oversight for the protocol. - **Function**: ZERO holders are entitled to claim a pro-rata share of protocol revenues (e.g., excess Minter yield, Power token auction proceeds) accumulated in the DistributionVault. Additionally, ZERO tokens are used to vote in the ZeroGovernor on fundamental changes, such as upgrading the core governance contracts themselves, altering critical system parameters (like governance thresholds), or changing the accepted CashTokens for proposal fees. ![Roles](https://docs.m0.org/images/technical-documentations/roles/roles.png) ## Minters ### Role Definition Minters are entities authorized to generate `$M` by securing Eligible Collateral in offchain Special Purpose Vehicles (SPVs) and maintaining an onchain Collateral Value that represents these offchain assets. ### Key Functions Minters interact with the protocol primarily through the `MinterGateway` contract: ### 1. Activation and Status Management - `activateMinter(address minter_)`: A newly approved (by governance) minter must be activated before they can mint. This explicit step stores the active status locally within `MinterGateway` for gas efficiency, avoiding repeated external checks. - Once deactivated, a minter cannot be reactivated. This permanence also optimizes gas by eliminating the need for further status checks against the external `TTGRegistrar`. ### 2. Collateral Management - `updateCollateral(collateral, retrievalIds, metadataHash, validators, timestamps, signatures)`: Updates collateral value with validator signatures - `proposeRetrieval(collateral)`: Proposes to retrieve excess collateral - Must maintain sufficient collateral to support minted `$M` tokens according to the governance-set mint ratio ### 3. Minting Process - `proposeMint(amount, destination)`: Proposes to mint a specific amount of tokens - `mintM(mintId)`: Executes a previously proposed mint after the mint delay period and before mint time-to-live period ### Lifecycle and Constraints Minters go through several states: 1. **Inactive**: Approved by governance but not activated 2. **Active**: Called `activateMinter()` and can mint tokens 3. **Frozen**: Temporarily restricted from minting (but can still burn) 4. **Deactivated**: Permanently removed from the system Minters face several key constraints: - Must update collateral within the required interval (typically daily). As of today, the update collateral interval is set to 30 hours as per [this proposal](https://governance.m0.org/proposal/17943707564765307947230727208177770356513863441428702266086965014917482113266){rel=""nofollow""}. - Must maintain collateralization above the mint ratio - Pay interest on minted tokens at the minter rate - Face penalties for missed updates or undercollateralization ### Penalties Minters can receive penalties in two cases: 1. **Missed Updates Penalty**: Applied when minters fail to update collateral within required intervals - Calculated as `penaltyRate * principalOfActiveOwedM * missedIntervals` - Charged only once per missed interval 2. **Undercollateralization Penalty**: Applied when active owed `$M`exceeds allowed maximum - Calculated as `penaltyRate * principalOfExcessOwedM * timeSpan / updateCollateralInterval` - Proportional to the duration of undercollateralization ## Validators ### Role Definition Validators serve as a critical security layer, verifying the accuracy of Minters' collateral updates and having emergency powers to protect the protocol. They are trusted entities responsible for bridging the gap between offchain assets and onchain accounting. ### Key Functions Validators interact with the protocol primarily through signature generation and special authority functions: ### 1. Collateral Verification - Sign offchain messages (`updateCollateral` digest) verifying collateral updates, which includes the list of retrieval proposals being resolved. - Signatures include timestamps to prevent replay attacks - Multiple validators must sign collateral updates to meet the threshold ### 2. Security Controls - `cancelMint(address minter, uint256 mintId)`: Cancel suspicious mint proposals - `freezeMinter(address minter)`: Temporarily freeze a minter's ability to mint new tokens ### 3. Signature Requirements - Signatures must be provided in ascending order by validator address - Each signature must have a timestamp newer than the last signature from that validator - The protocol enforces a threshold of validator signatures for valid collateral updates ### Security Mechanisms Validators implement several security protections: - **Timestamp Verification**: Ensures signatures aren't reused or used out of order - **Signature Uniqueness**: Each validator can only provide one valid signature per collateral update - **Emergency Powers**: Ability to cancel mints and freeze Minters to mitigate potential harm ## Earners ### Role Definition Earners are approved addresses that can enable earning on their `$M` token balance, allowing them to receive yield based on the protocol's earning rate. This is a key feature that separates `$M` from traditional stablecoins. ### Key Functions Earners interact with the protocol primarily through the `MToken` contract: ### 1. Earning Status Management - `startEarning()`: Converts a regular balance to an earning balance (requires TTG approval) - `stopEarning()`: Converts an earning balance back to a regular balance (keeps all accrued interest) - `stopEarning(address)`: Safety function allowing anyone to stop earning for an account that's been removed from the approved list ### 2. Balance Mechanism When an account is in earning status: - Their balance automatically increases over time through continuous compounding - Balances are stored internally as "principal amounts" that grow with the earning index - No transactions are needed to accrue interest ### Rate Mechanics Earners receive yield based on the Earner Rate, which is determined by a sophisticated rate model: - The rate model ensures yield to earners never exceeds interest paid by minters - Uses a mathematical formula with both instantaneous and time-integrated approaches - Applied with a safety factor (98% of the theoretical safe rate) - Continuously updated when key functions are called ## Regular `$M` Holders ### Role Definition Regular `$M` holders use it as a stablecoin without the earning feature. Their balances remain static and only change through explicit transactions. ### Key Functions Regular `$M` holders have standard ERC20 capabilities with additional features: ### Token Operations - Standard transfers (`transfer`, `transferFrom`), approvals (`approve`), and balance inquiries (`balanceOf`). - Support for gasless approvals through EIP-2612 permits (`permit`). - Support for transfers with authorization through EIP-3009 (`transferWithAuthorization`, `receiveWithAuthorization`). ### Transfer Mechanics Transfers between different types of accounts follow specific rules: - Between two non-earning accounts: Standard token transfer. - Between non-earning and earning accounts: Balance conversion occurs, using protocol-favoring rounding (principal rounded down on receive, principal rounded up on send). - All transfers are subject to consistency checks and appropriate balance updates in the `MToken` contract. **Note on Minter Debt Repayment**: Regular `$M` holders can choose to interact with the `MinterGateway`'s `burnM` function. This allows them to burn their *own* tokens to help repay the outstanding debt of a specific Minter (either active or deactivated), acting as an indirect way to support protocol stability or participate in the informal liquidation of a deactivated minter's position. This is distinct from standard token operations. ## Power Token Holders ### Role Definition Power token holders act as the primary governance participants, responsible for day-to-day protocol governance through the `StandardGovernor` and emergency actions through the EmergencyGovernor. ### Key Functions Power token holders can influence the protocol through several mechanisms: ### 1. Standard Governance - Vote on adding/removing addresses from minter/validator/earner lists - Vote on key protocol parameters like mint ratio, collateral update interval, etc. - Propose and vote on protocol parameter changes - `StandardGovernor` proposals require a simple majority to pass (yes votes > no votes) ### 2. Emergency Governance - Vote on emergency actions through the EmergencyGovernor - Can take immediate action to protect the protocol in emergency situations - EmergencyGovernor uses a threshold ratio mechanism requiring a minimum percentage of yes votes ### 3. Token Mechanics - Power token is inflationary (10% per active voting epoch) - Token holders can directly vote or delegate their voting power - Snapshots of balances, voting powers, and delegations occur at epoch boundaries - Inflation rewards are only given to those who vote on ALL proposals in a voting epoch - Cannot transfer/delegate/mint during odd epochs (voting epochs) - Voting power is calculated from the previous epoch's balance ### 4. Dutch Auction - Can buy Power tokens during Dutch auctions during non-voting epochs - Price decreases exponentially over time, with the slope halving every period - Purchase price is relative to the previous epoch's total supply - Payments go to the Distribution Vault ## Zero Token Holders ### Role Definition Zero token holders possess meta-governance rights, essentially serving as the highest level of governance in the protocol. They have the unique ability to completely redeploy the governance system and receive rewards from excess yield and other protocol fees. ### Key Functions Zero token holders have special privileges in the protocol: ### 1. Meta-Governance - Can vote to completely redeploy the standard and emergency governance systems through the ZeroGovernor - Can reset governance to either the current Power token or Zero token holders (complete governance reset) - Can update the threshold ratio for proposals in ZeroGovernor or EmergencyGovernor - Can set new cashTokens for the entire system - ZeroGovernor proposals require a minimum threshold of votes to pass ### 2. Reward Mechanism - Receive Zero token rewards for participation in `StandardGovernor` voting - Claim excess yield and fees from the Distribution Vault - Buy Power token with a discount during Dutch auctions ### 3. Token Mechanics - ERC20 token with 6 decimals - Supports EIP-3009, EIP-712 with EIP-1271 support, and EIP-5805 - Can only be minted by `StandardGovernor` as reward for voting participation - Used for voting in ZeroGovernor - Epochs are 15 days long with snapshots taken at the end of each epoch ## Role Interactions and System Balance The M0 Protocol maintains balance through carefully designed interactions between roles: ### Minter-Validator Relationship - Minters propose collateral updates and mint requests - Validators verify collateral and can intervene when necessary - This creates a checks-and-balances system for minting activity ### Minter-Earner Balance - Minters pay interest on borrowed tokens (minter rate) - Earners receive interest on their holdings (earner rate) - Rate models ensure earner yield never exceeds minter interest obligations ### Governance Hierarchy - Power token holders control standard parameter changes through `StandardGovernor` - Zero token holders have meta-governance capabilities through ZeroGovernor - EmergencyGovernor provides quick response capability for urgent issues - This creates a tiered governance structure for different types of decisions ### Distribution Vault Mechanism - The DistributionVault receives excess yield generated by Minters not distributed to Earners - It also receives cash tokens from Power token auctions and unrefunded proposal fees - Zero token holders can claim their pro-rata share of these accumulated assets - This provides an incentive mechanism for Zero token holders to participate in governance ### Cash Token System - Cash tokens are used to pay proposal fees in `StandardGovernor` - They're also used to purchase Power tokens in Dutch auctions - Expected to be WETH and `$M` according to the Whitepaper - Must be in an allowed set defined at ZeroGovernor deployment - Can be changed via ZeroGovernor proposals ### Economic Incentives - Minters are incentivized to maintain proper collateralization - Validators are trusted to verify offchain collateral accurately - Earners benefit from yield generated by the system - Power token holders are rewarded with inflation for active participation in all proposals - Zero token holders can claim excess yield and fees while maintaining meta-governance rights ## Trust Model The protocol operates with the following trust assumptions: - **Validators**: Fully trusted to verify offchain collateral accurately and monitor onchain activities. They must be reachable and able to issue valid signatures for minters in a timely manner. - **Minters**: Trusted to maintain their offchain collateral and meet protocol obligations. Minters are expected to update their collateral at required intervals and pay any imposed penalties. - **`$M` holders**: Not trusted, but can freely use the token as a stablecoin without special permissions. - **Power token holders**: Trusted to act in the best interest of the protocol for day-to-day governance. They are expected to vote on proposals and participate actively in governance. - **Zero token holders**: Trusted with meta-governance authority to reset the entire governance system if necessary. They have the highest level of governance power and are assumed to act as responsible stewards of the protocol. - **CashTokens**: Expected to be WETH and `$M` as stated in the Whitepaper, but nothing prevents deployment with other cashTokens. These must be ERC20-compliant without special behavior (such as transfer hooks, rebasing mechanisms, or transfer fees). The protocol incorporates several safeguards against potential trust violations: 1. Multi-level governance (`StandardGovernor`, EmergencyGovernor, ZeroGovernor) 2. Collateral verification through multiple validators 3. Penalty systems for minters who don't follow protocol rules 4. Economic incentives aligning with protocol health for all participants This trust model creates a balanced system where each role has appropriate permissions and incentives to maintain protocol stability and security. # M0 On Solana 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](https://docs.m0.org/images/technical-documentations/solana/solana_m_programs.png) ### Onchain Programs The onchain logic is modular, distributed across the following main Solana programs written in Rust using the Anchor framework. | Program | Mainnet Program ID | Devnet Program ID | Description | | ----------------- | ---------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Portal** | `mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY` | `mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY` | The gateway for bridging `$M` between EVM chains and Solana. A fork of Wormhole's NTT program with M0-specific payload support and executor integration. | | **Earn** | `mz2vDzjbQDUDXBH6FPF5s4odCJ4y8YLE5QWaZ8XdZ9Z` | `mz2vDzjbQDUDXBH6FPF5s4odCJ4y8YLE5QWaZ8XdZ9Z` | Manages yield distribution and earner permissions for the base `$M` token with frozen-by-default security model. | | **M Extensions** | `3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5da` | `3C865D264L4NkAm78zfnDzQJJvXuU3fMjRUvRxyPi5da` | Unified extension framework (`m_ext`) compiled with feature flags for NoYield, ScaledUi, and Crank models. | | **Swap Facility** | `MSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzH` | `MSwapi3WhNKMUGm9YrxGhypgUEt7wYQH3ZgG32XoWzH` | Permissioned router for atomic swaps between whitelisted M0 extensions (`ext_swap` program). | ### Key Onchain Assets | Asset | Mainnet Mint Address | Devnet Mint Address | Description | | --------------- | --------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | **`$M` Token** | `mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo` | `mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6` | The base yield-bearing stablecoin, implemented as a Token-2022 asset with frozen-by-default security. | | **`$wM` Token** | `mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp` | `mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp` | The 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:** ```rust 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:** ```rust // $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:** ```rust [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 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. ```rust // 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: ```rust // Executor message structure pub struct ExecutorMessage { pub instruction_data: Vec, pub program_id: Pubkey, pub account_metas: Vec, } ``` 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 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 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: ```rust // 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):** ```rust pub struct ExtGlobal { pub admin: Pubkey, pub earn_authority: Option, // 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, } 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:** ```rust // 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 into extension token - **`unwrap(amount: u64)`:** Unwraps extension token back - **`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 ```rust pub struct SwapGlobal { pub bump: u8, pub admin: Pubkey, pub whitelisted_unwrappers: Vec, pub whitelisted_extensions: Vec, } 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)** ```tsx 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)** ```tsx 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**](https://docs.m0.org/resources/addresses#solana). ## 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](https://docs.m0.org/resources/addresses) for Fogo-specific program IDs. ## Source Code & Audits - **Portal & Earn Programs:** [m0-foundation/solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} - **Extension Framework & Swap Facility:** [m0-foundation/solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} - **Audits:** Security audits for all programs can be found in the [**Audits resource page**](https://docs.m0.org/resources/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 # Wrapped M (wM) ## Overview ### What is Wrapped `$M` (wM)? Wrapped `$M` (wM) is a non-rebasing ERC-20 wrapper for the rebasing and immutable `$M` token. It maintains yield-earning capabilities while providing compatibility with DeFi protocols that require standard ERC-20 tokens. While the native `$M` token automatically increases balances as yield accrues, Wrapped `$M` maintains static balances where yield can be explicitly claimed. This makes Wrapped `$M` compatible with protocols that aren't designed to handle rebasing tokens, dramatically expanding the utility of `$M` in the broader DeFi ecosystem. ![Wrapped M](https://docs.m0.org/images/technical-documentations/m/wM.png){.doc-logo} ### Why Wrapped M? Every M0 extension eventually wants to connect to certain utilities such as DeFi protocols, on/off ramps, etc. In order to concentrate liquidity, that utility should always exist in the form of M0's stablecoin building block. However, rebasing tokens automatically change their balances as yield accrues, which creates challenges for many DeFi protocols that expect static token balances. Wrapped `$M` solves this by: 1. Maintaining fixed token balances that don't automatically increase 2. Separately tracking and allowing manual claiming of accrued yield 3. Preserving the full economic benefits of the M0 platform while improving protocol compatibility 4. Enabling protocol-specific administration of earning capabilities ### Key Features - **DeFi Compatibility**: Works with lending platforms, AMMs, and other protocols that require standard ERC-20 behavior - **Yield Preservation**: Maintains yield-earning capabilities of the underlying `$M` token - **Value & Solvency**: wM aims for a direct value link to `$M` for earners. The target value for an earning token is its underlying `$M` principal plus accrued yield: [[[]{.katex-mathml}[[[]{.strut style="height:0.8778em;vertical-align:-0.1944em;"}[1]{.mord}[[ ]{.mord}]{.mord.text}[[earning ]{.mord}]{.mord.text}[[wM]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[wM yield ]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.7667em;vertical-align:-0.0833em;"}[1]{.mord}[[ ]{.mord}]{.mord.text}[[M]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[Accrued Yield]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br Critically, the entire system maintains a solvency invariant, ensuring it's always fully backed. The total `$M` held by the wM contract is always greater than or equal to the total wM supply plus all accrued yield owed to earners: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.3591em;vertical-align:-1.6647em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities, aka wM supply + accrued yield]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4715em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.1576em;"}[[]{.pstrut style="height:3em;"}[[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.8424em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.6647em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display}:br This guarantee is maintained through the system's design, where yield earned on `$M` held for non-earning wM holders, along with minor positive rounding effects during transfers, accumulates as "excess" reserves within the contract. - **Flexible Yield Options**: Configurable claiming and recipient mechanisms - **Delegated Administration**: Trusted admins can manage earner status without governance - **Protocol Value Capture**: System design naturally accumulates excess reserves that benefit the broader protocol ecosystem ### Comparison to `$M` | **Feature** | **`$M`** | **Wrapped M** | | ------------------ | ---------------------------------- | --------------------------------------- | | Balance Behavior | Automatically increases (rebasing) | Remains static until explicit claim | | Earning Mechanism | Implicit (balances grow) | Explicit (yield accumulates separately) | | Yield Realization | Automatic with balance updates | Manual claiming required | | DeFi Compatibility | Limited due to rebasing | Wide compatibility with standard ERC-20 | | Recipient Options | Limited | Configurable yield recipients | | Administration | Governance only | Governance + Delegated admins | ## Core Concepts ### Dual Mode: Earning vs. Non-Earning The wM token operates in two distinct modes for each account: #### Non-Earning Mode - Default state for new accounts - Balances remain static (standard ERC-20 behavior) - No yield accrues on these tokens - Simpler accounting with direct token amounts #### Earning Mode - Must be explicitly activated (requires approval via Governance or EarnerManager admin designation) - Balances remain static in storage - Represents a growing value based on a **principal amount** and an **index**, using an accounting model analogous to the native `$M` token. - Crucially, the **index used is derived from the underlying `$M` token** (wM does not maintain its own independent index). - Yield accumulates based on this principal and derived index, and can be claimed separately. Users can switch between Non-Earning and Earning modes if they meet the requirements for earning. ### Account Status Each wM account maintains several key properties tracked internally within the `WrappedMToken` contract: - **Balance**: The current wM token balance held by the account. This value remains static between operations and *excludes* any unclaimed yield. - **Earning Status**: A boolean flag indicating whether the account is currently in Earning Mode (true) or Non-Earning Mode (false). - **Earning Principal**: For accounts in Earning Mode, this stores the base principal amount used for yield calculations. This value is derived from the account's balance when it starts earning and remains constant while earning. - **Claim Recipient**: An optional address specified by the user via `setClaimRecipient()` to receive their claimed yield. If not set by the user, and no Governance override exists, yield is sent to the account owner itself. - **Earner Details**: A boolean flag indicating if settings apply when an account's earning status is managed by an EarnerManager admin. - If true, it implies the EarnerManager contract holds details (managing admin address, fee rate) for this account. - When a fee rate is set by the admin, a portion of the claimed yield (calculated as yield \* feeRate / 10000) is sent to that admin upon claiming, and the remaining net yield goes to the designated claim recipient (user-set, governance-set, or default owner). **Principal, Balance, and Yield Relationship:** When an account enters Earning Mode (`startEarningFor`), its current balance is converted into its initial `earningPrincipal`. This conversion is performed by the `IndexingMath.getPrincipalAmountRoundedDown`(balance, currentIndex) function, which calculates the principal amount corresponding to the balance at the currentIndex, taking into account the system's fixed-point scaling for the index and applying rounding rules that slightly favor the protocol. This `earningPrincipal` remains fixed while the account continues in Earning Mode. As the global `currentIndex` (derived from the `$M` token's index) increases over time, the value represented by this fixed principal grows. The fundamental relationship is: [[[]{.katex-mathml}[[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[Value Represented]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[currentIndex]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The account's stored balance only increases when yield is explicitly claimed. Therefore, the `accruedYield` represents the difference between the current value represented by the principal and the static stored balance: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[[accruedYield]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[currentIndex]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[balance]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} So, the target state before claiming is effectively: [[[]{.katex-mathml}[[[]{.strut style="height:0.7778em;vertical-align:-0.0833em;"}[[balance]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[accruedYield]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2778em;"}[≈]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[earningPrincipal]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[currentIndex]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Calling the `claimFor()` function calculates this `accruedYield`. It then handles potential fee deductions if `hasEarnerDetails` is true (sending the fee portion to the managing admin). The remaining net yield is transferred to the designated `claimRecipient`. If the `claimRecipient` is the account owner itself, their stored balance is increased by this net yield amount. This operation "catches up" the account's stored balance to reflect the value earned up to that point (minus any fees paid). ### Yield Generation and Claiming #### How Yield Accrues For accounts in earning mode, yield accrues based on: - The account's principal amount - The global wM index (derived from the `$M` token's index) - The time elapsed since the last index update The formula is: [[[]{.katex-mathml}[[[]{.strut style="height:0.6944em;"}[a]{.mord.mathnormal}[ccr]{.mord.mathnormal style="margin-right:0.0278em;"}[u]{.mord.mathnormal}[e]{.mord.mathnormal}[d]{.mord.mathnormal}[Y]{.mord.mathnormal style="margin-right:0.2222em;"}[i]{.mord.mathnormal}[e]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[d]{.mord.mathnormal}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[(]{.mopen}[p]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[in]{.mord.mathnormal}[c]{.mord.mathnormal}[i]{.mord.mathnormal}[p]{.mord.mathnormal}[a]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[c]{.mord.mathnormal}[u]{.mord.mathnormal}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[r]{.mord.mathnormal style="margin-right:0.0278em;"}[e]{.mord.mathnormal}[n]{.mord.mathnormal}[t]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[ba]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[an]{.mord.mathnormal}[ce]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Where: - `principal` is the stored earning principal for the account - `currentIndex` is the current wM index - `balance` is the current token balance #### Claiming Process When yield is claimed: 1. The accrued yield is calculated based on the current index 2. The account's balance is provisionally increased by the full yield amount 3. If the account's earning status is managed by an active EarnerManager admin with a configured fee rate, a percentage of the yield is deducted and sent to that admin as a fee 4. The remaining yield (minus any fees) is transferred to the designated recipient (or remains with the account owner if they are the recipient) Claiming doesn't change the principal amount, only the final balance reflects the net yield received. ### Index Mechanism Rather than maintaining its own independent index, wM derives its index from the `$M` token's index: #### When Earning is Enabled The wM index is calculated as: [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1em;vertical-align:-0.25em;"}[(]{.mopen}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[c]{.mord.mathnormal.mtight}[u]{.mord.mathnormal.mtight}[r]{.mord.mathnormal.mtight style="margin-right:0.0278em;"}[r]{.mord.mathnormal.mtight style="margin-right:0.0278em;"}[e]{.mord.mathnormal.mtight}[n]{.mord.mathnormal.mtight}[t]{.mord.mathnormal.mtight}[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[[ ]{.mord}]{.mord.text}[/]{.mord}[[ ]{.mord}]{.mord.text}[e]{.mord.mathnormal}[nab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}[)]{.mclose}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Where: - `M current index` is the current index from the `$M` token - `enableMIndex` is the `$M` index when earning was enabled - `disableIndex` is the previous wM index (if earning was previously disabled) #### When Earning is Disabled The wM index becomes static: [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[in]{.mord.mathnormal.mtight}[d]{.mord.mathnormal.mtight}[e]{.mord.mathnormal.mtight}[x]{.mord.mathnormal.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} This approach ensures that wM's yield calculation accurately reflects the underlying `$M` token's yield performance. ### Yield Recipients The wM system provides flexible options for directing claimed yield: #### Default Behavior By default, yield is claimed to the same account that generated it. #### Custom Recipients Account holders can specify a different address to receive their yield by calling `setClaimRecipient()`. #### Governance Overrides The TTG governance can set override recipients for specific accounts through the Registrar. #### Priority Order When determining where to send claimed yield: 1. User-specified recipient (if set) 2. Governance-specified override (if set) 3. The account itself (default) This flexibility allows for a variety of yield utilization strategies. ### System Invariants and Excess The wM system is designed around a critical solvency invariant to ensure it's always fully backed by the underlying `$M` token. **Balance Invariant**: The total `$M` token balance held by the `WrappedMToken` contract must always be greater than or equal to the sum of all wM tokens in circulation plus all accrued yield owed to earners. Expressed conceptually: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[Total wM Supply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[[Total Accrued Yield]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} Expressed using contract variables: [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.223em;vertical-align:-1.5285em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4715em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.1576em;"}[[]{.pstrut style="height:3em;"}[[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.8424em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.6944em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.5285em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **How the Invariant is Maintained:** Crucially, the `M Balance` held by the `WrappedMToken` contract is not static. Since the `WrappedMToken` contract itself is registered as an `$M` earner (as shown in the `$M` dashboard where the wM contract is the largest holder), **the underlying `$M` tokens it holds continuously accrue yield according to the `$M` token's rebasing mechanism.** This intrinsic yield generation on the *entire* pool of `$M` tokens held within the wM contract is fundamental to maintaining the solvency invariant. It ensures the `$M` balance grows naturally over time, actively keeping assets (M held) ahead of liabilities (wM supply + accrued yield). **Excess Reserves:** The difference between the actual `M Balance` held by the contract and the total calculated liabilities (`totalNonEarningSupply + projectedEarningSupply`) constitutes the **"excess"** reserves within the system. This excess primarily accumulates from two sources explained further below: 1. The yield generated by the underlying `$M` tokens that back the balances of **non-earning** wM holders (as this yield isn't owed back to them). 2. Minor positive rounding effects during various token operations (transfers, wrapping/unwrapping, starting/stopping earning). This invariant and the resulting excess ensure that the system remains fully solvent, can always honor unwrap requests, and captures additional value for the protocol ecosystem. ## Features and Operations ### Basic Token Operations #### Wrapping `$M` to wM Users can deposit `$M` tokens to receive an equivalent amount of wM: ```solidity wrap(recipient, amount) ``` The `$M` tokens are held by the wM contract and can potentially earn yield. #### Unwrapping wM to M Users can burn wM tokens to receive the equivalent amount of M: ```solidity unwrap(recipient, amount) ``` Any accrued yield remains in the user's wM balance if they're in earning mode. #### Gasless Operations The contract supports wrapping with permits for improved UX: ```solidity wrapWithPermit(recipient, amount, deadline, signature) ``` #### Standard ERC-20 Operations All standard ERC-20 functions are supported, including: - `transfer(to, amount)` - `transferFrom(from, to, amount)` - `approve(spender, amount)` - `balanceOf(account)` ### Earning Management #### Starting to Earn Approved accounts can activate earning status: ```solidity startEarningFor(account) ``` This converts their balance to a principal amount that will begin accruing yield. #### Stopping Earning Accounts can deactivate earning status (or anyone can do this for an account that's no longer approved): ```solidity stopEarningFor(account) ``` This claims any outstanding yield and converts the balance back to non-earning. #### Global Earning Status The wM contract itself must be an approved earner in the `$M` token system. Earning can be enabled or disabled for the entire system: ```solidity enableEarning() disableEarning() ``` A key improvement in wM v2 is the ability to re-enable earning after it has been disabled. ### Yield Management #### Claiming Yield Accrued yield can be claimed at any time: ```solidity claimFor(account) ``` This increases the account's wM balance by the yielded amount and triggers appropriate transfers based on recipient settings. #### Setting Claim Recipients Users can direct their yield to another address: ```solidity setClaimRecipient(recipient) ``` This allows for flexible yield strategies without changing the underlying balance. #### Checking Accrued Yield Several view functions allow users and integrations to check the yield that has accrued for an earning account but has **not yet been claimed**: - `accruedYieldOf(account)`: - Returns the amount of wM tokens earned as yield based on the account's `earningPrincipal` and the time elapsed (represented by the increase in the `currentIndex`) **since the last claim or since earning started**. - This represents **only the unclaimed portion** of the yield. It does *not* include yield that has already been claimed and added to the balance. - If the account is not in Earning Mode, this returns 0. - `balanceWithYieldOf(account)`: - Returns the sum of the account's current stored `balanceOf` **plus** the currently claimable `accruedYieldOf`. - This function effectively shows the total wM value attributable to the account *at that moment*, representing what the balance *would be* if the accrued yield were claimed instantly (before considering any fees or alternate claim recipients). **Effect of Claiming:** When `claimFor(account)` is successfully executed, the calculated accrued yield is processed. If the yield (net of fees) is directed to the account owner, their stored `balanceOf` increases. Consequently, immediately after a claim, `accruedYieldOf` will return 0 (or a negligible amount due to block timing), as the yield is no longer "accrued but unclaimed". `balanceWithYieldOf` will then equal `balanceOf`. New yield will start accumulating again as time passes and the `currentIndex` increases further. ### Excess Management & Value Capture **Concept:** The wM contract holds underlying `$M` tokens. Because the wM contract itself earns yield on *all* these `$M` tokens, but wM yield is only accrued for *earning-mode* wM holders, a surplus (or "excess") of `$M` tokens naturally accumulates within the contract over time. This primarily comes from the `$M` yield earned on tokens backing non-earning wM balances. ::note M and WrappedM are sharing the same Earners list. An earner of `$M` is an earner of WrappedM. :: **Claiming Excess:** This accumulated excess M, representing value captured by the protocol, can be claimed and transferred out using the `claimExcess()` function. ```solidity claimExcess() returns (uint240 claimed_) ``` **Destination:** The excessDestination address, set during deployment, receives these claimed tokens. Typically, this is the **Distribution Vault**, directing this value back to the protocol ecosystem and ultimately benefiting Zero token holders. ### Admin Operations via EarnerManager The EarnerManager contract empowers designated admins ('special users') with the delegated authority to approve wM earners, managing status for potentially many accounts outside the standard governance process. #### Setting Earner Status Admins can enable accounts for earning and set fee rates: ```text setEarnerDetails(account, status, feeRate) ``` #### Bulk Operations Multiple accounts can be managed in a single transaction: ```text setEarnerDetails(accounts[], statuses[], feeRates[]) ``` #### Checking Earner Status Various functions provide information about earner status: ```text earnerStatusFor(account) getEarnerDetails(account) ``` ## Technical Design ### Account Model The wM contract uses a sophisticated account model to efficiently track balances and yield information: ```solidity struct Account { bool isEarning; // Whether the account is in earning mode uint240 balance; // Current token balance (excluding yield) uint112 earningPrincipal; // Principal amount for earning accounts bool hasClaimRecipient; // Whether a custom recipient is set bool hasEarnerDetails; // Whether admin fee settings exist } ``` This structure packs related data together to minimize storage costs while maintaining all necessary information for the dual-mode system. ### Principal-Based Accounting A key aspect of wM's design is its principal-based accounting system for earning accounts: #### Principal Conversions When an account starts earning, its balance is converted to a principal amount: ```text principal = balance / currentIndex (rounded down) ``` This principal remains constant while the account is earning, while its value in tokens grows with the index. #### Present Value Calculation The current token value of a principal is: ```text presentValue = principal * currentIndex ``` This calculation is used when checking balances and claiming yield. #### Rounding Strategy The contract employs consistent rounding rules: - When converting from tokens to principal (adding to earning), principal is rounded DOWN - When calculating present value (for balance displays), the value is exact - When subtracting from earning accounts, principal is rounded UP These rules slightly favor the protocol, creating a small buffer of excess tokens that enhances stability. ### Supply Tracking The contract maintains precise tracking of token supply: - `totalNonEarningSupply`: Sum of all non-earning account balances - `totalEarningSupply`: Sum of all earning account balances (excluding accrued yield) - `totalEarningPrincipal`: Sum of all earning account principals - Total supply = `totalNonEarningSupply + totalEarningSupply` - `projectedEarningSupply`: The total earning supply if all accrued yield was claimed These values are used for various calculations, including excess determination. ### Transfer Mechanics The transfer system handles different scenarios based on account earning status: #### Same Status Transfers - Between earning accounts: Converts the amount to principal using the current index - Between non-earning accounts: Simple balance transfers without conversion #### Cross-Status Transfers - From earning to non-earning: Converts from principal to tokens - From non-earning to earning: Converts from tokens to principal - Affects total earning and non-earning supply tracking Each transfer type ensures that exact token amounts move between accounts while maintaining proper accounting in the appropriate mode. ### Fee Mechanism When enabled by an admin through the EarnerManager, fees can be taken from yield: - Fees are a percentage (in basis points, max 10,000) of claimed yield - Fee amounts are transferred to the admin who approved the account - If an admin is removed from the admin list, their fee settings become invalid - Fees are processed during the claim operation This creates an incentive for admins to onboard and manage accounts in the system. ### Excess Mechanism The `WrappedMToken` (wM) contract is designed to naturally accumulate "excess" `$M` tokens over time. This excess represents the `$M` tokens held by the contract *above and beyond* what is required to fully back all circulating wM tokens and their accrued yield entitlements. This mechanism enhances system stability and creates a value capture opportunity for the protocol. **Core Principle: Yield Mismatch** The fundamental driver of excess accumulation stems from a mismatch in yield earning: 1. **wM Contract Earns `$M` Yield:** As confirmed by its status as a major `$M` holder and its requirement to be an approved M earner (`enableEarning` requires `_isThisApprovedEarner`), the **entire balance of `$M` tokens held within the `WrappedMToken` contract continuously earns yield** according to the `$M` token's native rebasing mechanism. 2. **Not All wM Holders Earn wM Yield:** However, only wM holders whose accounts are explicitly set to **Earning Mode** (via Governance or EarnerManager approval) accrue a corresponding claimable wM yield entitlement based on their principal. Holders in **Non-Earning Mode** do not accrue wM yield. **Sources of Excess Accumulation:** The excess `$M` balance primarily grows from: 1. **Yield on `$M` Backing Non-Earning wM (Primary Source):** The `$M` yield generated by the portion of `$M` tokens backing the wM held by users in Non-Earning Mode directly contributes to the excess. Since these users aren't entitled to wM yield, the `$M` yield earned on their underlying share remains within the contract as surplus. 2. **Rounding Effects during Principal Conversions (Secondary Source & Potential Deficit):** The contract utilizes principal-based accounting for earners, requiring conversions between token amounts and principal values whenever balances change (e.g., `wrap`, `unwrap`, `startEarningFor`, `stopEarningFor`, cross-status transfers). 3. **Direct `$M` Transfers:** Any `$M` tokens sent directly to the `WrappedMToken` contract address (outside the `wrap` function) also become part of the excess. 4. **Cross-Status Transfers:** The mechanics of transfers between earning and non-earning accounts can also contribute minor gains due to the specific rounding applied during the principal conversions involved. **Calculating Excess:** The contract calculates the available excess using the `excess()` view function: [[[]{.katex-mathml}[[[]{.strut style="height:1.0385em;vertical-align:-0.3552em;"}[[[Excess]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[−]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:2.3341em;vertical-align:-1.5841em;"}[[[[[[]{.pstrut style="height:3em;"}[[[[Total wM Liabilities]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-1.4159em;"}[[]{.pstrut style="height:3em;"}[[[[[[]{.pstrut style="height:3em;"}[[]{.brace-left style="height:0.548em;"}[]{.brace-center style="height:0.548em;"}[]{.brace-right style="height:0.548em;"}]{.stretchy style="height:0.548em;min-width:1.6em;"}]{.svg-align style="top:-2.102em;"}[[]{.pstrut style="height:3em;"}[[[(totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}[[ProjectedEarningSupply)]{.mord}]{.mord.text}]{.mord}]{style="top:-3em;"}]{.vlist style="height:0.75em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.898em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{style="top:-3em;"}]{.vlist style="height:0.75em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:1.5841em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.minner.munder}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **Excess Management:** The accumulated excess `$M` tokens can be claimed and transferred out of the contract via the `claimExcess()` function. The `excessDestination` is set during deployment and typically points to the **Distribution Vault**, creating a revenue stream for the broader protocol ecosystem, ultimately benefiting Zero token holders. ## Integration Considerations ### For DeFi Protocols When integrating with wM, protocols should be aware of: - Standard ERC-20 behavior (no rebasing) - Distinction between balance and balance-with-yield - Impact of earning status on user experience - Potential for yield to accumulate while tokens are locked ### For Developers Developers working with wM should understand: - The relationship between wM and the underlying `$M` token - The impact of earning status on token accounting - The claiming process and its effect on balances - The consistent rounding patterns used throughout the contract ### Common Patterns Several patterns are recommended when working with wM: - Check both `balanceOf()` and `balanceWithYieldOf()` to understand a user's position - Consider calling `claimFor()` before critical operations to ensure balances reflect all value - Be aware of the impact of earning status transitions on token amounts - Understand the priority order for claim recipients when handling claimed yield ## Security Considerations ### Trust Model The wM system operates with several trust assumptions: - **MigrationAdmin**: Fully trusted role that can change contract implementation - **EarnerManager MigrationAdmin**: Can change EarnerManager implementation - **Registrar**: Source of truth for system parameters - **MToken**: Assumed to function correctly - **Admins**: Trusted to manage earning status appropriately ### Edge Cases Several edge cases are worth understanding: #### Admin Removal If an admin is removed from the admins list: - Accounts they approved lose earning status - Their fee settings become invalid - Accounts should be stopped from earning to prevent further yield accrual #### Disabled Earning If the wrapper contract is removed from M's earner list: - `disableEarning()` should be called to prevent further yield accrual - Individual accounts can continue to earn until that point #### Balance Precision The system has specific precision limitations: - All balances limited to uint240 to match `$M` token - Principal amounts limited to uint112 for gas efficiency - Indices use uint128 with 12 decimal places of precision ### Rounding Effects The consistent rounding strategy creates specific effects: - Multiple conversions between earning and non-earning states will result in small token losses - Transfers between accounts with different earning statuses create small protocol gains - These tiny amounts accumulate as protocol reserves, enhancing stability ## Upgrades and Migration ### Key Improvements in V2 Wrapped `$M` (wM) fundamentally operates as a non-rebasing token where yield is realized through manual claiming, a characteristic distinct from the native `$M` token's automatic rebasing. The V2 implementation builds upon this established model with several important enhancements: 1. **Re-enabling Earning**: V2 allows earning to be re-enabled after being disabled 2. **Admin-Managed Earners**: Introduction of EarnerManager for delegated administration 3. **Fee Mechanism**: Ability for admins to take fees from yield 4. **Principal-Based Accounting**: Changed from lastIndex tracking to principal amount tracking 5. **Explicit Claim Recipients**: Added ability for users to explicitly set yield recipients 6. **Gas Efficiency**: Optimized storage layout and operations ### Migration from V1 The migration from V1 to V2 involved several key transformations: #### Account Model Change - V1: Used last interaction index for earning accounts - V2: Uses principal amount for earning accounts - Migration formula: `principal = balance / lastIndex` #### Index Handling - V1: Used an array for enable/disable indices - V2: Uses enableMIndex and disableIndex slots - Migration preserved earning state and index values #### Migration Process The migration was handled through a specialized migrator contract that: 1. Identified all earning accounts in V1 2. Converted their account structure to the V2 format 3. Preserved their earning status and equivalent yield position # Wrapped M Specification ## Contract overview `WrappedMToken` (wM) is a non-rebasing ERC-20 wrapper for the rebasing `$M` token. It maintains yield-earning capabilities while providing compatibility with DeFi protocols that require standard ERC-20 tokens. - **Decimals:** 6 (matches `$M`) - **Index:** Derived from the underlying `$M` token's index - **Yield realization:** Explicit claiming (not automatic rebasing) --- ## Key functions ### Wrapping and Unwrapping | Function | Description | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `wrap(address recipient, uint256 amount)` | Deposit `$M` tokens, receive equivalent wM | | `unwrap(address recipient, uint256 amount)` | Burn wM tokens, receive equivalent `$M` | | `wrapWithPermit(address recipient, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` | Wrap with gasless `$M` approval | ### ERC20 Standard | Function | Description | | -------------------------------------------------------- | ------------------------------------------------- | | `transfer(address to, uint256 amount)` | Transfer wM tokens | | `transferFrom(address from, address to, uint256 amount)` | Transfer wM on behalf of another address | | `approve(address spender, uint256 amount)` | Approve a spender allowance | | `balanceOf(address account)` | Returns stored balance (excludes unclaimed yield) | | `totalSupply()` | Returns total wM in circulation | ### Earning Management | Function | Description | | ---------------------------------- | -------------------------------------------------- | | `startEarningFor(address account)` | Activate earning mode for an approved account | | `stopEarningFor(address account)` | Deactivate earning mode (claims outstanding yield) | | `enableEarning()` | Enable earning for the wM contract itself on `$M` | | `disableEarning()` | Disable earning for the wM contract on `$M` | ### Yield Management | Function | Description | | -------------------------------------- | --------------------------------------------- | | `claimFor(address account)` | Claim accrued yield for an account | | `setClaimRecipient(address recipient)` | Set a custom address to receive claimed yield | ### Balance and Yield Queries | Function | Returns | | ------------------------------------- | ------------------------------------------------ | | `balanceOf(address account)` | Stored balance (excludes unclaimed yield) | | `accruedYieldOf(address account)` | Unclaimed yield for an earning account | | `balanceWithYieldOf(address account)` | `balanceOf` + `accruedYieldOf` | | `isEarning(address account)` | Whether the account is in earning mode | | `totalNonEarningSupply()` | Sum of all non-earning balances | | `totalEarningSupply()` | Sum of all earning balances (excluding yield) | | `projectedEarningSupply()` | Total earning supply if all yield were claimed | | `excess()` | `$M` held by contract minus total wM liabilities | ### Excess Management | Function | Description | | --------------------- | ------------------------------------------------------------------------------- | | `claimExcess()` | Transfer accumulated excess `$M` to `excessDestination` | | `excessDestination()` | Returns the address that receives claimed excess (typically Distribution Vault) | ### Admin Operations (via EarnerManager) | Function | Description | | -------------------------------------------------------------------------- | ---------------------------------------------------- | | `setEarnerDetails(address account, bool status, uint16 feeRate)` | Set earning status and fee rate for an account | | `setEarnerDetails(address[] accounts, bool[] statuses, uint16[] feeRates)` | Bulk set earning details | | `earnerStatusFor(address account)` | Check earner status for an account | | `getEarnerDetails(address account)` | Get full earner details including admin and fee rate | --- ## Account storage model ```solidity struct Account { bool isEarning; // Whether the account is in earning mode uint240 balance; // Current token balance (excluding yield) uint112 earningPrincipal; // Principal amount for earning accounts bool hasClaimRecipient; // Whether a custom recipient is set bool hasEarnerDetails; // Whether admin fee settings exist } ``` --- ## Events | Event | Description | | ---------------------------------------------------------------------------- | ---------------------------------- | | `Transfer(address indexed from, address indexed to, uint256 value)` | Standard ERC20 transfer | | `Approval(address indexed owner, address indexed spender, uint256 value)` | Standard ERC20 approval | | `StartedEarning(address indexed account)` | Account entered earning mode | | `StoppedEarning(address indexed account)` | Account exited earning mode | | `Claimed(address indexed account, address indexed recipient, uint256 yield)` | Yield claimed | | `ExcessClaimed(uint240 amount)` | Excess `$M` claimed to destination | --- ## Errors | Error | Condition | | ----------------------------------------------------------------------- | ------------------------------------------------ | | `NotApprovedEarner()` | Account is not approved for earning | | `EarningIsEnabled()` | Operation not permitted while earning is enabled | | `EarningIsDisabled()` | Operation requires earning to be enabled | | `InsufficientBalance(address account, uint256 balance, uint256 amount)` | Transfer, unwrap, or burn exceeds balance | --- ## Index mechanism The wM index is derived from the underlying `$M` token's index: **When earning is enabled:** [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[index]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:2.0463em;vertical-align:-0.686em;"}[[]{.mopen.nulldelimiter}[[[[[[]{.pstrut style="height:3em;"}[[e]{.mord.mathnormal}[nab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[M]{.mord.mathnormal style="margin-right:0.109em;"}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.mord}]{style="top:-2.314em;"}[[]{.pstrut style="height:3em;"}[]{.frac-line style="border-bottom-width:0.04em;"}]{style="top:-3.23em;"}[[]{.pstrut style="height:3em;"}[[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[currentIndex]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}]{.mord}]{style="top:-3.677em;"}]{.vlist style="height:1.3603em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.686em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.mfrac}[]{.mclose.nulldelimiter}]{.mord}[]{.mspace style="margin-right:0.2222em;"}[×]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} **When earning is disabled:** [[[]{.katex-mathml}[[[]{.strut style="height:0.8333em;vertical-align:-0.15em;"}[w]{.mord.mathnormal style="margin-right:0.0269em;"}[[M]{.mord.mathnormal style="margin-right:0.109em;"}[[[[[[]{.pstrut style="height:2.7em;"}[[[[index]{.mord.mtight}]{.mord.text.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.55em;margin-left:-0.109em;margin-right:0.05em;"}]{.vlist style="height:0.3361em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.15em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[=]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.6944em;"}[d]{.mord.mathnormal}[i]{.mord.mathnormal}[s]{.mord.mathnormal}[ab]{.mord.mathnormal}[l]{.mord.mathnormal style="margin-right:0.0197em;"}[e]{.mord.mathnormal}[I]{.mord.mathnormal style="margin-right:0.0785em;"}[n]{.mord.mathnormal}[d]{.mord.mathnormal}[e]{.mord.mathnormal}[x]{.mord.mathnormal}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} --- ## Yield claim recipient priority When determining where to send claimed yield: 1. User-specified recipient (via `setClaimRecipient`) 2. Governance-specified override (via Registrar) 3. The account itself (default) --- ## Solvency invariant [[[]{.katex-mathml}[[[]{.strut style="height:1.0496em;vertical-align:-0.3552em;"}[[[M Balance]{.mord}]{.mord.text}[[[[[[]{.pstrut style="height:2.7em;"}[[[(]{.mopen.mtight}[[wM contract]{.mord.mtight}]{.mord.text.mtight}[)]{.mclose.mtight}]{.mord.mtight}]{.sizing.reset-size6.size3.mtight}]{style="top:-2.5198em;margin-right:0.05em;"}]{.vlist style="height:0.3448em;"}[​]{.vlist-s}]{.vlist-r}[[[]]{.vlist style="height:0.3552em;"}]{.vlist-r}]{.vlist-t.vlist-t2}]{.msupsub}]{.mord}[]{.mspace style="margin-right:0.2778em;"}[≥]{.mrel}[]{.mspace style="margin-right:0.2778em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[totalNonEarningSupply]{.mord}]{.mord.text}[]{.mspace style="margin-right:0.2222em;"}[+]{.mbin}[]{.mspace style="margin-right:0.2222em;"}]{.base}[[]{.strut style="height:0.8889em;vertical-align:-0.1944em;"}[[projectedEarningSupply]{.mord}]{.mord.text}]{.base}]{.katex-html ariaHidden="true"}]{.katex}]{.katex-display} The excess above this threshold accumulates from yield on `$M` backing non-earning wM holders and minor rounding effects. --- ## Related - [Wrapped M overview](https://docs.m0.org/protocol/wrapped-m) - Full conceptual documentation - [M Token specification](https://docs.m0.org/protocol/m-token-spec) - `$M` token spec - [Distribution Vault](https://docs.m0.org/protocol/distribution-vault) - Where excess is typically directed # API Reference ## Getting an API key Both Orchestration and Protocol APIs require authentication. Get in touch with the M0 team to obtain your API key. [Contact us →](https://www.m0.org/contact-us){rel=""nofollow""} --- ## Orchestration API (REST) **What it does:** Get quotes and execute conversions between M0 stablecoin extensions and other stablecoins (USDC, USDT). This is the API powering **M0's Onchain Orchestration** product. - **Endpoint**: `https://gateway.m0.xyz/v1/orchestration` - **Type**: REST - **Authentication**: API key in `x-api-key` header **Good for:** - Getting conversion quotes between your stablecoin extension and USDC/USDT - Integrating M0 liquidity into your product's flows - Automating stablecoin distribution to you, your partners or clients [Orchestration API overview →](https://docs.m0.org/api-reference/orchestration/overview) [Full endpoint reference →](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} [Type generation →](https://docs.m0.org/api-reference/orchestration/type-generation) --- ## Protocol API (GraphQL) **What it does:** Query on-chain data - token supplies, holder balances, rewards accrual, earner status, and configuration parameters. - **Endpoint**: `https://protocol-api.m0.org/graphql` - **Type**: GraphQL - **Authentication**: API key in `Authorization` header **Good for:** - Monitoring token supply and collateral coverage - Querying rewards earned by a specific stablecoin extension - Building dashboards on top of protocol state - Auditing stablecoin extension balances and earner approvals [Try the interactive playground →](https://protocol-api.m0.org/graphql){rel=""nofollow""} [Protocol API overview →](https://docs.m0.org/api-reference/protocol/overview) [Common query recipes →](https://docs.m0.org/api-reference/protocol/recipes/network-supply) --- ## Get started ::card-group :::card --- icon: i-lucide-split title: Orchestration API to: https://docs.m0.org/api-reference/orchestration/overview --- Get quotes, place orders, and manage cross-chain operations. ::: :::card --- icon: i-lucide-key title: Protocol API to: https://docs.m0.org/api-reference/protocol/overview --- Query on-chain protocol data via GraphQL, with authentication setup and key storage best practices. ::: :::card --- icon: i-lucide-book-open title: GraphQL Recipes to: https://docs.m0.org/api-reference/protocol/recipes/network-supply --- Common GraphQL queries for network supply, token data, yields, and more. ::: :: # POST /orders/{originChain}/{orderId}/cancel This endpoint builds a cancellation transaction for an existing limit order. The returned payload must be signed and submitted to the blockchain by the caller to complete the cancellation. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text POST /orders/{originChain}/{orderId}/cancel ``` ### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` ### Path Parameters | Field | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------- | | `originChain` | `Chain` | Yes | The chain where the order was created (e.g., `Ethereum`) | | `orderId` | `string` | Yes | The ID of the limit order to cancel | ### Body Parameters (optional) | Field | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `caller` | `string` | No | SVM only: the base58 pubkey that will sign and pay for the resulting Solana transaction. Required for a permissionless cancel after the order's `fillDeadline`; defaults to the original sender. Ignored for EVM destinations. | ## Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type TransactionPayload = components["schemas"]["TransactionPayload"]; const originChain = "Ethereum"; const orderId = "0xabc123..."; const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders/${originChain}/${orderId}/cancel`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, }, ); if (!response.ok) { const error = await response.text(); throw new Error(`Cancel order failed: ${error}`); } const payload: TransactionPayload = await response.json(); ``` ## Response ### Success Response (200) Returns a `TransactionPayload` to submit on-chain to complete the cancellation — either an `EvmPayload` or `SvmPayload` depending on the chain. ```typescript import type { components } from "./m0-swap"; type TransactionPayload = components["schemas"]["TransactionPayload"]; type EvmPayload = components["schemas"]["EvmPayload"]; type SvmPayload = components["schemas"]["SvmPayload"]; ``` The payload carries its own `chain` (and `chainId` for EVM payloads) indicating where the transaction must be broadcast: - **Same-chain orders** (origin == destination): broadcast on the order's chain. The payload's `chain` matches the URL `originChain`. - **Cross-chain orders** (origin != destination): the cancel must be initiated on the order's **destination chain**, which then dispatches a CancelReport message back to the origin chain to release the deposited funds. The payload's `chain` is the destination chain and **differs from the URL path parameter**. The payload's `value` includes the bridge fee. Always read `payload.chain` (and `payload.chainId` for EVM) to determine the broadcast target — do not assume it matches the URL path parameter. See the [Quote](https://docs.m0.org/api-reference/orchestration/quote) docs for payload execution examples. ### Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | --------------------- | ---------------------------------------------------------------------------- | | `400` | `BadOrderRequest` | Malformed request (unknown chain, malformed `orderId`, unsupported `caller`) | | `404` | `OrderNotFound` | The referenced order does not exist on-chain | | `409` | `OrderNotCancellable` | The order is already in a terminal state (`CANCELLED` / `COMPLETED`) | | `500` | `CancelOrderError` | Unexpected internal failure — retrying may succeed | ## Example Response ```json { "type": "evm", "chain": "Ethereum", "chainId": 1, "to": "0x1234567890abcdef...", "data": "0xabcdef...", "value": "0" } ``` ## Executing the Cancellation Once you have the response, submit the payload to the blockchain to complete the cancellation: ```typescript import { createWalletClient, http } from "viem"; import { mainnet } from "viem/chains"; import type { components } from "./m0-swap"; type EvmPayload = components["schemas"]["EvmPayload"]; async function executeCancelOrder(payload: EvmPayload, account: `0x${string}`) { const client = createWalletClient({ chain: mainnet, transport: http(), account, }); const hash = await client.sendTransaction({ to: payload.to as `0x${string}`, data: payload.data as `0x${string}`, value: BigInt(payload.value), }); return hash; } ``` ## Notes - Only the original order creator can cancel a limit order before its `fillDeadline`; on SVM, a permissionless cancel after the deadline requires the `caller` body parameter - A `409` response means the order is in a terminal state (filled or already cancelled) and no action is needed - The cancellation is not final until the returned transaction payload is submitted and confirmed on-chain # GET /orders/{originChain}/{orderId} This endpoint returns the full on-chain state plus indexer enrichment for a single order. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /orders/{originChain}/{orderId} ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Path Parameters | Field | Type | Required | Description | | ------------- | -------- | -------- | -------------------------------------------------------- | | `originChain` | `Chain` | Yes | The chain where the order was created (e.g., `Ethereum`) | | `orderId` | `string` | Yes | The order ID to retrieve | ## Example Request ```typescript import type { components } from "./m0-swap"; type Order = components["schemas"]["Order"]; const originChain = "Ethereum"; const orderId = "0xabc123"; const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders/${originChain}/${orderId}`, { method: "GET", headers: { "x-api-key": "YOUR_API_KEY", }, }, ); if (response.status === 404) { throw new Error("Order not found"); } if (!response.ok) { const error = await response.text(); throw new Error(`Get order failed: ${error}`); } const order: Order = await response.json(); ``` ## Response ### Success Response (200) Returns an `Order` object. On-chain fields are authoritative; indexer-only fields (`openTx`, `fillCount`, `fillReportCount`, `resolvedAt`, `fills`) are optional and may be absent if the indexer has not yet caught up or is unavailable. ```typescript import type { components } from "./m0-swap"; type Order = components["schemas"]["Order"]; type OrderStatus = components["schemas"]["OrderStatus"]; type Fill = components["schemas"]["Fill"]; ``` ### Response Fields | Field | Type | Description | | ------------------ | --------------- | ---------------------------------------------------- | | `orderId` | `string` | Unique order identifier | | `version` | `number` | Order version | | `nonce` | `string` | Order nonce | | `sender` | `string` | Sender wallet address | | `recipient` | `string` | Recipient wallet address | | `originChainId` | `number` | Origin chain ID | | `destChainId` | `number` | Destination chain ID | | `tokenIn` | `string` | Input token address | | `tokenOut` | `string` | Output token address | | `amountIn` | `string` | Input amount (smallest unit) | | `amountOut` | `string` | Target output amount (smallest unit) | | `designatedSolver` | `string` | Assigned solver address | | `createdAt` | `number` | Creation timestamp (Unix seconds) | | `fillDeadline` | `number` | Fill deadline timestamp (Unix seconds) | | `status` | `OrderStatus` | Current status (`CREATED`, `COMPLETED`, `CANCELLED`) | | `amountOutFilled` | `string` | Total output filled so far | | `amountInReleased` | `string` | Input amount released so far | | `amountRefunded` | `string` | Amount refunded | | `openTx` | `string | null` | Transaction hash that opened the order | | `fillCount` | `number | null` | Number of fills | | `fillReportCount` | `number | null` | Number of fill reports | | `resolvedAt` | `number | null` | Resolution timestamp when applicable | | `fills` | `Fill[] | null` | Fill events recorded by the indexer, chronological | ### Fill Fields An order may have multiple fills when filled by multiple solvers or in multiple transactions. | Field | Type | Description | | ------------------- | -------- | ---------------------------------- | | `filledAt` | `number` | Fill timestamp (Unix seconds) | | `chainId` | `number` | Chain ID where the fill occurred | | `transactionHash` | `string` | Fill transaction hash | | `solver` | `string` | Solver address that filled | | `amountInToRelease` | `string` | Input amount released by this fill | | `amountOutFilled` | `string` | Output amount filled by this fill | ::note Further inspect the response fields on the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ### Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | ----------------- | ------------------------------------------------------------ | | `400` | `BadOrderRequest` | Malformed request (unknown chain, malformed `orderId`, etc.) | | `404` | `OrderNotFound` | The referenced order does not exist on-chain | ## Example Response ```json { "orderId": "0xabc123", "version": 1, "nonce": "42", "sender": "0xYourWalletAddress", "recipient": "0xYourWalletAddress", "originChainId": 1, "destChainId": 8453, "tokenIn": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "tokenOut": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountIn": "1000000", "amountOut": "999500", "designatedSolver": "0xSolverAddress", "createdAt": 1771511463, "fillDeadline": 1771515063, "status": "COMPLETED", "amountOutFilled": "999500", "amountInReleased": "1000000", "amountRefunded": "0", "openTx": "0xOpenTxHash", "fillCount": 1, "fillReportCount": 1, "resolvedAt": 1771511780, "fills": [ { "filledAt": 1771511700, "chainId": 8453, "transactionHash": "0xFillTxHash", "solver": "0xSolverAddress", "amountInToRelease": "1000000", "amountOutFilled": "999500" } ] } ``` # GET /orders This endpoint lists orders with optional filters for sender, status, chain IDs, and pagination. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /orders ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Query Parameters | Field | Type | Required | Description | | --------------- | ------------- | -------- | -------------------------------------------------------------- | | `sender` | `string` | No | Filter by sender wallet address | | `status` | `OrderStatus` | No | Filter by order status: `CREATED`, `COMPLETED`, or `CANCELLED` | | `originChainId` | `number` | No | Filter by origin chain ID | | `destChainId` | `number` | No | Filter by destination chain ID | | `limit` | `number` | No | Maximum number of results to return | | `offset` | `number` | No | Pagination offset | ## Example Request ```typescript import type { components } from "./m0-swap"; type OrdersResponse = components["schemas"]["OrdersResponse"]; const params = new URLSearchParams({ sender: "0xYourWalletAddress", status: "CREATED", limit: "20", offset: "0", }); const response = await fetch( `https://gateway.m0.xyz/v1/orchestration/orders?${params.toString()}`, { method: "GET", headers: { "x-api-key": "YOUR_API_KEY", }, }, ); if (!response.ok) { const error = await response.text(); throw new Error(`Get orders failed: ${error}`); } const result: OrdersResponse = await response.json(); ``` ## Response ### Success Response (200) Returns an `OrdersResponse` object. ```typescript import type { components } from "./m0-swap"; type OrdersResponse = components["schemas"]["OrdersResponse"]; type OrderSummary = components["schemas"]["OrderSummary"]; type OrderStatus = components["schemas"]["OrderStatus"]; ``` ### Response Fields | Field | Type | Description | | -------- | ---------------- | -------------------------------- | | `orders` | `OrderSummary[]` | List of matching orders | | `total` | `number` | Total matching records available | | `limit` | `number` | Page size returned | | `offset` | `number` | Pagination offset returned | `OrderSummary` is a compact order shape sourced from the indexer-fed database, so on-chain-only fields (`nonce`, `fillDeadline`, `recipient`, `version`) are not included. Use [`GET /orders/{originChain}/{orderId}`](https://docs.m0.org/api-reference/orchestration/order-status) for the full resource. ::note Further inspect the response fields on the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Example Response ```json { "orders": [ { "orderId": "0xabc123", "createdAt": 1771511463, "originChainId": 1, "sender": "0xYourWalletAddress", "tokenIn": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountIn": "1000000", "destChainId": 8453, "tokenOut": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "amountOut": "999500", "designatedSolver": "0xSolverAddress", "openTx": "0xOpenTxHash", "amountOutFilled": "0", "fillCount": 0, "amountInReleased": "0", "fillReportCount": 0, "amountRefunded": "0", "status": "CREATED", "resolvedAt": null } ], "total": 1, "limit": 20, "offset": 0 } ``` ## Notes - Use `limit` and `offset` together for pagination - `status` values are uppercase enum values: `CREATED`, `COMPLETED`, or `CANCELLED` - `createdAt` and `resolvedAt` are Unix timestamps (seconds) # Orchestration API This API provides quotes for moving and converting between M0 extensions and other stablecoins ## Base URL ```text https://gateway.m0.xyz/v1/orchestration ``` ## Authentication The M0 Orchestration API requires authentication via API keys to access its endpoint. This ensures that only authorized users can interact with the API and access its data. [Contact us](https://www.m0.org/contact-us){rel=""nofollow""} to obtain an API key. ### Using API Keys To authenticate your requests, include the API key in the Authentication header of your HTTP requests. For example post with curl: ```sh curl -i \ --request GET \ --header "x-api-key: YOUR_API_KEY" \ https://gateway.m0.xyz/v1/orchestration/supported-assets ``` API keys should be kept secret and not exposed in public repositories or client-side code. If you believe your API key has been compromised, please contact us immediately to revoke the key and issue a new one ## Endpoints - `GET /supported-assets` - `POST /quote` - `GET /orders` - `GET /orders/{originChain}/{orderId}` - `POST /orders/{originChain}/{orderId}/cancel` - `GET /topology/routes` - `GET /topology/stats` For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. ## Errors All API error responses share the same body shape: ```json { "code": "BadQuoteRequest", "message": "Human-readable description of what went wrong", "requestId": "abc-123" } ``` - `code` — stable machine-readable identifier for the failure mode. Branch on it rather than on the HTTP status, since multiple errors may map to the same status over time. - `message` — human-readable description, safe to surface in a UI. - `requestId` — per-request correlation ID matching the server logs. Include it when filing support tickets. # POST /quote This endpoint returns one or more quotes for a given asset route and amount. Each quote includes the expected output amount, estimated fill time, and transaction payloads ready to be signed and submitted to the blockchain. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text POST /quote ``` ### Headers ```text Content-Type: application/json x-api-key: YOUR_API_KEY ``` ### Body Parameters | Field | Type | Required | Description | | -------------- | ----------------- | -------- | -------------------------------------------------------------------------------- | | `route` | `Route` | Yes | The `source` and `destination` assets, each a `{ chain, address }` pair | | `amountIn` | `string` | Yes | Input amount in the token's smallest unit | | `sender` | `string` | Yes | The wallet address sending the input asset | | `recipient` | `string` | No | The address receiving the output asset (defaults to `sender`) | | `maxNumQuotes` | `number` | No | Maximum number of quotes to return (default `1`) | | `providers` | `ProvidersParams` | No | `include` or `exclude` lists of providers to constrain routing (set at most one) | Check the [API reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""} for detailed schema definitions. ## Example Request ```typescript import type { components } from "./m0-swap"; // From type generation type Quote = components["schemas"]["Quote"]; const response = await fetch("https://gateway.m0.xyz/v1/orchestration/quote", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify({ route: { source: { chain: "Ethereum", address: "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", // M token }, destination: { chain: "Base", address: "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", // M token }, }, amountIn: "1000000", // 1 M (6 decimals) sender: "0xYourWalletAddress", maxNumQuotes: 1, }), }); const quotes: Quote[] = await response.json(); ``` ## Example Response ```json [ { "route": { "source": { "chain": "Ethereum", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b" }, "destination": { "chain": "Base", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b" } }, "recipient": "0xYourWalletAddress", "amountIn": "1000000", "amountOut": "999500", "estFillTime": 180, "payloads": [ { "provider": "m-wormhole-portal", "annotation": "Bridge M from Ethereum to Base", "data": { "type": "evm", "chain": "Ethereum", "chainId": 1, "to": "0x1234567890abcdef...", "data": "0xabcdef...", "value": "0" } } ] } ] ``` ## Error Responses All errors share the `{ code, message, requestId }` body shape. Branch on `code` rather than on the HTTP status. | Status | Code | Description | | ------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `400` | `BadQuoteRequest` | Malformed or invalid body (missing fields, same source and destination, both `include` and `exclude` set, unsupported asset) | | `404` | `NoQuotesAvailable` | The request was valid but no provider could produce a quote for this route and amount | | `500` | `QuoteError` | Unexpected internal failure while computing quotes — retrying may succeed | ## Executing Quote Payloads Once you have a quote, you can execute its payloads using your preferred chain interface. Here is an example executing a payload on Base using [Viem](https://viem.sh){rel=""nofollow""}. ### EVM Payload Execution ```typescript import type { components } from "./m0-swap"; // From type generation import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; type EvmPayload = components["schemas"]["EvmPayload"]; async function executeEvmPayload( payload: EvmPayload, account: ReturnType, ) { const client = createPublicClient({ chain: base, transport: http(), }); // Send the transaction const hash = await client.sendTransaction({ to: payload.to as `0x${string}`, data: payload.data as `0x${string}`, value: BigInt(payload.value), }); // Wait for confirmation const receipt = await client.waitForTransactionReceipt({ hash, confirmations: 1, }); return { hash, receipt }; } ``` ## Notes - The `amountIn` parameter should be provided in the token's smallest unit (e.g., for a 6-decimal token, `1000000` represents 1 token) - Results are ranked by best `amountOut`, fastest `estFillTime`, and fewest payloads (in that order), and the response is capped at `maxNumQuotes` (default `1`) - Payloads should be executed in order - wait for each transaction to confirm before sending the next - Cross-chain quotes may include multiple payloads (e.g., approve + bridge) - The `estFillTime` is an estimate in seconds for the entire route to complete # GET /supported-assets This endpoint returns a list of all tokens that can be used as source or destination assets when requesting quotes. Each asset includes chain information, contract address, symbol, decimals, and whether it's an M0 protocol extension. ::note **TypeScript Types**: Generate types for this API with a single command. See [Type Generation](https://docs.m0.org/api-reference/orchestration/type-generation). :: ::note **API Reference**: For detailed schema definitions and interactive testing, see the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. :: ## Request ### Endpoint ```text GET /supported-assets ``` ### Headers ```text x-api-key: YOUR_API_KEY ``` ### Parameters This endpoint accepts no parameters. ## Response ### Success Response (200) Returns an array of `Asset` objects. ```typescript // Using generated types (see type generation) import type { components } from "./m0-swap"; type Asset = components["schemas"]["Asset"]; type Chain = components["schemas"]["Chain"]; type ChainRuntime = components["schemas"]["ChainRuntime"]; ``` ### Asset Fields | Field | Type | Description | | ------------- | -------------- | ------------------------------------------------ | | `chain` | `Chain` | The blockchain network where this asset exists | | `runtime` | `ChainRuntime` | The chain's runtime environment (`evm` or `svm`) | | `address` | `string` | Token contract address | | `icon` | `string` | URL to the token's icon image | | `decimals` | `number` | Number of decimal places for the token | | `symbol` | `string` | Token ticker symbol (e.g., "M", "USDC") | | `name` | `string` | Full token name | | `m0Extension` | `boolean` | Whether this token is an M protocol extension | ## Example Request ```typescript import type { components } from "./m0-swap"; type Asset = components["schemas"]["Asset"]; const response = await fetch( "https://gateway.m0.xyz/v1/orchestration/supported-assets", { headers: { "x-api-key": "YOUR_API_KEY", }, }, ); const assets: Asset[] = await response.json(); ``` ## Example Response ```json [ { "chain": "Ethereum", "runtime": "evm", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "icon": "https://example.com/m-token.png", "decimals": 6, "symbol": "M", "name": "M Token", "m0Extension": true }, { "chain": "Ethereum", "runtime": "evm", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "icon": "https://example.com/usdc.png", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "m0Extension": false }, { "chain": "Solana", "runtime": "svm", "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "icon": "https://example.com/usdc.png", "decimals": 6, "symbol": "USDC", "name": "USD Coin", "m0Extension": false }, { "chain": "Base", "runtime": "evm", "address": "0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b", "icon": "https://example.com/m-token.png", "decimals": 6, "symbol": "M", "name": "M Token", "m0Extension": true } ] ``` ## Notes - Assets may appear on multiple chains with the same symbol but different addresses - The `runtime` field helps determine which wallet type is needed (EVM wallet vs Solana wallet) - Use the `decimals` field to properly format amounts when displaying to users or constructing quote requests - The `m0Extension` field identifies tokens that are part of the M protocol ecosystem # TypeScript Type Generation ## Quick Start ```bash npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./m0-swap.d.ts ``` This generates a type definition file with full type coverage for all API endpoints, request parameters, and response objects. ## Generated Types The generated file exports types organized by OpenAPI structure: ```typescript import type { components, operations } from "./m0-swap"; // Schema types (most commonly used) type Asset = components["schemas"]["Asset"]; type Quote = components["schemas"]["Quote"]; type Order = components["schemas"]["Order"]; type OrdersResponse = components["schemas"]["OrdersResponse"]; type OrderStatus = components["schemas"]["OrderStatus"]; type Route = components["schemas"]["Route"]; type Chain = components["schemas"]["Chain"]; type Payload = components["schemas"]["Payload"]; type EvmPayload = components["schemas"]["EvmPayload"]; type SvmPayload = components["schemas"]["SvmPayload"]; // Operation types (for request/response typing) type QuoteRequest = operations["quote_quote"]["requestBody"]["content"]["application/json"]; type QuoteResponse = operations["quote_quote"]["responses"]["200"]["content"]["application/json"]; ``` ## Usage with fetch Create a typed wrapper for API calls: ```typescript import type { components } from "./m0-swap"; type Quote = components["schemas"]["Quote"]; type Asset = components["schemas"]["Asset"]; type Order = components["schemas"]["Order"]; type OrdersResponse = components["schemas"]["OrdersResponse"]; type Chain = components["schemas"]["Chain"]; const API_BASE = "https://gateway.m0.xyz/v1/orchestration"; export async function getQuotes(params: { route: { source: { chain: string; address: string }; destination: { chain: string; address: string }; }; amountIn: string; sender: string; recipient?: string; maxNumQuotes?: number; }): Promise { const response = await fetch(`${API_BASE}/quote`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify(params), }); return response.json(); } export async function getSupportedAssets(): Promise { const response = await fetch(`${API_BASE}/supported-assets`, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } export async function getOrders(params?: { sender?: string; status?: "CREATED" | "COMPLETED" | "CANCELLED"; originChainId?: number; destChainId?: number; limit?: number; offset?: number; }): Promise { const query = new URLSearchParams(); if (params?.sender) query.set("sender", params.sender); if (params?.status) query.set("status", params.status); if (params?.originChainId !== undefined) query.set("originChainId", String(params.originChainId)); if (params?.destChainId !== undefined) query.set("destChainId", String(params.destChainId)); if (params?.limit !== undefined) query.set("limit", String(params.limit)); if (params?.offset !== undefined) query.set("offset", String(params.offset)); const queryString = query.toString(); const url = queryString ? `${API_BASE}/orders?${queryString}` : `${API_BASE}/orders`; const response = await fetch(url, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } export async function getOrder( originChain: Chain, orderId: string, ): Promise { const response = await fetch(`${API_BASE}/orders/${originChain}/${orderId}`, { headers: { "x-api-key": YOUR_API_KEY }, }); return response.json(); } ``` ## Type Aliases For convenience, create a types file that re-exports commonly used types: ```typescript // types/m0-swap.ts import type { components } from "./m0-swap.d.ts"; export type Asset = components["schemas"]["Asset"]; export type Quote = components["schemas"]["Quote"]; export type Order = components["schemas"]["Order"]; export type OrderSummary = components["schemas"]["OrderSummary"]; export type OrdersResponse = components["schemas"]["OrdersResponse"]; export type OrderStatus = components["schemas"]["OrderStatus"]; export type Fill = components["schemas"]["Fill"]; export type Route = components["schemas"]["Route"]; export type AssetAddress = components["schemas"]["AssetAddress"]; export type Payload = components["schemas"]["Payload"]; export type TransactionPayload = components["schemas"]["TransactionPayload"]; export type EvmPayload = components["schemas"]["EvmPayload"]; export type SvmPayload = components["schemas"]["SvmPayload"]; export type Chain = components["schemas"]["Chain"]; export type ChainRuntime = components["schemas"]["ChainRuntime"]; export type Providers = components["schemas"]["Providers"]; export type ProvidersParams = components["schemas"]["ProvidersParams"]; ``` Then import from your alias file: ```typescript import type { Quote, Asset, EvmPayload } from "./types/m0-swap"; ``` ## Keeping Types Updated Regenerate types when the API changes: ```bash # Add to package.json scripts { "scripts": { "generate:types": "openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./src/types/m0-swap.d.ts" } } ``` ```bash pnpm generate:types ``` ## openapi-typescript Options Common options for customization: ```bash # Output to specific directory npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./src/types/api.d.ts # Generate with immutable types (readonly) npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./api.d.ts --immutable # Export types (for re-exporting) npx openapi-typescript https://gateway.m0.xyz/v1/orchestration/openapi.json -o ./api.d.ts --export-type ``` See the [openapi-typescript documentation](https://openapi-ts.dev/){rel=""nofollow""} for more options. ## API Reference For interactive schema exploration and testing, visit the [API Reference](https://gateway.m0.xyz/v1/orchestration/docs/reference){rel=""nofollow""}. # Protocol API The Protocol API is a GraphQL API for querying on-chain data: token supplies, holder balances, rewards accrual, earner status, and protocol configuration parameters. ## Endpoint ```text https://protocol-api.m0.org/graphql ``` An [interactive GraphQL playground](https://protocol-api.m0.org/graphql){rel=""nofollow""} is available at the same URL for exploring the schema and testing queries. ## Authentication The M0 Protocol API requires authentication via API keys to access its endpoint. This ensures that only authorized users can interact with the API and access its data. [Contact us](https://www.m0.org/contact-us){rel=""nofollow""} to obtain an API key. ### Using API Keys To authenticate your requests, include the API key in the `Authorization` header of your HTTP requests. For example post with curl: ```sh curl -i \ --request POST \ --header "Content-Type: application/json" \ --header "Authorization: ApiKey " \ --data '{"query":"{ __typename }"}' \ https://protocol-api.m0.org/graphql ``` API keys should be kept secret and not exposed in public repositories or client-side code. If you believe your API key has been compromised, please contact us immediately to revoke the key and issue a new one. ### Storing API Keys Securely To protect your API key and prevent unauthorized access, follow these best practices: #### Restrict Access by Origin and IP Configure your API key to limit where it can be used from: - **IP Allowlisting**: Restrict the API key to specific IP addresses or CIDR ranges that correspond to your backend servers - **Origin Restrictions**: If applicable, limit the domains or origins that can make requests using your API key Contact us to configure these restrictions for your API key. This adds an additional layer of security, ensuring that even if your key is compromised, it cannot be used from unauthorized locations. #### Use a Backend Service Consider implementing a backend service that: - Stores the API key securely in environment variables or a secrets manager - Acts as a proxy between your frontend and the M0 Protocol API - Handles authentication on behalf of your users This approach ensures that your API key is never transmitted to or accessible from the client. ## Query Recipes Common GraphQL queries are documented as ready-to-use recipes: - [Network Supply](https://docs.m0.org/api-reference/protocol/recipes/network-supply) - [Token Overview](https://docs.m0.org/api-reference/protocol/recipes/token-overview) - [Token Holders](https://docs.m0.org/api-reference/protocol/recipes/token-holders) - [Daily Yields](https://docs.m0.org/api-reference/protocol/recipes/daily-yields) - [Collateral Composition](https://docs.m0.org/api-reference/protocol/recipes/collateral-composition) - [Earner Rate History](https://docs.m0.org/api-reference/protocol/recipes/earner-rate-history) - [Minter Daily Expenses](https://docs.m0.org/api-reference/protocol/recipes/minter-daily-expenses) - [Protocol Configuration](https://docs.m0.org/api-reference/protocol/recipes/protocol-config) # Collateral Composition Use this query to retrieve current collateral stats, daily averages, and time-series grouped by remaining term and on-chain tokens. ```graphql query CollateralComposition( $from: String! $to: String! $customGroups: [CustomGroupInput!] ) { CollateralCurrent { eligibleTreasuries nonEligibleTreasuries remainingTerm totalCash totalTreasuries yieldToMaturity totalTokenCollateral eligibleTokenCollateral nonEligibleTokenCollateral } TreasuriesTimeSeries: CollateralTimeSeries( from: $from to: $to groupBy: "remainingTerm" customGroups: $customGroups ) { definitions { name } groups { name datapoints { absoluteAmount date relativeAmount averageYieldToMaturity } } } OnChainCollateralTimeSeries: CollateralTimeSeries( from: $from to: $to groupBy: "token" ) { definitions { name } groups { name datapoints { absoluteAmount date relativeAmount nav yieldToMaturity tokenBalance } } } } ``` ## Parameters - **from**: start time (e.g., "2025-01-01") - **to**: end time (e.g., "2025-10-01") - **customGroups**: optional array of groups to bucket remaining terms ### Example `customGroups` ```json { "name": "Short-term", "key": "remainingTerm", "values": ["0-1Y", "1-3Y"] } ``` You can pass multiple groups to create custom buckets for the `remainingTerm` series. # Daily Yields This guide outlines how to retrieve daily yield accrual data for various stablecoins using our GraphQL API. Depending on the stablecoin, yield values can be queried as aggregated data for specific stablecoins or for individual holders. ## Aggregated Yield Data by Stablecoin For most of our stablecoins, we provide aggregated yield and supply data per chain. This includes chain-specific queries with a prefix for other stablecoins. The snippets below use **Metamask USD (mUSD)** as an example. You can find all supported stablecoins and chain prefixes below ### Aggregated data ```graphql # Fetches aggregated data for mUSD on Mainnet, Linea, and BSC query GetMusdStablecoin { mainnet: musd_mainnet_stablecoins { id claimed minted accruedYield supply lastUpdate } linea: musd_linea_stablecoins { id claimed minted accruedYield supply lastUpdate } bsc: musd_bsc_stablecoins { id claimed minted accruedYield supply lastUpdate } } ``` You can also get daily snapshots for yield and supply for these stablecoins. ### Daily Yield Snapshots ```graphql # Fetches daily yield snapshots for mUSD on Mainnet query GetMusdDailyYields { musd_mainnet_yieldStats_collection(interval: day) { id timestamp amount blockNumber } } ``` ### Daily Supply Snapshots ```graphql # Fetches daily supply snapshots for mUSD on Mainnet query GetMusdDailySupply { musd_mainnet_supplyStats_collection(interval: day) { id timestamp amount blockNumber } } ``` ::note You can find more supported queries in the interactive [GraphQL Playground documentation](https://protocol-api.m0.org/graphql){rel=""nofollow""}. :: ### Supported Stablecoins and Prefixes The following table lists the stablecoins that support prefixed queries and the corresponding prefixes for each chain. | Stablecoin | Ticker | Prefixes | | :----------- | :-------- | :---------------------------------------------- | | USDhl | USDHL | `usdhl_hyperevm_` | | Braid Dollar | USDZ | `usdz_mainnet_`, `usdz_arbitrum_` | | Dfns Rewards | 0fns | `dfns_mainnet_` | | Startale USD | USDSC | `usdsc_soneium_` | | MANTRA USD | mantraUSD | `mantrausd_mantra_` | | Metamask USD | mUSD | `musd_mainnet_`, `musd_linea_`, `musd_binance_` | | Daylight USD | GRID | `grid_plasma_` | ## Yield Snapshots on Ethereum For stablecoins like Noble Dollar (USDN), Wrapped `$M` (`$wM`), Usual (UsualM), and USDai, use the `MHolderSnapshots` and `MHolders` queries that return yield information for `$M` Earners on **Ethereum**. ### For Noble Dollar (USDN) This query returns daily snapshots of accrued yield for a given USDN holder. You can specify a starting date with the `from` parameter. If `from` is omitted, it defaults to the last 30 days. ```graphql # Fetches daily yield snapshots for a USDN holder query UsdnYieldSnapshots($address: String!, $from: String) { MHolderSnapshots(address: $address, from: $from) { balance accruedYield timestamp principal } } ``` **Example:** Get accrued yield snapshots for a USDN holder since April 1st, 2025. ```json { "address": "0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16", "from": "2025-04-01" } ``` To get only the current values for USDN holders, you can use the `MHolders` query. ```graphql # Fetches current yield values for the top 100 USDN holders query GetUsdnHolders { MHolders(first: 100) { address balance accruedYield } } ``` ### For Wrapped `$M` (`$wM`) / Usual (UsualM) This query returns daily snapshots of accrued yield for a given wM holder. Like the previous query, it supports the `from` parameter and defaults to the last 30 days if omitted. ```graphql # Fetches daily yield snapshots for a wM holder query WmYieldSnapshots($address: String!, $from: String) { WMHolderSnapshots(address: $address, from: $from) { balance accruedYield timestamp principal } } ``` **Example:** Get accrued yield snapshots for a wM holder since April 1st, 2025. ```json { "address": "0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470", "from": "2025-04-01" } ``` ### For USDai on Arbitrum This query retrieves current yield values for USDai holders on Arbitrum. ```graphql # Fetches current yield values for USDai holders on Arbitrum query GetUsdaiArbitrumData { WMHoldersArbitrum { address balance accruedYield claimedYield unclaimedYield } } ``` ## Yield Snapshots on Solana Use `MSolanaYieldSnapshots` to retrieve daily yield snapshots for `$M` earners on Solana. ```graphql # Fetches daily yield snapshots for all $M earners on Solana query GetMSolanaDailyYields { MSolanaYieldSnapshots { address dayTimestamp balance accruedYield earned } } ``` The `address` and date filters are optional. For date ranges, use `day_timestamp_gte` and `day_timestamp_lte`. ```graphql # Fetches daily yield snapshots for a specific holder in a time range query GetMSolanaDailyYieldsForAddress( $address: String! $from: String! $to: String! ) { MSolanaYieldSnapshots( address: $address day_timestamp_gte: $from day_timestamp_lte: $to orderDirection: desc ) { address dayTimestamp balance accruedYield earned fromTs } } ``` All timestamps are in seconds. - `address`: is the earner's Solana address - `dayTimestamp`: timestamp for the day entry - `timestamp`: timestamp at which the `earned` value got calculated - `earned`: yield accrual between `fromTs` to `timestamp` value - `accruedYield`: total earnings **Example holder:** [USDKY `BVo3...eVjc`](https://solscan.io/account/BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc){rel=""nofollow""} ```json { "address": "BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc", "from": "2025-01-01", "to": "2025-01-31" } ``` ### Supported Solana earners Currently, the query above will provide information for the following earners | Earner | Vault's token account | | :----- | :------------------------------------------- | | USDKY | BVo36cZqxD6KUJGhHPZvBDPbe1Q5fR7ekYDj1mbReVjc | | USDK | EHNaRY1ZdtaoPVMqE3TW6pacACzEoU1e9V1ToLyavowN | | wM | 7upNeuSPSpinN7zzEsrxMe6p3N6tMub67dkkm5LFBTvp | | USD+ | 67GWo9en5KtPfeZn5LWrCyhL2w31rf1oqeCoszPUKsAs | | XO | GeBTfYujk4JLr2MBCZznMTLTGch1QrUyYcMKen4KUFU2 | # Earner Rate History This query will return snapshots of the `$M` earner rate ```graphql query EarnerRateSnapshotsQuery { rates: mToken_latestRateSnapshots( orderDirection: desc orderBy: timestamp first: 4 ) { value timestamp } } ``` Example output: ```json { "data": { "rates": [ { "value": "407", "timestamp": "1747235135" }, { "value": "415", "timestamp": "1747223375" }, { "value": "415", "timestamp": "1747221779" }, { "value": "415", "timestamp": "1747215359" } ] } } ``` # Minter Daily Expenses This query will return the daily expenses of the Minter identified by `address`. The date range starts at `from` until the current time. ```graphql query MinterExpensesQuery($address: String!, $from: String) { MinterExpenses(address: $address, from: $from) { amount timestamp date: timestampAsDate } } ``` ## Example Get the Minter expenses for MXON `0x7F7489582b64ABe46c074A45d758d701c2CA5446` since January 1st, 2025. variables: ```json { "address": "0x7F7489582b64ABe46c074A45d758d701c2CA5446", "from": "01 Jan 2025" } ``` When `from` is not specified, it defaults to the last 30 days. # Network Supply Use this query to get the current network supply and the reserves (collateral) of the M0 Protocol. ::note Heads up! supply is currently only available on Ethereum. See Limitations below. :: ```graphql query NetworkSupplyQuery($from: String!, $to: String!) { supply: totalOwedMs(orderBy: blockTimestamp, orderDirection: desc, first: 1) { amount } collateral: CollateralTimeSeriesGroupByType(from: $from, to: $to) { eligibleTotal } } ``` Example output with `from` and `to` variables set to the last "2025-10-03": ```json { "data": { "supply": [ { "amount": "779753099463256" } ], "collateral": [ { "eligibleTotal": 797237488334889 } ] } } ``` As you noticed, the collateral query is a time series query, so you can get the collateral information for a specific time range. For detailed breakdowns of collateral by remaining term and on-chain tokens, see [Collateral composition](https://docs.m0.org/api-reference/protocol/recipes/collateral-composition). ### Limitations Note that network supply is currently only available for Ethereum through the Protocol API. Support for other chains where `$M` is present (Arbitrum, Hyperliquid, etc.) is coming soon. # Protocol Configuration Get historical values for protocol configuration. Current values are featured in the [Governance app](https://governance.m0.org/config/protocol){rel=""nofollow""} too. The configuration includes the following keys: - `update_collateral_interval` - `update_collateral_threshold` - `penalty_rate` - `mint_delay` - `mint_ttl` - `mint_ratio` - `minter_freeze_time` - `base_minter_rate` - `max_earner_rate` - `guidance` - `minter_rate_model` - `earner_rate_model` To query them all: ```graphql query ProtocolConfigQuery { protocolConfigs { id value key blockTimestamp } } ``` Example output: ```json { "data": { "protocolConfigs": [ { "id": "0x25b657dfec5d4c889e0b4050224deaaa00c3707590a7e46c536d6218d673078846010000", "value": "108000", "key": "update_collateral_interval", "blockTimestamp": "1715827991" }, { "id": "0x2637ef87d12653c1d02fff79ad1de0ead33bf13e6f8c475b5efb15bc8547f74ea1000000", "value": "5", "key": "penalty_rate", "blockTimestamp": "1715828219" }, { "id": "0x268e7cf3cfe0d5c4e8b8b62cbeb8ae36497cb7c3f3f75d2d788150e1c97b407d2a010000", "value": "10800", "key": "mint_ttl", "blockTimestamp": "1715828387" } ] } } ``` Use filters to narrow down data. E.g., filter by `max_earner_rate` ```graphql query ProtocolConfigQuery { protocolConfigs(where: { key: "max_earner_rate" }) { id value key blockTimestamp } } ``` # Token Holders This guide explains how to query holder information across different chains in the M0 ecosystem. Each chain features M0-powered stablecoins that are included in the holders response. ## Ethereum Query current `$M` holders on Ethereum with their balances and accrued yields: ```graphql query MHoldersQuery { MHolders(first: 100) { address balance accruedYield } } ``` ### Featured Stablecoins on Ethereum The holders response includes the following M0-powered stablecoins: - **`$M` (Wrapped)** (WM) - `0x437cc33344a0b27a429f795ff6b469c72698b291` - **Metamask USD** (mUSD) - `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` - **Noble** (USDN) - `0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16` - **Usual** (UsualM) - `0x4Cbc25559DbBD1272EC5B64c7b5F48a2405e6470` ## Arbitrum Query current `$M` holders on Arbitrum with their balances and accrued yields: ```graphql query WMHoldersArbitrumQuery { WMHoldersArbitrum(first: 100) { address balance accruedYield claimedYield unclaimedYield } } ``` ### Featured Stablecoins on Arbitrum The holders response includes the following M0-powered stablecoins: - **USDai** (USDai) - `0x83Ae82Bd4054e815fB7B189C39D9CE670369ea16` ## Hyperliquid Query a specific `$M` holder on Hyperliquid: ```graphql query HyperliquidHolderQuery { hyperliquid_holder { address isEarning accruedYield } } ``` ### Featured Stablecoins on Hyperliquid The holders response includes the following M0-powered stablecoins: - USDhl (USDhl) - `0xb50A96253aBDF803D85efcDce07Ad8becBc52BD5` ## Linea Query a specific `$M` holder on Linea: ```graphql query LineaHolderQuery { linea_holders { address isEarning accruedYield } } ``` ### Featured Stablecoins on Linea The holders response includes the following M0-powered stablecoins: - **Metamask USD** (mUSD) - `0xacA92E438df0B2401fF60dA7E4337B687a2435DA` ## Solana Coming soon. # Token Overview This guide explains how to retrieve token data for M0 protocol tokens. The API provides metadata, holders, and supply information for multiple tokens including: - **`$M`** - **POWER** - **ZERO** ## Query Multiple Tokens Use the `tokens` query to retrieve metadata, supply data, and holder information for all tokens: ```graphql query TokensOverview { tokens { id name symbol decimals totalSupplys(first: 2, orderBy: blockTimestamp, orderDirection: desc) { value blockTimestamp } holders(first: 10, orderBy: balance, orderDirection: desc) { address balance } } } ``` This query returns: - Token metadata (name, symbol, decimals, contract address) - Historical total supply snapshots - Top token holders and their balances Use the `token` query to retrieve data for a specific token: ```graphql query TokenOverview($id: String!) { token(id: $id) { id name symbol decimals } } ``` ## Important: Indexer Data Lag The supply data returned by the indexer represents the last-seen state and may not reflect the absolute latest on-chain data. There can be a delay between when a transaction occurs on-chain and when the indexer processes it. ## Querying Supply Directly from Chain For the most up-to-date supply data, query the blockchain directly via RPC: ```typescript // Using viem, ethers.js or web3.js const totalSupply = await tokenContract.totalSupply(); ``` Note that the method to retrieve the total supply may vary between contracts. Before querying the blockchain directly, review the contract's ABI and documentation to ensure you're using the correct method. You can find our token contracts on [Deployments](https://docs.m0.org/resources/addresses). # Deployments The M0 platform and TTG were deployed to Ethereum Mainnet on May 7th 2024. Wrapped M was deployed to Ethereum Mainnet on Aug 14th 2024. ## Ethereum | Contract | Address | ABI | Repository | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://etherscan.io/address/0x866a2bf4e572cbcf37d5071a7a58503bfb36be1b){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x866a2bf4e572cbcf37d5071a7a58503bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | MinterGateway | [0xf7a27FE3579C420575a09D279f78CA6a3E5e75D0](https://etherscan.io/address/0xf7a27FE3579C420575a09D279f78CA6a3E5e75D0){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xf7a27FE3579C420575a09D279f78CA6a3E5e75D0#code#L1){rel=""nofollow""} | [MinterGateway](https://github.com/m0-foundation/protocol/blob/main/src/MinterGateway.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | StandardGovernor | [0xd0C5194740cDb03ee44c45Aa36E3C2e40aC95e39](https://etherscan.io/address/0xd0C5194740cDb03ee44c45Aa36E3C2e40aC95e39){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xd0C5194740cDb03ee44c45Aa36E3C2e40aC95e39#code#L1){rel=""nofollow""} | [StandardGovernor](https://github.com/m0-foundation/ttg/blob/main/src/StandardGovernor.sol){rel=""nofollow""} | | EmergencyGovernor | [0x911301f8f6CBf40dB7f5B0FCF99B70ec4d84c744](https://etherscan.io/address/0x911301f8f6CBf40dB7f5B0FCF99B70ec4d84c744){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x911301f8f6CBf40dB7f5B0FCF99B70ec4d84c744#code#L1){rel=""nofollow""} | [EmergencyGovernor](https://github.com/m0-foundation/ttg/blob/main/src/EmergencyGovernor.sol){rel=""nofollow""} | | ZeroGovernor | [0xf0c1e9583AEcb62eb3ad0D16E00e27bF2a5a58a2](https://etherscan.io/address/0xf0c1e9583AEcb62eb3ad0D16E00e27bF2a5a58a2){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xf0c1e9583AEcb62eb3ad0D16E00e27bF2a5a58a2#code#L1){rel=""nofollow""} | [ZeroGovernor](https://github.com/m0-foundation/ttg/blob/main/src/ZeroGovernor.sol){rel=""nofollow""} | | POWER Token | [0x1d3BabC63FC67AaC5D5d3C60ffE38aD35720Cd1f](https://etherscan.io/address/0x1d3BabC63FC67AaC5D5d3C60ffE38aD35720Cd1f){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x1d3BabC63FC67AaC5D5d3C60ffE38aD35720Cd1f#code#L1){rel=""nofollow""} | [PowerToken](https://github.com/m0-foundation/ttg/blob/main/src/PowerToken.sol){rel=""nofollow""} | | ZERO Token | [0x006FCe274745Ff662017785223d1ACdd0CBBf410](https://etherscan.io/address/0x006FCe274745Ff662017785223d1ACdd0CBBf410){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x006FCe274745Ff662017785223d1ACdd0CBBf410#code#L1){rel=""nofollow""} | [ZeroToken](https://github.com/m0-foundation/ttg/blob/main/src/ZeroToken.sol){rel=""nofollow""} | | DistributionVault | [0xaEb70A80DDFD8b4b5c0A5E5e85F67FcC1Faab27F](https://etherscan.io/address/0xaEb70A80DDFD8b4b5c0A5E5e85F67FcC1Faab27F){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xaEb70A80DDFD8b4b5c0A5E5e85F67FcC1Faab27F#code#L1){rel=""nofollow""} | [DistributionVault](https://github.com/m0-foundation/ttg/blob/main/src/DistributionVault.sol){rel=""nofollow""} | | Hub Portal | [0xF81b4Bec6Ca8f9fe7bE01CA734F55B2b6e03A7a0](https://etherscan.io/address/0xF81b4Bec6Ca8f9fe7bE01CA734F55B2b6e03A7a0){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xF81b4Bec6Ca8f9fe7bE01CA734F55B2b6e03A7a0#code#L1){rel=""nofollow""} | [HubPortal](https://github.com/m0-foundation/m-portal/blob/main/src/HubPortal.sol){rel=""nofollow""} | | Wormhole Transceiver | [0x0763196A091575adF99e2306E5e90E0Be5154841](https://etherscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841){rel=""nofollow""} | [ABI](https://etherscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Arbitrum | Contract | Address | ABI | Repository | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://arbiscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://arbiscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://arbiscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://arbiscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Transceiver | [0x0763196A091575adF99e2306E5e90E0Be5154841](https://arbiscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841#code#L1){rel=""nofollow""} | | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://arbiscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://arbiscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://arbiscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Base | Contract | Address | ABI | Repository | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://basescan.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://basescan.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://basescan.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://basescan.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://basescan.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://basescan.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://basescan.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://basescan.org/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Transceiver | [0x0763196A091575adF99e2306E5e90E0Be5154841](https://basescan.org/address/0x0763196A091575adF99e2306E5e90E0Be5154841){rel=""nofollow""} | [ABI](https://basescan.org/address/0x0763196A091575adF99e2306E5e90E0Be5154841#code#L1){rel=""nofollow""} | | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://basescan.org/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://basescan.org/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://basescan.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://basescan.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Optimism | Contract | Address | ABI | Repository | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://optimistic.etherscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://optimistic.etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://optimistic.etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://optimistic.etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Wormhole Transceiver | [0x0763196A091575adF99e2306E5e90E0Be5154841](https://optimistic.etherscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0x0763196A091575adF99e2306E5e90E0Be5154841#code#L1){rel=""nofollow""} | | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://optimistic.etherscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://optimistic.etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://optimistic.etherscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Linea | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://lineascan.build/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://lineascan.build/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://lineascan.build/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://lineascan.build/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://lineascan.build/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://lineascan.build/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://lineascan.build/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://lineascan.build/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://lineascan.build/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## BNB | Contract | Address | ABI | Repository | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://bscscan.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://bscscan.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://bscscan.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://bscscan.com/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://bscscan.com/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://bscscan.com/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://bscscan.com/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://bscscan.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://bscscan.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## HyperEVM | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://hyperevmscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://hyperevmscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://hyperevmscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://hyperevmscan.io/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [Portal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/Portal.sol){rel=""nofollow""} | | Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://hyperevmscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [Vault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/Vault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://hyperevmscan.io/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://hyperevmscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://hyperevmscan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Plume | Contract | Address | ABI | Repository | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.plume.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.plume.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://explorer.plume.org/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://explorer.plume.org/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://explorer.plume.org/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://explorer.plume.org/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://explorer.plume.org/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.plume.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.plume.org/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Mantra | Contract | Address | ABI | Repository | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://blockscout.mantrascan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://blockscout.mantrascan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://blockscout.mantrascan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://blockscout.mantrascan.io/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://blockscout.mantrascan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://blockscout.mantrascan.io/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://blockscout.mantrascan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://blockscout.mantrascan.io/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Soneium | Contract | Address | ABI | Repository | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://soneium.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://soneium.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://soneium.blockscout.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://soneium.blockscout.com/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://soneium.blockscout.com/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://soneium.blockscout.com/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://soneium.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | | Uniswap V3 Adapter | [0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b](https://soneium.blockscout.com/address/0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b){rel=""nofollow""} | [ABI](https://soneium.blockscout.com/address/0x023bd2F0A95373C55FC8D1c5F8e60cC3B9Bc4f4b#code#L1){rel=""nofollow""} | | ## Plasma | Contract | Address | ABI | Repository | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://plasmascan.to/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://plasmascan.to/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://plasmascan.to/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://plasmascan.to/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://plasmascan.to/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://plasmascan.to/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://plasmascan.to/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://plasmascan.to/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Citrea | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.mainnet.citrea.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b#code#L1){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.mainnet.citrea.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291#code#L1){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://explorer.mainnet.citrea.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c#code#L1){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE](https://explorer.mainnet.citrea.xyz/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE#code#L1){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Spoke Vault | [0x3349e443068F76666789C4f76F00D9c4F38A4DdE](https://explorer.mainnet.citrea.xyz/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x3349e443068F76666789C4f76F00D9c4F38A4DdE#code#L1){rel=""nofollow""} | [SpokeVault](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokeVault.sol){rel=""nofollow""} | | Bridge | [0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3](https://explorer.mainnet.citrea.xyz/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0x51DcE104E5ba88fabC19A2C519f955bb834b0DC3#code#L1){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.mainnet.citrea.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.mainnet.citrea.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278#code#L1){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## 0G | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://chainscan.0g.ai/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://chainscan.0g.ai/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://chainscan.0g.ai/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://chainscan.0g.ai/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Bridge | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://chainscan.0g.ai/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://chainscan.0g.ai/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://chainscan.0g.ai/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Fluent | Contract | Address | ABI | Repository | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://fluentscan.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://fluentscan.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://fluentscan.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://fluentscan.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Bridge | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://fluentscan.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=contract-viewer){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://fluentscan.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://fluentscan.xyz/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Moca | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://moca-mainnet.cloud.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract-viewer){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://moca-mainnet.cloud.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract-viewer){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://moca-mainnet.cloud.blockscout.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract-viewer){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://moca-mainnet.cloud.blockscout.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract-viewer){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Bridge | [0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd](https://moca-mainnet.cloud.blockscout.com/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0xaCffEC28C4eEe21C889a4e6C0704c540Ed9D4fDd?tab=contract-viewer){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://moca-mainnet.cloud.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://moca-mainnet.cloud.blockscout.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract-viewer){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Monad | Contract | Address | ABI | Repository | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://monadvision.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=Contract#contract-abi){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://monadvision.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=Contract#contract-abi){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://monadvision.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://monadvision.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=Contract#contract-abi){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://monadvision.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=Contract#contract-abi){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Bridge | [0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32](https://monadvision.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xfCc1d596Ad6cAb0b5394eAa447d8626813180f32?tab=Contract#contract-abi){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://monadvision.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://monadvision.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=Contract#contract-abi){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Rise | Contract | Address | ABI | Repository | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `$M` Token | [0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b](https://explorer.risechain.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b?tab=contract_abi){rel=""nofollow""} | [MToken](https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol){rel=""nofollow""} | | Wrapped `$M` Token | [0x437cc33344a0B27A429f795ff6B469C72698B291](https://explorer.risechain.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x437cc33344a0B27A429f795ff6B469C72698B291?tab=contract_abi){rel=""nofollow""} | [WrappedMToken](https://github.com/m0-foundation/wrapped-m-token/blob/v2/src/WrappedMToken.sol){rel=""nofollow""} | | Registrar | [0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c](https://explorer.risechain.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c?tab=contract_abi){rel=""nofollow""} | [Registrar](https://github.com/m0-foundation/ttg/blob/main/src/Registrar.sol){rel=""nofollow""} | | Spoke Portal | [0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd](https://explorer.risechain.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0xD925C84b55E4e44a53749fF5F2a5A13F63D128fd?tab=contract_abi){rel=""nofollow""} | [SpokePortal](https://github.com/m0-foundation/m-portal-lite/blob/main/src/SpokePortal.sol){rel=""nofollow""} | | Bridge | [0x77EF4e9D37524069f81890C537a5c5d390bb4b4d](https://explorer.risechain.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0x77EF4e9D37524069f81890C537a5c5d390bb4b4d?tab=contract_abi){rel=""nofollow""} | | | Swap Facility | [0xB6807116b3B1B321a390594e31ECD6e0076f6278](https://explorer.risechain.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278){rel=""nofollow""} | [ABI](https://explorer.risechain.com/address/0xB6807116b3B1B321a390594e31ECD6e0076f6278?tab=contract_abi){rel=""nofollow""} | [SwapFacility](https://github.com/m0-foundation/evm-m-extensions/blob/main/src/orchestration/SwapFacility.sol){rel=""nofollow""} | ## Solana ### Mainnet | Program/Token | Address | Explorer | Repository | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | M Token | [mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo](https://explorer.solana.com/address/mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzerokyEX9TNDoK4o2YZQBDmMzjokAeN6M2g2S3pLJo){rel=""nofollow""} | [solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} | | Wrapped M Token | [mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp){rel=""nofollow""} | [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} | | Portal | [mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY){rel=""nofollow""} | | | Transceiver | [J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm){rel=""nofollow""} | [View](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm){rel=""nofollow""} | | | Quoter | [Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ){rel=""nofollow""} | [View](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ){rel=""nofollow""} | | ### Devnet | Program/Token | Address | Explorer | Repository | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | M Token | [mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6](https://explorer.solana.com/address/mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroZRGCah3j5xEWp2Nih3GDejSBbH1rbHoxDg8By6?cluster=devnet){rel=""nofollow""} | [solana-m](https://github.com/m0-foundation/solana-m){rel=""nofollow""} | | Wrapped M Token | [mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzeroXDoBpRVhnEXBra27qzAMdxgpWVY3DzQW7xMVJp?cluster=devnet){rel=""nofollow""} | [solana-m-extensions](https://github.com/m0-foundation/solana-m-extensions){rel=""nofollow""} | | Portal | [mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY?cluster=devnet){rel=""nofollow""} | | | Transceiver | [J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/J1bVGcwG3nPsAJsi3GFNqC9NZmKatSuoutPbaKMiT7Bm?cluster=devnet){rel=""nofollow""} | | | Quoter | [Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ?cluster=devnet){rel=""nofollow""} | [View](https://explorer.solana.com/address/Nqd6XqA8LbsCuG8MLWWuP865NV6jR1MbXeKxD4HLKDJ?cluster=devnet){rel=""nofollow""} | | --- ## API Reference ::card-group :::card --- icon: i-lucide-code title: API Reference to: https://docs.m0.org/api-reference/introduction --- Explore the M0 API for querying protocol data, token metrics, and on-chain activity. ::: :: # Audits ## M0 Platform and TTG Smart Contracts The M0 platform and TTG smart contracts went through a series of extensive audits by industry-leading security companies. | Auditor | Date | Report | | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Quantstamp | Jan 2024 - March 2024 | [Quantstamp\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Quantstamp%20Audit%20Report.pdf){rel=""nofollow""} | | Three Sigma | Jan 2024 - March 2024 | [ThreeSigma\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/ThreeSigma%20Audit%20Report.pdf){rel=""nofollow""} | | Certora | Jan 2024 - March 2024 | [Certora\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Certora%20Audit%20report.pdf){rel=""nofollow""} | | Chainsecurity | Jan 2024 - March 2024 | [Chainsecurity\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/ChainSecurity%20Audit%20Report.pdf){rel=""nofollow""} | | OpenZeppelin | Jan 2024 - March 2024 | [OpenZeppelin report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/OpenZeppelin%20Audit%20Report.pdf){rel=""nofollow""} | | Prototech Labs | Jan 2024 - Feb 2024 | [PrototechLabs\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Prototech%20Labs%20Audit%20Report.pdf){rel=""nofollow""} | | Kirill Fedoseev | Dec 2023 - April 2024 | [Kirill Fedoseev report.md](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Kirill%20Fedoseev%20Independent%20Auditor%20Report.md){rel=""nofollow""} | | Sherlock | March 2024 - April 2024 | [Sherlock\_report.pdf](https://github.com/m0-foundation/documentation/blob/main/protocol-audit-reports/Sherlock%20Audit%20Report.pdf){rel=""nofollow""} | ## Wrapped M (wM) Wrapped M (wM) went through a series of audits in summer 2024. | Auditor | Date | Report | | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Chainsecurity | July 2024 - Aug 2024 | [ChainSecurity Wrapped `$M` Audit Report.pdf](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/ChainSecurity%20Wrapped%20M%20Audit%20Report.pdf){rel=""nofollow""} | | Three Sigma | July 2024 - Aug 2024 | [Three Sigma Wrapped `$M` Audit Report.pdf](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/ThreeSigma%20Wrapped%20M%20Audit%20Report.pdf){rel=""nofollow""} | | Kirill Fedoseev | July 2024 - Aug 2024 | [Kirill Fedoseev Wrapped `$M` Audit Report.md](https://github.com/m0-foundation/documentation/blob/main/wrapped-M-audit-reports/Kirill%20Fedoseev%20Audit%20Report.md){rel=""nofollow""} | ## EVM M0-Extensions EVM M0-Extensions, smart contract extensions for the M0 platform, went through comprehensive audits in 2025. | Auditor | Date | Report | | ------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Certora | July 2025 | [Certora\_MExtension\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/Certora%20MExtension%20Security%20Assessment%20Final%20Report.pdf){rel=""nofollow""} | | ChainSecurity | July 2025 | [ChainSecurity\_MExtensions\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/ChainSecurity_M0_M_Extensions_audit_draft.pdf){rel=""nofollow""} | | Guardian | August 2025 | [Guardian\_MExtensions\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/Guardian%20Audits%20M0%20Extensions%20Report%20Aug%205.pdf){rel=""nofollow""} | ## Solana M0-Extensions Solana M0-Extensions, the Solana implementation of M0 platform stablecoin extensions, underwent thorough security audits in 2025. | Auditor | Date | Report | | -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Halborn | June 2025 | [Halborn\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/halborn_m_extensions_audit_report.pdf){rel=""nofollow""} | | Adevar | July 2025 | [Adevar\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/adevar_m_extensions_audit_report.pdf){rel=""nofollow""} | | Ottersec | July 2025 | [Ottersec\_SolanaExtensions\_report.pdf](https://github.com/m0-foundation/solana-m-extensions/blob/main/audits/ottersec_m_extensions_audit_report.pdf){rel=""nofollow""} | ## M0 Portal Lite M0 Portal Lite, the lightweight cross-chain bridge solution, was audited by leading security firms in 2025. | Auditor | Date | Report | | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Three Sigma | April 2025 | [ThreeSigma\_PortalLite\_report.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/Three%20Sigma%20-%20M0PortalLite.pdf){rel=""nofollow""} | | ChainLight | May 2025 | [ChainLight\_PortalLite\_report.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/ChainLight%20-%20M%20Portal%20Lite%20Security%20Audit%20v1.0.pdf){rel=""nofollow""} | | ChainSecurity | September 12, 2025 | [ChainSecurity\_M0\_M\_Portal\_Lite\_audit.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/ChainSecurity_M0_M_Portal_Lite_audit.pdf){rel=""nofollow""} | | Guardian Audits | August 15, 2025 | [GuardianAudits\_M0\_PortalLite\_audit.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/GuardianAudits_M0_PortalLite_audit.pdf){rel=""nofollow""} | | Halborn | October 27, 2025 | [Halborn - M Portal Lite 10-27-25.pdf](https://github.com/m0-foundation/m-portal-lite/blob/main/audits/Halborn%20-%20M%20Portal%20Lite%2010-27-25.pdf){rel=""nofollow""} | ## Solana M0 Solana M0, the core M0 platform implementation on Solana, received comprehensive security reviews in 2025. | Auditor | Date | Report | | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Halborn | March 2025 | [Halborn\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/halborn_solana_m_audit.pdf){rel=""nofollow""} | | OtterSec | April 2025 | [OtterSec\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/ottersec_solana_m_audit.pdf){rel=""nofollow""} | | Sec3 | May 2025 | [Sec3\_SolanaM\_report.pdf](https://github.com/m0-foundation/solana-m/blob/main/audits/sec3_solana_m_audit_report.pdf){rel=""nofollow""} | ## mUSD mUSD, an M0 stablecoin extension designed for MetaMask, is an upgradeable ERC20 token contract derived from`MYieldToOne`, designed to wrap into a non-rebasing token, where all accrued rewards is claimable by a single designated recipient, underwent comprehensive security audits in August 2025. | Auditor | Date | Report | | ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ChainSecurity | August 26, 2025 | [ChainSecurity\_M0\_MUSD\_audit.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/ChainSecurity_M0_MUSD_audit.pdf){rel=""nofollow""} | | Consensys Diligence | August 2025 | [ConsensysDiligence\_M0\_MUSD\_audit.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/ConsensysDiligence_M0_MUSD_audit.pdf){rel=""nofollow""} | | Guardian Audits | August 15, 2025 | [GuardianAudits\_M0\_MUSD\_report.pdf](https://github.com/m0-foundation/mUSD/blob/main/audits/GuardianAudits_M0_MUSD_report.pdf){rel=""nofollow""} | | Kirill Fedoseev | August 6, 2025 | [MZero-review-report-v1-private.md](https://github.com/m0-foundation/mUSD/blob/main/audits/MZero-review-report-v1-private.md){rel=""nofollow""} | ## Limit Order Protocol The Limit Order Protocol is the core component of the M0 liquidity delivery system, enabling users to submit same-chain or cross-chain limit orders to exchange one token for another. The protocol underwent comprehensive security audits by leading firms. | Auditor | Date | Report | | --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ChainSecurity | January 22, 2026 | [chainsecurity\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/phase_one/chainsecurity_report.pdf){rel=""nofollow""} | | Guardian Audits | January 30, 2026 | [guardian\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/phase_one/guardian_report.pdf){rel=""nofollow""} | | Halborn | January 14, 2026 | [halborn\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/phase_one/halborn_report.pdf){rel=""nofollow""} | | Sherlock | December 16, 2025 | [sherlock\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/phase_one/sherlock_report.pdf){rel=""nofollow""} | | Adevar | January 23, 2026 | [adevar\_report.pdf](https://github.com/m0-foundation/liquidity-delivery/blob/main/audits/phase_two/adevar_report.pdf){rel=""nofollow""} | ## Multichain V2 Multichain V2, the next generation of M0's cross-chain infrastructure, underwent comprehensive security audits by leading firms. | Auditor | Date | Report | | --------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Adevar | January 21, 2026 | [Adevar\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Adevar_M0_Portal_V2.pdf){rel=""nofollow""} | | ChainSecurity | February 24, 2026 | [ChainSecurity\_M0\_Portal\_V2\_Liquidity\_Delivery.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/ChainSecurity_M0_Portal_V2_Liquidity_Delivery.pdf){rel=""nofollow""} | | Guardian Audits | January 30, 2026 | [Guardian\_M0\_Portal\_V2\_Liquidity\_Delivery.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Guardian_M0_Portal_V2_Liquidity_Delivery.pdf){rel=""nofollow""} | | Halborn | January 16, 2026 | [Halborn\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Halborn_M0_Portal_V2.pdf){rel=""nofollow""} | | Sherlock | December 19, 2025 | [Sherlock\_M0\_Portal\_V2.pdf](https://github.com/m0-foundation/m-portal-v2/blob/main/evm/audits/Sherlock_M0_Portal_V2.pdf){rel=""nofollow""} | ## JMI (Just Mint It) JMI (Just Mint It), a multi-collateral extension for M tokens, underwent security audits in late 2025. | Auditor | Date | Report | | --------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Guardian Audits | December 12, 2025 | [M0\_EVM-M\_Extensions\_Review\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/JMI/M0_EVM-M_Extensions_Review_report.pdf){rel=""nofollow""} | | Sherlock | November 13, 2025 | [Sherlock\_JMI\_report.pdf](https://github.com/m0-foundation/evm-m-extensions/blob/main/audits/JMI/2025_12_10_Final_M0_Collaborative_Audit_Report_1765332345.pdf){rel=""nofollow""} | # Glossary #### Earner A holder or distributor of M0-powered stablecoins whose address is approved to earn the Earner Rate. #### Earner Rate The annualized percentage paid in the Earn Mechanism. #### Eligible Collateral A description of portfolio composition which can be placed in Eligible Custody Solutions and be used to generate an onchain Collateral Value, which is subsequently used to mint M0-powered stablecoins. #### Eligible Custody Solution A description of entity structures, jurisdictions, contractual agreements, and other details that will suffice for the custody of Eligible Collateral. #### Mint Delay A period of time between when a Issuer has called Propose Mint and when they can first call Mint. It serves as a protective measure to ensure all actors have sufficient time to audit each Propose Mint. #### Mint Ratio The fraction size of a Issuer's onchain Collateral Value that they can mint. #### Issuer #### Issuer An institution that connects to the protocol to generate and manage stablecoin supply. #### Issuer Freeze Time A period of time during which an Issuer will be disabled to call Propose Mint and Mint methods. Can be called by any Validator on any Issuer by passing the Issuer address as the argument of the Freeze method. It can be called multiple times on the same Issuer to reset the Issuer Freeze Time window. If Freeze is called during an already existing Issuer Freeze Time, the Issuer Freeze Time window will restart from the beginning. #### Issuer Rate The annualized percentage charged continuously to Issuers on their outstanding debt. #### Penalty Rate The percentage charged for missing Update Collateral interval(s) or being undercollateralized on outstanding debt that is in excess of the amount a Issuers is permitted to have generated. It is assessed any time Accrue Penalty is called, which is embedded in both Update Collateral and Burn. This is a fixed percentage and not an annualized rate. #### Periphery Contract A smart contract that adds supplement functionality, but exists outside of the core M0 protocol. #### `POWER` One of the two utility tokens used in the M0 TTG. It is used to vote on active proposals and can be considered the primary management token of the mechanism. `POWER` holders will earn `ZERO` in exchange for their direct participation in governance. #### Propose Mint Time To Live The amount of time after the Mint Delay during which a Issuer can complete the mint operation before Propose Mint expires. It serves as a protective measure to ensure that Issuers cannot call Propose Mint and then execute the Mint at a much later date. #### TTG TTG stands for Two Token Governor and is a mechanism by which holders of the voting tokens are penalized for failing to vote. There are two utility tokens used in the M0 TTG: `POWER` and `ZERO`. #### Update Collateral Interval This amount of time is the period between which Update Collateral must be called by a Issuer. If they do not call Update Collateral within this amount of time after their previous call, their onchain Collateral Value is set to 0 and they will incur Penalty Rate on the next update. #### Validator An independent entity that provides timely information about the offchain collateral being used to back M0-powered stablecoins. #### ZERO One of the two utility tokens used in the M0 TTG, is earned by `POWER` holders in exchange for their direct participation in governance. The `ZERO` holders are the recipient of all remaining value that is not paid out to the Earn Mechanism for their participation in protocol governance. The anticipated accumulation of tokens to the `ZERO` holders are Proposal Fee payments from proposal submission, the payments from `POWER` token auctions, and a portion of Issuer Rate and Penalty Rate charges to Issuers. # LLMs.txt M0 docs provide machine-readable text files that AI tools can consume to understand the M0 protocol, its terminology, and technical documentation. ## What is LLMs.txt? [LLMs.txt](https://llmstxt.org){rel=""nofollow""} is a standard format that provides website content in a way optimized for large language models. Instead of parsing HTML, AI tools can read structured markdown that contains the essential information. ## Available Routes | Route | Description | Size | | ------------------------- | ------------------------------------------------------- | -------------- | | `/llms.txt` | Overview of all documentation sections with page links | \~5K tokens | | `/llms-full.txt` | Complete documentation content — all pages concatenated | \~200K+ tokens | | `/llms/get-started.txt` | Get Started section only | Smaller | | `/llms/launch-guide.txt` | Launch Guide section only | Smaller | | `/llms/protocol-docs.txt` | Protocol Docs section only | Smaller | | `/llms/resources.txt` | Resources section only | Smaller | ## Choosing the Right File - Use **`/llms.txt`** when you need a quick overview or want to find the right section to dive into. - Use **section-specific files** (e.g., `/llms/protocol-docs.txt`) when you know which area you need and want to keep token usage low. - Use **`/llms-full.txt`** when you need comprehensive context across all M0 documentation. This is large — only use it with tools that support large context windows. ## Usage with AI Tools ### Cursor Add as a documentation source in your project's `.cursor/rules` file: ```text @https://docs.m0.org/llms-full.txt ``` Or reference a specific section: ```text @https://docs.m0.org/llms/protocol-docs.txt ``` ### Windsurf Add to your project rules or reference the URL directly in chat: ```text Refer to https://docs.m0.org/llms-full.txt for M0 documentation ``` ### ChatGPT, Claude, and Other Chat Tools Paste the URL directly in your conversation: ```text Read https://docs.m0.org/llms.txt and answer my questions about M0 ``` ::note For the best experience with M0 documentation, use the [MCP Server](https://docs.m0.org/agents/mcp-server) instead of LLMs.txt. MCP allows your AI assistant to search and fetch specific pages on demand rather than loading everything at once. :: # MCP Server M0 docs include a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io){rel=""nofollow""} server that lets AI assistants query, search, and navigate the documentation programmatically. ## What is MCP? MCP is an open protocol that standardizes how AI applications connect to external data sources and tools. By connecting your AI assistant to the M0 MCP server, it gains direct access to all M0 documentation — no copy-pasting required. ## Available Tools The M0 MCP server exposes the following tools: ### Documentation Tools | Tool | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | | `list-pages` | Lists all documentation pages with title, path, and description | | `get-page` | Retrieves the full markdown content of a specific page by path | | `search-docs` | Full-text search across all documentation pages | | `get-section` | Get all pages in a specific section (get-started, build, issuers, protocol, api-reference, resources, agents) | ### Reference Tools | Tool | Description | | --------------- | ------------------------------------------------------ | | `get-glossary` | Look up M0 protocol terminology definitions | | `get-addresses` | Look up contract addresses across all supported chains | ## Configuration Add the M0 MCP server to your AI assistant: ::tabs :::tabs-item{label="Claude Code"} ```bash claude mcp add --transport http m0-docs https://docs.m0.org/mcp ``` ::: :::tabs-item{label="Claude Desktop"} Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "m0-docs": { "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="Cursor"} Add to your `.cursor/mcp.json`: ```json { "mcpServers": { "m0-docs": { "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="VS Code"} Add to your `.vscode/mcp.json`: ```json { "servers": { "m0-docs": { "type": "http", "url": "https://docs.m0.org/mcp" } } } ``` ::: :::tabs-item{label="Windsurf"} Add to your `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "m0-docs": { "serverUrl": "https://docs.m0.org/mcp" } } } ``` ::: :: ## Usage Examples Once connected, you can ask your AI assistant questions like: - "Search M0 docs for how minting works" - "Look up the M token address on Base" - "What does TTG mean in M0?" - "Show me all protocol documentation pages" - "Get the full content of the getting started guide" The assistant will use the appropriate MCP tools to fetch accurate, up-to-date information directly from the docs. # Skills Skills are pre-built instruction sets that give AI coding agents specialized knowledge about M0. They provide context about the protocol, smart contracts, integration patterns, and best practices — so you can build on M0 without needing to explain everything from scratch. ## What are Skills? Skills are markdown-based instruction files that AI coding tools load as context. They contain: - Protocol architecture and terminology - Smart contract interfaces and addresses - Integration patterns and code examples - Best practices and common pitfalls When installed, your AI assistant can help you write M0 integration code, debug contract interactions, and follow M0 conventions automatically. ## Available Skills | Skill | Description | Download | | ---------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | M0 Protocol | Core protocol concepts, M token, minting/burning, TTG governance | [m0-protocol.md](https://docs.m0.org/skills/m0-protocol.md){rel=""nofollow""} | | Stablecoin Integration | Building with Wrapped M, extension patterns, cross-chain bridging | [m0-stablecoin-integration.md](https://docs.m0.org/skills/m0-stablecoin-integration.md){rel=""nofollow""} | ## Installation ::tabs :::tabs-item{label="Claude Code"} Install skills by downloading them into your project's `.claude/skills/` directory: ```bash mkdir -p .claude/skills/m0-protocol curl -o .claude/skills/m0-protocol/SKILL.md https://docs.m0.org/skills/m0-protocol.md mkdir -p .claude/skills/m0-stablecoin-integration curl -o .claude/skills/m0-stablecoin-integration/SKILL.md https://docs.m0.org/skills/m0-stablecoin-integration.md ``` Claude Code will automatically pick up skills from `.claude/skills/` in your project directory. ::: :::tabs-item{label="Cursor"} Add M0 skills as context in your `.cursor/rules` file: ```text @https://docs.m0.org/skills/m0-protocol.md @https://docs.m0.org/skills/m0-stablecoin-integration.md ``` Cursor will use these files as context when you ask questions about M0. ::: :::tabs-item{label="Other AI Tools"} For tools that support custom instructions or system prompts, reference the skill files directly: - **Direct URL**: Add `https://docs.m0.org/skills/m0-protocol.md` as a context source - **LLMs.txt**: Use the [LLMs.txt](https://docs.m0.org/agents/llms-txt) files for broader documentation coverage - **MCP Server**: Connect via the [MCP Server](https://docs.m0.org/agents/mcp-server) for on-demand, searchable access ::: :: ## Usage Once installed, you can ask your AI assistant to: - "Create a contract that wraps M tokens" - "How do I integrate M0 minting into my dApp?" - "What's the correct way to call proposeMint()?" - "Set up cross-chain M token bridging" - "What are the steps to get earner approval for my extension?" The assistant will use its M0 knowledge to provide accurate, context-aware code and guidance.