Technical Documentation

M Token

Complete technical documentation for the M token, the immutable ERC20-compliant token at the heart of the M0 Ecosystem.

Overview

M is for Money.

$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.

$M interacts closely with several key components of the M0 Protocol:

  • MinterGateway: Controls the total supply through secure mint and burn operations.
  • Rate Models: External smart contracts that dynamically calculate the interest rates for both Minters and Earners.
  • TTGRegistrar: The governance-controlled parameter store that, among other things, maintains the list of approved Earners and the addresses of the active Rate Models.
  • DistributionVault: 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-EarningEarning (allowed by governance)
Function exactly like regular ERC20 tokensAutomatically increase in value over time through continuous compounding
Value remains constant unless explicitly transferredStored internally as "principal amounts" that get multiplied by a growing index
Stored as actual token amounts in the contractInterest 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:principalAmount=amountcurrentIndexprincipalAmount = \frac{amount}{currentIndex}
    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:presentAmount=principalAmount×currentIndexpresentAmount = principalAmount \times currentIndex
  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.

OperationFromToRounding Rule for Amount Conversion to PrincipalProtocol FavoredInternal Function Involved (Illustrative)
Conversions
Start EarningNon-EarnerEarnerPrincipal from present amount rounded DOWNYes_getPrincipalAmountRoundedDown
Stop EarningEarnerNon-EarnerPresent amount from principal rounded DOWN (for the final balance calculation)Yes_getPresentAmountRoundedDown
Transfers
Earner to EarnerEarnerEarnerAmount to principal for sender: rounded UP; for receiver: (principal transferred directly)Sender: Yes_getPrincipalAmountRoundedUp (sender)
Earner to Non-EarnerEarnerNon-EarnerAmount to principal for sender: rounded UPYes_getPrincipalAmountRoundedUp (sender)
Non-Earner to EarnerNon-EarnerEarnerAmount to principal for receiver: rounded DOWNYes_getPrincipalAmountRoundedDown (receiver)
Mint/Burn
Mint to Earner-EarnerAmount to principal rounded DOWNYes_getPrincipalAmountRoundedDown
Burn from EarnerEarner-Amount to principal rounded UPYes_getPrincipalAmountRoundedUp

(Note: The table simplifies complex interactions. Refer to the MToken.sol 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:e(x)1+x2+3x228+x384+x416801x2+3x228x384+x41680e(x) \approx \frac{1 + \frac{x}{2} + \frac{3x^2}{28} + \frac{x^3}{84} + \frac{x^4}{1680}}{1 - \frac{x}{2} + \frac{3x^2}{28} - \frac{x^3}{84} + \frac{x^4}{1680}}
  • 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).
    // 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.
    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.

Copyright © M0 Foundation 2026