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
amount
is converted to a smaller "principal amount" using the following calculation: wherecurrentIndex
started 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
_balances
mapping, specificallyMBalance.rawBalance
) is updated to thisprincipalAmount
. TheirMBalance.isEarning
flag is set totrue
. - Global accounting variables are updated:
totalNonEarningSupply
is decreased by the originalamount
.principalOfTotalEarningSupply
is 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
principalAmount
from storage. - This principal is converted back to a "present amount" (actual token value) by multiplying:
- The resulting
presentAmount
includes all interest earned up to that moment. The calculation ofpresentAmount
fromprincipalAmount
is rounded down (_getPresentAmountRoundedDown
). - The user's
rawBalance
in storage is updated to thispresentAmount
. TheirMBalance.isEarning
flag is set tofalse
. - Global accounting variables are updated:
totalNonEarningSupply
is increased by thepresentAmount
.principalOfTotalEarningSupply
is 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 \approx 952.380952
(actual value depends on 6 decimals for $M and 12 decimals for index, then rounded down). Let's say it's952.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
.
- After time passes and the
currentIndex
grows to 1.08:- Their stored
rawBalance
(principal) is still952.380952 * 10^6
. balanceOf()
now returns(principalAmount * 1.08 * 10^12) / 10^{12} \approx 1028.571428 * 10^6
.
- Their stored
- When they call
stopEarning()
:presentAmount = (principalAmount * 1.08 * 10^{12}) / 10^{12}
(rounded down).- Their storage value for
rawBalance
becomes 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
1e12
internally due to 12 decimal places of precision for the index) and only increases over time. - Index Storage: The
MToken
contract 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^x
function is implemented onchain using a Padé 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
MinterGateway
during its ownupdateIndex()
calls (e.g., duringmintM
,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 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
,_transferAmountInKind
for 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
latestIndex
andlatestUpdateTimestamp
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.