Skip to content

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 \approx 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} \approx 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} \approx 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 Padé 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.