Skip to content

Institutional Model

Core Concept: Three-tier permissioned architecture where an admin delegates authority to Earn Managers, who manage their own sets of earners with custom fee arrangements.

When to Use:
  • Institutional platforms serving multiple client organizations
  • B2B services requiring bespoke yield-sharing agreements
  • Prime brokerages with per-client fee structures
  • Platforms needing granular control over who can hold and earn yield

EVM Implementation: MEarnerManager

The MEarnerManager extension is a powerful model designed for institutional, B2B, and permissioned use cases. It provides granular, per-account control over who can hold the token and what share of the yield they receive.

This model is the perfect choice for fintech platforms, prime brokerages, or any application serving a limited number of clients who require bespoke, onchain enforced fee structures. Its core feature is a whitelist: only approved addresses can interact with the token.

Source Code: MEarnerManager.sol

Architecture and Mechanism

The architecture of MEarnerManager revolves around a central manager role that curates a list of approved "earners" with individual fee arrangements.

Roles (Access Control)

  • DEFAULT_ADMIN_ROLE: The contract administrator, responsible for granting and revoking roles.
  • EARNER_MANAGER_ROLE: The gatekeeper and controller of the extension's economics. This role is responsible for:
    • Whitelisting and de-listing accounts via setAccountInfo
    • Setting a custom feeRate for each whitelisted account (0-10,000 basis points)
    • Setting the global feeRecipient address that collects all fees

How It Works

  1. Whitelisting is Mandatory: Before any address can hold, receive, wrap, unwrap, approve, or transfer the token, it must be added to the whitelist by the EARNER_MANAGER_ROLE via the setAccountInfo function.
  2. Custom Fee Rates: When an account is whitelisted, the manager assigns it a specific feeRate (from 0% to 100%). This allows for different commercial agreements with different clients.
  3. Earning Control: The contract can only enable earning once using enableEarning(). Once disabled, it cannot be re-enabled (enforced by the EarningCannotBeReenabled error).
  4. Yield Accrual: The contract uses a principal-based system similar to MYieldFee. Each user has a principal amount and their yield is calculated based on the difference between their present amount and current principal balance.
  5. On-Demand Claiming: Yield for a specific user is only realized when claimFor(account) is called. This can be called by anyone on behalf of the whitelisted account.
  6. Fee Splitting: The claimFor function calculates the total yield for the account since its last claim, then splits it:
    • The user receives Total Yield * (1 - feeRate)
    • The feeRecipient receives Total Yield * feeRate

This mechanism provides ultimate control and ensures that all token interactions are confined to a known set of participants.

Key Interface & Functions

Storage Layout

The storage is designed around the Account struct, keeping each user's data neatly organized.

// Each whitelisted user has an Account struct
struct Account {
    uint256 balance;        // Current token balance
    bool isWhitelisted;     // Whitelist status
    uint16 feeRate;        // Fee rate in basis points (0-10,000)
    uint112 principal;     // Principal amount for yield calculation
}
 
struct MEarnerManagerStorageStruct {
    address feeRecipient;
    uint256 totalSupply;
    uint112 totalPrincipal;
    bool wasEarningEnabled;
    uint128 disableIndex;
    mapping(address account => Account) accounts;
}

Management Functions (The Control Panel)

These are the primary functions used by the EARNER_MANAGER_ROLE.

  • setAccountInfo(address account, bool status, uint16 feeRate) external: The main function to manage the whitelist. It can add an account, remove it, or update its feeRate. Claims yield for the account before making changes. Reverts with InvalidAccountInfo if trying to set a non-zero fee rate for a non-whitelisted account.
  • setAccountInfo(address[] calldata accounts, bool[] calldata statuses, uint16[] calldata feeRates) external: A batch version for managing multiple accounts in one transaction. Reverts with ArrayLengthMismatch if array lengths don't match or ArrayLengthZero if arrays are empty.
  • setFeeRecipient(address feeRecipient_) external: Sets or updates the single address that will receive all collected fees. The fee recipient is automatically whitelisted with a 0% fee rate.

Core Yield & Claim Functions

  • claimFor(address account) external: The function to trigger a yield claim and fee split for a specific whitelisted account. Returns (yieldWithFee, fee, yieldNetOfFee). Can be called by anyone.
  • claimFor(address[] calldata accounts) external: Batch version that claims yield for multiple accounts. Returns arrays of yield amounts, fees, and net yields.

Earning Control Functions

  • enableEarning() external: Starts earning yield on the held $M. Can only be called once - if earning was previously enabled, it reverts with EarningCannotBeReenabled.
  • disableEarning() external: Stops earning yield and records the disable index. Reverts with EarningIsDisabled if already disabled.

View Functions

  • isWhitelisted(address account) external view: A simple check to see if an account is on the approved list. This is used as a gate for all transfers, wraps, and unwraps.
  • feeRateOf(address account) external view: Returns the custom fee rate for a specific account.
  • principalOf(address account) external view: Returns the principal amount for an account.
  • accruedYieldAndFeeOf(address account) external view: Calculates the current claimable yield, the fee portion, and the net yield for an account without executing the claim. Returns (yieldWithFee, fee, yieldNetOfFee).
  • accruedYieldOf(address account) external view: Returns only the net yield after fees for an account.
  • accruedFeeOf(address account) external view: Returns only the fee portion of the yield for an account.
  • balanceWithYieldOf(address account) external view: Returns the account's current balance plus accrued net yield.
  • feeRecipient() external view: Returns the current fee recipient address.
  • totalPrincipal() external view: Returns the total principal of all accounts.
  • projectedTotalSupply() external view: Shows what the total supply would be if all yield was claimed.
  • disableIndex() external view: Returns the index when earning was disabled (0 if never disabled).
  • wasEarningEnabled() external view: Returns whether earning was ever enabled.
  • isEarningEnabled() external view: Returns whether earning is currently enabled (was enabled and not disabled).
  • currentIndex() external view: Returns the current index for yield calculations. Uses the disable index if earning is disabled, otherwise uses the M token's current index.

Access Control Enforcement

All operations (wrap, unwrap, transfer, approve) enforce that all involved parties are whitelisted:

  • _beforeApprove() - Checks both account and spender are whitelisted
  • _beforeWrap() - Checks earning is enabled and both depositor and recipient are whitelisted
  • _beforeUnwrap() - Checks the account is whitelisted
  • _beforeTransfer() - Checks msg.sender, sender, and recipient are all whitelisted

Constants

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

Error Handling

The contract includes comprehensive error handling:

  • NotWhitelisted(address account): Thrown when a non-whitelisted account tries to interact
  • EarningCannotBeReenabled: Thrown when trying to enable earning after it was already enabled once
  • InvalidFeeRate: Thrown when fee rate exceeds 100%
  • InvalidAccountInfo: Thrown when trying to set non-zero fee rate for non-whitelisted account
  • ZeroAdmin: Thrown when admin address is 0x0
  • ZeroEarnerManager: Thrown when earner manager address is 0x0
  • ZeroFeeRecipient: Thrown when fee recipient address is 0x0
  • ZeroAccount: Thrown when account address is 0x0
  • ArrayLengthMismatch: Thrown when array lengths don't match in batch operations
  • ArrayLengthZero: Thrown when arrays are empty in batch operations

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

SVM Implementation: Crank

The Crank extension model is designed for institutional and B2B platforms that require granular control over who can earn yield and how that yield is distributed. It implements a three-tier permissioning hierarchy where an admin delegates authority to Earn Managers, who in turn manage their own sets of earners.

This model is used by Wrapped M ($wM), the first-party M0 extension on Solana, and serves as the blueprint for any protocol serving institutional clients or requiring bespoke yield-sharing agreements.

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

Migration Note: In V2, $wM migrated from the legacy ext_earn program to the unified m_ext framework using the crank feature flag.

Architecture and Mechanism

The Crank model implements a sophisticated three-tier hierarchy for yield management:

Three-Tier Hierarchy

  1. Admin (Top Level)

    • Root authority for the extension
    • Can add/remove Earn Managers
    • Can set the Earn Authority
    • Manages core protocol configuration
  2. Earn Managers (Middle Tier)

    • Delegated authorities approved by Admin
    • Each manager can add/remove their own earners
    • Can configure custom fee rates (in basis points)
    • Can specify fee recipient token accounts
    • Useful for protocols, DAOs, or institutional partners managing their own communities
  3. Earners (User Level)

    • Individual accounts authorized to earn yield
    • Managed by a specific Earn Manager
    • Can set custom yield recipient addresses
    • Subject to their Earn Manager's fee rate

Roles (Access Control)

  • admin: The root administrator, typically a secure multi-sig or governance contract.
  • earn_authority: A permissioned key that executes the yield distribution crank (typically an offchain bot).
  • earn_manager: An intermediary authority that can manage a subset of earners and charge fees.
  • earner: An individual account approved to hold the extension token and earn yield.
  • wrap_authority: Addresses authorized to perform wrap/unwrap operations (e.g., swap facility).

How It Works: Crank-Based Yield Distribution

Unlike the ScaledUi model where yield is automatically distributed via rebasing, the Crank model uses an offchain calculation + onchain distribution pattern:

Yield Distribution Flow

Step 1: Yield Accrues in Vault

The extension's m_vault holds $M tokens that are registered as earners in the base $M program. As the $M index updates, the vault's balance grows.

Step 2: Offchain Balance Calculation

The earn_authority (typically an offchain bot) calculates the weighted average balance for each earner since their last claim. This ensures yield is distributed fairly even if balances change during the distribution period.

Step 3: Onchain Claiming (The "Crank")

The earn_authority iterates through eligible earners and calls claim_for(earner, calculated_balance) for each one. This instruction:

  • Calculates yield owed based on the snapshot balance
  • Deducts the Earn Manager's fee (if configured)
  • Mints new extension tokens to the earner's recipient account
  • Mints fee tokens to the Earn Manager's fee recipient account
  • Updates the earner's last_index and last_balance

Step 4: Continuous Operation

The crank runs periodically (e.g., daily or weekly) to distribute newly accrued yield to all active earners.

Key State Structures

ExtGlobal Account

The top-level configuration for the extension.

pub struct ExtGlobal {
    pub admin: Pubkey,
    pub earn_authority: Option<Pubkey>,
    pub ext_mint: Pubkey,
    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::Crank
    pub last_m_index: u64,
    pub last_ext_index: u64,
}

EarnManager Account

Represents a delegated authority managing a set of earners.

pub struct EarnManager {
    pub manager: Pubkey,                // The manager's authority
    pub fee_bps: u64,                   // Fee rate in basis points (e.g., 500 = 5%)
    pub fee_token_account: Pubkey,      // Where manager fees are sent
    pub bump: u8,
}

Earner Account

Tracks an individual earner's yield distribution state.

pub struct Earner {
    pub user: Pubkey,                       // The earner's wallet
    pub user_token_account: Pubkey,         // Token account earning yield
    pub recipient_token_account: Pubkey,    // Where yield is sent (can differ)
    pub earn_manager: Pubkey,               // The managing EarnManager PDA
    pub last_balance: u64,                  // Balance at last claim
    pub last_index: u64,                    // Index at last claim
    pub bump: u8,
}

Key Instructions

Admin Instructions

  • add_earn_manager(manager: Pubkey, fee_bps: u64, fee_token_account: Pubkey): Adds a new Earn Manager with specified fee configuration.
  • remove_earn_manager(manager: Pubkey): Removes an Earn Manager (earners must be removed first).
  • set_earn_authority(new_earn_authority: Option<Pubkey>): Sets or updates the earn authority that can execute claims.

Earn Manager Instructions

  • add_earner(user: Pubkey, user_token_account: Pubkey, recipient_token_account: Pubkey): Adds a new earner to this manager's set.
  • remove_earner(user: Pubkey): Removes an earner from this manager's set.
  • configure_earn_manager(new_fee_bps: u64, new_fee_token_account: Pubkey): Updates the manager's fee configuration.
  • transfer_earner(user: Pubkey, new_earn_manager: Pubkey): Transfers an earner to a different Earn Manager.

Earn Authority Instructions

  • claim_for(user: Pubkey, snapshot_balance: u64): Distributes yield to a specific earner based on the calculated snapshot balance.
  • sync(): Updates the extension's index from the base $M program (similar to ScaledUi).

Earner Instructions

  • set_recipient(new_recipient_token_account: Pubkey): Allows an earner to change where their yield is sent. Useful for DeFi integrations where tokens are locked in a contract.

Wrap/Unwrap

  • wrap(amount: u64): Wraps $M into the extension token (requires wrap_authority).
  • unwrap(amount: u64): Unwraps extension token back into $M (requires wrap_authority).

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