Skip to content

User Yield Model

Core Concept: Yield is automatically distributed to all token holders, creating a yield-bearing stablecoin. An optional protocol fee can be configured to capture a percentage for the admin.

When to Use:
  • DeFi protocols offering yield-bearing stablecoins to users
  • Consumer wallets with built-in savings accounts
  • GameFi economies where users are rewarded for holding
  • Applications prioritizing user incentives while optionally capturing protocol revenue

EVM Implementation: MYieldFee

The MYieldFee extension is the ideal model for creating a user-facing, yield-bearing stablecoin. It allows you to build a custom token where yield continuously accrues to all holders, while giving you, the protocol developer, the ability to take a small, configurable fee on that yield.

This model is perfect for dApps, wallets, and DeFi protocols that want to offer a compelling, native stablecoin that shares the underlying $M yield with its users, creating a powerful incentive for adoption and liquidity.

Source Code:

Architecture and Mechanism

MYieldFee uses a sophisticated continuous indexing mechanism to distribute yield fairly and efficiently to all token holders.

Roles (Access Control)

MYieldFee defines three key management roles:

  • DEFAULT_ADMIN_ROLE: The root administrator, capable of granting and revoking other roles.
  • FEE_MANAGER_ROLE: Controls the protocol's revenue. This role can set the feeRate (0-10,000 bps) and the feeRecipient address.
  • CLAIM_RECIPIENT_MANAGER_ROLE: Can redirect a user's yield claims to another address via setClaimRecipient(). Useful for features like auto-compounding vaults or delegating rewards.

How It Works: Continuous Indexing

  1. Principal vs. Balance: When a user wraps $M, their token balance is recorded, along with a principal value. Think of principal as their "share" of the total pool.
  2. The Growing Index: The contract maintains a currentIndex which starts at 1e12 (EXP_SCALED_ONE) and grows over time based on the $M yield rate (minus the protocol fee).
  3. Accruing Yield: A user's "true" balance at any time is calculated using IndexingMath.getPresentAmountRoundedDown(principal, currentIndex). Since the index is always increasing, their effective balance grows continuously. The accruedYieldOf(account) is the difference between this true balance and their last claimed balance.
  4. Claiming: When a user calls claimYieldFor(account), their balance is updated to the current true value, effectively "cashing in" their accrued yield.
  5. The Fee: The protocol fee is what's left over. The totalAccruedFee() is calculated as the contract's total $M balance minus the projected total supply owed to all users. This surplus can be claimed by anyone via claimFee(), which mints new tokens to the feeRecipient.

L1 vs. L2 (MSpokeYieldFee)

The mechanism has a slight variation for L2 deployments.

  • On L1 (Ethereum): MYieldFee uses block.timestamp to calculate the currentIndex, as yield accrues continuously.
  • On L2s (MSpokeYieldFee): To prevent over-printing tokens due to the delay in $M's index propagation from L1, MSpokeYieldFee uses a RateOracle. The index growth is capped by the latestUpdateTimestamp from the bridged $M token on the L2, ensuring its growth never outpaces the underlying asset.

Key Interface & Functions

Storage Layout

The state management is more complex to handle the indexing logic.

struct MYieldFeeStorageStruct {
    uint256 totalSupply;
    uint112 totalPrincipal;
    uint128 latestIndex;
    uint16 feeRate;
    address feeRecipient;
    uint40 latestUpdateTimestamp;
    uint32 latestRate;
    bool isEarningEnabled;
    mapping(address account => uint256 balance) balanceOf;
    mapping(address account => uint112 principal) principalOf;
    mapping(address account => address claimRecipient) claimRecipients;
}

Core Yield Functions

  • accruedYieldOf(address account) external view: Returns the amount of unclaimed yield for a specific user. Calculated as the difference between their present amount (based on principal and current index) and their recorded balance.
  • claimYieldFor(address account) external: Claims the accrued yield for a user, updating their balance. Can be called by anyone on behalf of the user. If a claim recipient is set, the yield is automatically transferred to that address. Emits YieldClaimed and Transfer events.
  • totalAccruedFee() external view: Shows the total fee amount available for the protocol to claim. Calculated as the contract's $M balance minus the projected total supply.
  • claimFee() external: Mints the accrued fee amount and sends it to the feeRecipient. Returns the amount claimed. Can be called by anyone.

Indexing Functions

These functions are the heart of the yield distribution mechanism.

  • currentIndex() external view: Calculates the current, up-to-the-second index value using continuous indexing math. This is the core of the yield calculation.
  • earnerRate() external view: Returns the effective yield rate for token holders, which is the underlying $M rate minus the protocol's feeRate: (ONE_HUNDRED_PERCENT - feeRate) * _currentEarnerRate() / ONE_HUNDRED_PERCENT.
  • updateIndex() external: Function that updates the index by refreshing the latestIndex, latestRate, and latestUpdateTimestamp in storage. This is called automatically during key operations and returns the updated index.

Management Functions

  • setFeeRate(uint16 feeRate_) external: Callable by the FEE_MANAGER_ROLE to adjust the protocol's take rate (in basis points, 0-10,000). Updates the index if earning is enabled.
  • setFeeRecipient(address feeRecipient_) external: Callable by the FEE_MANAGER_ROLE to change the treasury address. Claims any existing fees for the previous recipient first.
  • setClaimRecipient(address account, address claimRecipient) external: Callable by the CLAIM_RECIPIENT_MANAGER_ROLE to redirect yield claims. Claims any existing yield for the account before making the change.

Earning Control Functions

  • enableEarning() external: Starts earning yield on the held $M. Updates the index and calls IMTokenLike(mToken).startEarning(). Reverts if earning is already enabled.
  • disableEarning() external: Stops earning yield. Updates the index, resets earning state, and calls IMTokenLike(mToken).stopEarning(address(this)). Reverts if earning is already disabled.

View Functions

  • balanceWithYieldOf(address account) external view: Returns the user's current balance plus any unclaimed yield.
  • principalOf(address account) external view: Returns the user's principal amount.
  • projectedTotalSupply() external view: Shows what the total supply would be if all yield was claimed.
  • totalAccruedYield() external view: Shows the total unclaimed yield for all users.
  • totalPrincipal() external view: Returns the total principal of all users.
  • claimRecipientFor(address account) external view: Returns the claim recipient for an account (defaults to the account itself if none set).
  • feeRate() external view: Returns the current fee rate in basis points.
  • feeRecipient() external view: Returns the current fee recipient address.
  • isEarningEnabled() external view: Returns whether earning is currently enabled.
  • latestIndex() external view: Returns the last stored index.
  • latestRate() external view: Returns the last stored rate.
  • latestUpdateTimestamp() external view: Returns the timestamp of the last index update.

Constants

  • ONE_HUNDRED_PERCENT: Set to 10,000 (representing 100% in basis points)
  • FEE_MANAGER_ROLE: keccak256("FEE_MANAGER_ROLE")
  • CLAIM_RECIPIENT_MANAGER_ROLE: keccak256("CLAIM_RECIPIENT_MANAGER_ROLE")

MSpokeYieldFee Specific

For L2 deployments, MSpokeYieldFee adds:

  • rateOracle() external view: Returns the address of the rate oracle used to get the earner rate.

Ready to build? Follow the implementation guide to deploy your MYieldFee extension.

SVM Implementation: ScaledUi

The ScaledUi extension is the most seamless way to create a yield-bearing stablecoin for end-users on Solana. It uses the scaled-ui mechanism to automatically and efficiently distribute yield to all token holders.

This model is perfect for DeFi protocols, consumer wallets, and any application that wants to offer a compelling stablecoin where user balances grow automatically in their wallets without requiring any transactions or claims.

Source Code: The ScaledUi model is compiled from the m_ext program using the scaled-ui Rust feature flag. The source code is available in the solana-m-extensions repository.

Architecture and Mechanism

The ScaledUi model combines the yield-generating power of $M with the scaled-ui mechanism to create a truly native, yield-bearing asset. The scaled-ui variant can have an optional fee that goes to the admin.

Roles (Access Control)

  • admin: The root administrator, responsible for initializing the contract and managing wrap_authorities.
  • wrap_authority: An address authorized by the admin to perform wrap and unwrap operations, typically a trusted swap program.
  • Keeper/Anyone: The core sync function is permissionless, allowing any user or automated keeper bot to call it to keep the yield index up to date.

How It Works: Onchain Indexing & Scaled-UI

  1. Initialization: The extension is deployed with the scaled-ui mechanism enabled.
  2. Wrapping: Users wrap $M via an authorized wrap_authority and receive the extension token. The underlying $M is held in the contract's vault (m_vault). Extensions automatically sync on wrap/unwrap.
  3. Yield Accrual: The m_vault is registered as an earner in the base $M program, and its $M balance begins to grow as yield accrues.
  4. Permissionless Sync: Anyone can call the sync() instruction, or bridge an index update. This is the engine of the yield distribution.
  5. Index Calculation: The sync() function reads the current index from the base $M earn program and compares it to the last synced index stored in the extension's global_account. It calculates the yield accrued in the vault during that period.
  6. Mint Update: Using this new yield data, the program calculates the new effective scaled-ui rate for the extension token.
  7. Automatic Balance Updates: Wallets and explorers will display a higher balance for all holders, reflecting the accrued yield based on the scaled-ui mechanism. The user's "principal" balance (amount) doesn't change, but their "UI amount" grows with each sync.
  8. Optional Fee: If configured, a fee can be charged that goes to the admin.

This mechanism is incredibly efficient, as a single sync transaction updates the yield for every token holder simultaneously.

Key Interface & Functions

The model's logic is powered by the generic M Extension program, configured to operate in a rebasing mode.

Storage Layout

The ExtGlobal account stores the configuration and state, with YieldConfig being crucial for tracking the index synchronization.

// State structure from m_ext program
pub struct ExtGlobal {
    pub admin: Pubkey,
    pub ext_mint: Pubkey,             // The extension mint using scaled-ui
    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<Pubkey>,
}
 
pub struct YieldConfig {
    pub variant: YieldVariant,        // Set to YieldVariant::ScaledUi
    pub last_m_index: u64,            // Tracks the last synced index from the base M program
    pub last_ext_index: u64,          // Tracks the extension's own index
}

Core Yield Function

  • sync(): A permissionless instruction that updates the yield index. It reads the state of the base $M earn program and the extension's m_vault balance. It then calculates the new scaled-ui rate. The ext_mint account is writable in this instruction, which is essential for this mechanism to work. Index updates can also be bridged, and extensions automatically sync on wrap/unwrap operations.

Management Functions

  • initialize(wrap_authorities: Vec<Pubkey>): Sets up the extension's global state with the ScaledUi 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.
  • set_fee(fee_bps: u64): Set optional fee in basis points (goes to admin, typically 0 for full pass-through).

Standard M Extension Functions

  • wrap(amount: u64): Wraps $M into the extension token. Callable by a whitelisted wrap_authority.
  • unwrap(amount: u64): Unwraps the extension token back into $M. The amount specified is the principal amount; the user receives the principal plus any accrued yield. Callable by a whitelisted wrap_authority.

Error Handling

  • Unauthorized: Thrown if a non-authorized address attempts a privileged action.
  • InvalidMint: Ensures the correct mints are being used in wrap/unwrap.
  • MathOverflow / MathUnderflow: Protects against errors during the complex index calculations in the sync function.

Ready to build? Follow the implementation guide to deploy your ScaledUi extension.