M Token
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:
- Non-earning Balances: Standard ERC20-style token balances that remain static over time - non-rebasing amounts do not increase without external transactions.
- 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_EARNERSlist 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
permitfor 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 specifiedaccount_if that account is no longer on the governance-approvedAPPROVED_EARNERSlist. 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:
- The contract first checks if the user is approved as an earner in the M0 Governance system (via the
TTGRegistrar). - The user's current token balance (let's call it
amount) is read from storage. - This
amountis converted to a smaller "principal amount" using the following calculation:
wherecurrentIndexstarted at 1.0 (represented as 1e12) and has been growing continuously based on theEARNER_RATE_MODEL. - The conversion rounds down in favor of the protocol (a tiny fraction may be left, contributing to protocol reserves).
- The user's raw balance in storage (within the
_balancesmapping, specificallyMBalance.rawBalance) is updated to thisprincipalAmount. TheirMBalance.isEarningflag is set totrue. - Global accounting variables are updated:
totalNonEarningSupplyis decreased by the originalamount.principalOfTotalEarningSupplyis increased by theprincipalAmount.
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:
- The contract reads the user's current
principalAmountfrom storage. - This principal is converted back to a "present amount" (actual token value) by multiplying:
- The resulting
presentAmountincludes all interest earned up to that moment. The calculation ofpresentAmountfromprincipalAmountis rounded down (_getPresentAmountRoundedDown). - The user's
rawBalancein storage is updated to thispresentAmount. TheirMBalance.isEarningflag is set tofalse. - Global accounting variables are updated:
totalNonEarningSupplyis increased by thepresentAmount.principalOfTotalEarningSupplyis decreased by theprincipalAmount.
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):
- When they call
startEarning():principalAmount = 1000 / 1.05 ≈ 952.380952(actual value depends on 6 decimals for$Mand 12 decimals for index, then rounded down). Let's say it's952.380952 * 10^6as raw principal.- Their storage value for
rawBalancebecomes this principal. balanceOf()returns(principalAmount * 1.05 * 10^12) / 10^{12} ≈ 1000 * 10^6.
- After time passes and the
currentIndexgrows to 1.08:- Their stored
rawBalance(principal) is still952.380952 * 10^6. balanceOf()now returns(principalAmount * 1.08 * 10^12) / 10^{12} ≈ 1028.571428 * 10^6.
- Their stored
- When they call
stopEarning():presentAmount = (principalAmount * 1.08 * 10^{12}) / 10^{12}(rounded down).- Their storage value for
rawBalancebecomes thispresentAmount. balanceOf()now returns exactly thispresentAmount.
Transfer Mechanics
Transfers in MToken handle accounts with different earning statuses:
- 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.
- 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 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 effectivelyprincipalAmount * currentIndex. - Starting Point: The index starts at 1.0 (represented as
1e12internally due to 12 decimal places of precision for the index) and only increases over time. - Index Storage: The
MTokencontract inherits fromContinuousIndexing, which storeslatestIndex(uint128) andlatestUpdateTimestamp(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^xfunction is implemented onchain using a Pade approximant R(4,4) for gas efficiency and precision: - 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 whatbalanceOf()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
MinterGatewayduring its ownupdateIndex()calls (e.g., duringmintM,burnM,updateCollateral,deactivateMinter). This ensures synchronization between Minter interest payments and Earner yield accrual. - Internally within
MTokenbefore operations that change an account's earning status or modifyprincipalOfTotalEarningSupply, 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,_transferAmountInKindfor earning-to-earning).
Each MToken.updateIndex() call:
- Fetches the current earner rate from the
EARNER_RATE_MODEL(address obtained fromTTGRegistrar). - Calculates the time elapsed since
latestUpdateTimestamp. - Computes the new index:
newIndex = latestIndex * e^(rate * timeElapsed / SECONDS_PER_YEAR). - Updates
latestIndexandlatestUpdateTimestampin storage.
Precision and Efficiency
- Fixed-Point Arithmetic: The index uses 12 decimal places of precision (scaled by
1e12).$Mbalances 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
$Mis exclusively controlled by theMinterGatewaycontract. MToken.mint(address recipient, uint240 amount)can only be called by theminterGatewayaddress set inMToken.MToken.burn(address account, uint240 amount)can only be called by theminterGatewayaddress. This ensures that$Mtokens are only created or destroyed as per the protocol's minting (collateral-backed) and burning (debt-repayment) rules managed byMinterGateway.
Earning Mechanism Governance
- Earner Approval: Whether an account can switch to an earning balance (
startEarning()) is determined by M0 Governance. TheMTokencontract checks if an account is an approved earner by querying theTTGRegistrar(specifically, theAPPROVED_EARNERSlist or ifEARNERS_LIST_IGNOREDis 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 theTTGRegistrarand read byMTokenduringupdateIndex().
Safety Controls for Earners
- The
stopEarning(address account_)function allows anyone to call it for anaccount_that is no longer on theAPPROVED_EARNERSlist. 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 byMinterGatewayto ensure synchronization with Minter-side interest accruals and byMTokenitself 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
$Mtoken system. These small, accumulated amounts act as a buffer and eventually contribute to protocol reserves (e.g., claimable by theDistributionVault). - Explicit overflow checks and the use of libraries like
UIntMathandContinuousIndexingMathensure 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.
M0 Portals
Documentation of M0's cross-chain architecture, including the M Portal (Wormhole) and M Portal Lite (Hyperlane) implementations for bridging M across blockchains.
M Token Specification
Low-level technical specification for the M token - function signatures, events, errors, and storage layout.