Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · August 2025

M Extensions

for M0

M0 engaged Guardian to review the security of their M Extensions. From the 23rd of June to the 5th of August, a team of 5 auditors reviewed the source code in scope.

Published
Review window
June 23 to August 5, 2025
Language
Solidity
Chains
Ethereum, Arbitrum, Optimism
Sector
Stablecoins
  • 0 Critical
  • 1 High
  • 4 Medium
  • 33 Low
  • 0 Informational

15 resolved · 23 acknowledged

Scope

Overview

M0 engaged Guardian to review the security of their M Extensions. From the 23rd of June to the 5th of August, a team of 5 auditors reviewed the source code in scope.

Findings 38

  1. H-01 High MYieldFee : Yield Accrual Rewards Resolved
    Location
    MYieldFee.sol: 198-199

    Description

    Proof of concept: PoC

    The MYieldFee M Extension currently allows user balances to increase even when earning is disabled, because calling updateIndex resets _latestRate to a non-zero value—bypassing the isEarningEnabled() check in currentIndex.

    As a result, claimYieldFor can inflate user balances even when the extension isn’t receiving yield from the M token, leading to inaccurate accounting and potential insolvency.

    While SwapFacility blocks further actions during such states, M Extensions are designed to function independently and integrate with DeFi—making this behavior risky.

    Examples:

    • Lending protocols: Users could borrow against artificially inflated balances not backed by real yield.
    • DEXs: Balance-based logic (e.g., in minting or routing) could misbehave or be exploited.

    Additionally, if an extension is disabled and later re-enabled, updateIndex would incorrectly assume yield accrued throughout the entire disabled period.

    Recommendation

    Update the updateIndex function to respect the earning status.

    Consider adding

    if (isEarningEnabled()) return $.latestIndex;
    

    Resolution

    M0 Team: Resolved.

  2. M-01 Medium MYieldFee : Extension May Drift, Causing Insolvency Risk Rewards Acknowledged
    Location
    Global

    Description

    Proof of concept: PoC

    The MYieldFee extension mimics the yield accrual curve of the original M Token by independently tracking the earner rate and current index. However, the earner rate on the original M Token can be updated by MinterGateway.

    1. MinterGateway updates collateral.
    2. This triggers a call to mToken.updateIndex().
    3. Inside mToken.updateIndex(), _latestRate is refreshed using rateModel().rate().
    4. earnerRate() of M Token subsequently reflects this new _latestRate.

    Until the extension's updateIndex() function is manually invoked, it will continue operating based on stale parameters. This can create a discrepancy between the extension’s internal index and the actual yield curve of the M Token. Additionally, if mToken.stopEarning is triggered directly (without invoking the extension’s logic), the extension keeps accruing “fake” yield.

    This leads to minting of unbacked extension tokens. Over time, this mismatch can result in excess yield being distributed via the extension. If sustained long enough, and if the extension owner’s fee reserves are insufficient to absorb the difference, it could theoretically lead to insolvency.

    Recommendation

    • Extension owners should be made explicitly aware of the need to call updateIndex() whenever the

    underlying earner rate is updated.

    • Consider building a proactive alerting or notification mechanism to inform extension owners when

    a rate change is about to occur or has occurred.

    • Alternatively, explore automating index updates as part of key interactions or lifecycle hooks to

    minimize reliance on manual upkeep.

    • Ensure that extension.stopEarning() is invoked before or concurrently with mToken.stopEarning() to

    trigger any necessary extension-side logic (such as halting internal index updates).

    Resolution

    M0 Team: Acknowledged.

  3. M-02 Medium Wrong Swap Recipient Set Logical Error Resolved
    Location
    UniswapV3SwapAdapter.sol: 94

    Description

    If a path is given to the swapIn function in the UniswapV3SwapAdapter contract msg.sender is used as recipient instead of the given recipient parameter.

    Recommendation

    Use the given recipient parameter.

    Resolution

    M0 Team: Resolved.

  4. M-03 Medium MEarnerManager: Old Fee Recipient Address Not Reset Logical Error Acknowledged
    Location
    MEarnerManager.sol: 338-351

    Description

    In the MEarnerManager contract the fee recipient gets a fee rate of 0 (does not pay fees). When a new fee recipient is set the old fee recipient account is not reset therefore the old account still pays 0 fees.

    This can lead to loss of yield if not noticed. For example if the fee recipient was changed due to the address being compromised or due the employee with this address left the company etc.

    Recommendation

    Reset the old fee recipient account when a new on is set.

    Resolution

    M0 Team: Acknowledged.

  5. M-04 Medium MEarnerManager: Extraction Of Fake Yield Via DeFi Rewards Resolved
    Location
    MEarnerManager.sol: 169-170

    Description

    Even if earning is disabled for MEarnerManager, it continues to accrue yield as per the currentIndex. Users will no longer be able to exit via the swap facility, since it restricts the use of non-earning tokens in extensions.

    That said, extension tokens are expected to be used independently in DeFi. As a result, users may still exit through other protocols using their token balances, which now include yield accrued after earning was disabled—effectively allowing them to extract fake yield.

    Recommendation

    Consider implementing a snapshot of the index at the time earning is disabled to prevent post-disablement yield accrual from being misused.

    Resolution

    M0 Team: Resolved.

  6. L-01 Low swapInToken Steals Tokens From User Logical Error Resolved
    Location
    SwapFacility.sol: 161

    Description

    The swapInToken function is the main entry point for users to enter an extension. It allows users to trade external tokens (e.g., USDC) for extension tokens, and this core functionality is currently broken.

    The flow differs slightly depending on whether the desired extension token is $wM or another extension token. For non-$wM tokens, the flow looks like this:

    • The user calls the swapInToken function.
    • The external tokens (e.g., USDC) are transferred into the contract.
    • These tokens are swapped for $wM tokens on Uniswap, with the SwapFacility contract—not the

    user—set as the recipient.

    • The _swap function is then called to convert the received $wM tokens into the desired extension

    token using an unwrap/wrap flow that assumes the tokens belong to the user.

    The issue is that _swap attempts to use the user's $wM tokens, even though $wM tokens from the initial swap were sent to the SwapFacility contract. This leads to two potential problems:

    • The call will revert (DoS) if the user doesn't already hold enough $wM.
    • Or, it will steal $wM from the user—causing them to effectively pay twice (e.g., swapping $100

    USDC results in ~$200 worth of user tokens being deducted).

    The M0 team later clarified that this behavior was designed to support the current deployment of $wM, which uses msg.sender in the unwrap function and doesn’t account for SwapFacility as the receiver. They intend to update the $wM implementation in the future to MExtension Interface. Above issue is only applicable consider wM to be one of MExtensions.

    Recommendation

    Be aware of this issue when developing the new $wM implementation.

    Resolution

    M0 Team: Resolved.

  7. L-02 Low Contracts Can't Be Upgraded Upgradeability Resolved
    Location
    Global

    Description

    As observed in tests and confirmed in discussions with the M0 team, UUPS proxies are intended to be used for upgrades. However, UUPS proxies rely on ERC1967Proxy and are not upgradeable by default.

    The implementation contract must include upgrade logic and explicitly authorize upgrades by inheriting from OpenZeppelin’s UUPSUpgradeable and implementing the necessary authorization functions.

    Currently, neither the Extensions nor the SwapFacility contracts implement this logic. As a result, despite using a UUPS proxy structure, these contracts are not actually upgradeable.

    Recommendation

    To enable proper upgradeability, have the implementation contracts inherit from OpenZeppelin’s UUPSUpgradeable.sol and implement the required upgrade authorization functions.

    Reference: OpenZeppelin UUPSUpgradeable Docs

    Resolution

    M0 Team: Resolved.

  8. L-03 Low Swap Facility Blocks Actions DoS Acknowledged
    Location
    SwapFacility.sol: 287-288

    Description

    All M Extensions include a toggle to enable or disable yield earning. Any user interaction such as wrapping, unwrapping, or swapping must go through the SwapFacility.

    However, the SwapFacility restricts actions via _revertIfNotApprovedExtension, which only permits extensions that are currently earning.

    This effectively blocks all interactions with an M Extension once earning is disabled, even though the extension may still serve utility beyond yield accrual. (Swap with wM/USDC, Unwrap, Wrap)

    Recommendation

    Consider maintaining a dedicated list of approved extensions within SwapFacility that is independent of the active earners list. This allows for greater flexibility and separation of concerns between approval and earning status.

    Resolution

    M0 Team: Acknowledged.

  9. L-04 Low Incompatibility Of M Extensions Logical Error Acknowledged
    Location
    MExtension.sol: 31-35

    Description

    As per M0’s documentation, M Extensions are intended to act as wrappers that do not rebase, i.e., their balanceOf() should remain constant unless explicitly changed via user actions. However, due to the nature of continuous yield distribution, balances increase over time when claimFor(address) is called. While this isn’t technically rebasing (since it requires explicit action), the end result — a growing balance over time — introduces similar challenges. M Extensions are designed to be used independently across DeFi, but this yield-accruing behavior causes incompatibilities with many DeFi protocols, which generally assume that a token’s balance is static unless changed by transfer/mint/burn operations.

    Perpetual Protocols (e.g., GMX)

    • Unaccounted Balance as Deposit: Call claimFor(address) and then createDeposit()

    GMX Deposit Logic DEXs (e.g., Uniswap)

    • Unaccounted Balance as Deposit: Call claimFor() within the callback of a mint() operation.

    Uniswap V3 Pool Mint Logic Lending Protocols (e.g., Aave)

    • If claimFor() is called:
    • The collateral balance silently increases.
    • The protocol may overstate collateral value, or miscalculate interest.
    • See Aave–Lido integration spec for a similar class of issues with stETH and the design changes needed to accommodate it.

    Vaults (e.g., Yearn)

    • Unaccounted Balance as Deposit:Attack Vector: Call claimFor() just before deposit() into a vault.

    Yearn Deposit Logic

    We later understood that the M0 team has accounted for this issue and introduced a mitigation via the permissioned claimRecipient feature, which allows designated role holders to set custom recipients for DeFi pools. While this does help address many of the risks, it comes with certain limitations:

    • It introduces protocol-specific setup, requiring the M0 team to manually configure a different recipient for each new DeFi pool that integrates M Extensions.
    • It places the burden of yield redistribution on the integrating protocol, which may necessitate non-trivial contract modifications—such as tracking user entries

    and exits to ensure fair distribution.

    • This setup introduces a layer of centralization and operational friction, as each integration becomes dependent on coordination with the M0 team.

    Recommendation

    To ensure smoother and safer integration across DeFi: 1. Consider wrapping M Extensions in a token that behaves like wstETH or cTokens, where:

    • balanceOf() is constant
    • Yield is reflected via an exchangeRate() model
    • All rebasing or claiming is internalized and deterministic
    1. Allow self-directed recipient configuration:
    • Let users (e.g., DeFi pool creators) set their own yield recipient without needing M0’s intervention.
    • This reduces bottlenecks and central dependency.
    1. Proactively document integration considerations:
    • Highlight the claimFor behavior clearly.
    • Offer best practices for protocols to adapt (e.g., use wrapper, handle rebasing-like logic off-chain).

    Resolution

    M0 Team: Acknowledged.

  10. L-05 Low Race Condition On Access Control List Access Control Acknowledged
    Location
    Global

    Description

    M0 intends to implement blacklist/whitelist controls in its extensions, but inclusion or exclusion transactions can be frontrun by sophisticated attackers.

    They might consider DeFi pools as an escape hatch—for example, attackers have repeatedly used Curve’s 3-pool to circumvent a USDT blacklist.

    Recommendation

    Be aware of these scenarios and recommend frontrun-resistant RPC endpoints for extension-owner transactions that modify whitelist/blacklist status.

    Resolution

    M0 Team: Acknowledged.

  11. L-06 Low Design For L2 Oracle Sequencer Resilience Best Practices Acknowledged
    Location
    Global

    Description

    The L2 rate oracle hasn’t been implemented yet. Because L2 updates will occur in discrete jumps, sequencer uptime will be critical to ensure timely and accurate rate feeds once the oracle goes live.

    Recommendation

    Factor in sequencer availability when building the L2 rate oracle and design retry or fallback logic to mitigate downtime.

    Resolution

    M0 Team: Acknowledged.

  12. L-07 Low MEarnerManager: Missing Claim Rewards Resolved
    Location
    MEarnerManager.sol: 315-316

    Description

    In MEarnerManager, if a user is removed from the whitelist and then re-added without an intermediate claimFor call, they will retroactively accrue yield for the period during which they were not whitelisted.

    Recommendation

    Before returning early on a whitelist-status change, invoke an internal claimFor to settle any owed yield for the user. (100% fee)

    Resolution

    M0 Team: Resolved.

  13. L-08 Low MYieldFee: Claim Yield Rewards Resolved
    Location
    MYieldFee.sol: 232-233

    Description

    When setting a new claim recipient in MYieldFee, the yield accrued to date is not claimed for the current recipient—instead, all that yield immediately goes to the new recipient. (M0 has commented out this behavior and marked it optional.)

    Recommendation

    Revisit this behavior and, if it wasn’t intentional, insert a claim for the existing recipient before updating to the new one.

    Resolution

    M0 Team: Resolved.

  14. L-09 Low MSpokeYieldFee: Stepwise Jumps Rewards Acknowledged
    Location
    MSpokeYieldFee.sol

    Description

    MSpokeYieldFee uses a stepwise accrual curve rather than the continuous curve of its L1 counterpart.

    As a result, if the oracle rate moves from point A to B, yield will accrue for the entire A–B interval—even if, in reality, only a portion of that time should count.

    Recommendation

    Be aware of this stepwise behavior; as long as rates remain low and the oracle updates frequently, the risk should be minimal.

    Resolution

    M0 Team: Acknowledged.

  15. L-10 Low Blacklisted Tokens Permanently Locked Warning Acknowledged
    Location
    Global

    Description

    Protocol has a blacklist feature; however, once tokens are blacklisted there is no way for the extension owner or admin to seize those assets, effectively locking them in perpetuity.

    Recommendation

    Consider adding an option for the admin or extension owner to seize blacklisted assets.

    Resolution

    M0 Team: Acknowledged.

  16. L-11 Low Token Path Validation Bypassed Validation Acknowledged
    Location
    UniswapV3SwapAdapter.sol: 214-215

    Description

    Although M0 checks that the input and output tokens of a swap belong to the whitelist, an on-chain path could route through arbitrary intermediary tokens—circumventing those checks entirely.

    Recommendation

    Beware of this scenario, and if not intentional consider adding validations for all tokens included in path.

    Resolution

    M0 Team: Acknowledged.

  17. L-12 Low Entire Balance Swaps Between Extensions Fail DoS Acknowledged
    Location
    MExtension.sol: 226

    Description

    M0 roundings during transfers are always in favor of the protocol. However, this can cause a revert due to insufficient balance in edge-case scenarios where the entire balance needs to be swapped, such as during migrations between extensions, potentially affecting the last swapper.

    During the first step of the swap, mTokens need to be transferred from extensionIn to the swapFacility. Since extensionIn is an earner but the swapFacility is non-earner, this transfer requires rounding up.

    When the last user tries to swap their entire balance, the rounded-up mToken amount to transfer becomes greater than the mToken balance of the extension, causing the swap to fail.

    It is expected that a few extra wei will be deducted from the extension balance during _unwrap. However, unexpected reverts should be prevented when the entire balance is being swapped.

    Recommendation

    Document this behavior and inform users about the implications of swapping their entire balance.

    Resolution

    M0 Team: Acknowledged.

  18. L-13 Low MYieldFee: Earning Disabled Unexpected Behavior Resolved
    Location
    MYieldFee.sol: 294

    Description

    The MYieldFee extensions allows partners to set a fee for the yield generated on $M holdings. This fee can be update by the FEE_MANAGER_ROLE, with values ranging from 0 to 100%.

    If the fee rate is set to 100%, the updateIndex will set the latestRate to 0, as the new earnerRate is 100 - 100 = 0. Therefore, the isEarningEnabled function will return false.

    This is an unexpected behavior of the isEarningEnabled, as the $M tokens are still generating yield for the fee recipient, just not for the users. Worth comparing this behavior to the result of disableEarning, which will set both the latestRate to zero and effectively stop earning yield in$ M.

    Additionally, the currentIndex calls will always early return with the latestIndex so yield will never accrue even if the $M latestUpdateTimestamp changes.

    Recommendation

    Consider updating the isEarningEnabled function to return:

    IMTokenLike(mToken()).isEarning(address(this)).

    Alternatively, consider not allowing fee rate to be set to 100%.

    Resolution

    M0 Team: Resolved.

  19. L-14 Low MEarnerManager: Yield Fee Rounds Against The Protocol Logical Error Acknowledged
    Location
    MEarnerManager.sol: 175

    Description

    In the accruedYieldAndFeeOf function, the fee is calculated as fee = (yieldWithFee * feeRate_) / ONE_HUNDRED_PERCENT, which rounds down in favor of the user and against the protocol. This also results in a zero fee when claims are made with small amounts.

    Recommendation

    Round up the fee calculation in the accruedYieldAndFeeOf function.

    Resolution

    M0 Team: Acknowledged.

  20. L-15 Low MYieldFee: Accruing Yield When Earning Stops Logical Error Acknowledged
    Location
    MYieldFee.sol: 175

    Description

    When an extension is removed from the earner's list, any user can call the permissionless mToken.stopEarning function with the extension's address, instead of extension.stopEarning.

    Regarding the MYieldFee extensions, this is an issue as it calculates its own index based on the time delta and rate. Therefore, users will be earning "fake" yield, minting unbacked extension tokens.

    Although the swapFacility won't allow users to unwrap once earning has been disabled in the registrar, the extension token might be added to a Uniswap Pool, so users may exit here, while LPs are stuck with funds they can never unwrap.

    Recommendation

    Make sure the extension.stopEarning is executed at the exact same time the $M earnings are disabled.

    Resolution

    M0 Team: Acknowledged.

  21. L-16 Low Users Could Own M Tokens Warning Acknowledged
    Location
    Global

    Description

    The SwapFacility contract tries to prevent any normal user from receiving $M tokens. This invariant can be broken by malicious extensions.

    Even if fine in the first place an extension could upgrade it's code to be able to send out $M tokens to regular users as there is nothing blocking it.

    Recommendation

    Consider creating a mapping of addresses which are allowed to hold $M tokens to make sure this invariant holds.

    Resolution

    M0 Team: Acknowledged.

  22. L-17 Low Excess Yield Can't Be Claimed Logical Error Acknowledged
    Location
    MEarnerManager.sol

    Description

    Extensions can accumulate excess yield ($M balance greater than projected supply) due to different factors, like rounding, donations, or rate updates, specially in L2.

    Both the MYieldOne and MYieldFee rely on the $M balance of the contract. However, the MEarnerManager does not rely on the token balance but principal and current index calculations.

    The only way to claim fees is using claimFor, that claims both the user's yield and protocol's fee. Therefore, owner can't claim any excess yield accumulated in the extension.

    Recommendation

    Consider adding a claimExcess manager function to claim these yields.

    Resolution

    M0 Team: Acknowledged.

  23. L-18 Low Consider Using UniversalRouter For Swaps Informational Acknowledged
    Location
    UniswapV3SwapAdapter.sol

    Description

    The UniswapV3SwapAdapter contract creates swap parameters compatible with SwapRouter02, which is different than UniV3 SwapRouter and does not include a deadline parameter in ExactInputSingleParams and ExactInputParams.

    However, the official Uniswap documentation recommends using the UniversalRouter instead (reference):

    The UniversalRouter contract is the current preferred entrypoint for ERC20 and NFT swaps, replacing, among other contracts, SwapRouter02. An up-to-date list of deploy addresses by chain is hosted on GitHub.

    Recommendation

    Consider using UniversalRouter instead of SwapRouter02.

    Resolution

    M0 Team: Acknowledged.

  24. L-19 Low Unbacked Extension Balance Due To Rounding Rounding Acknowledged
    Location
    MYieldFee.sol: 441

    Description

    Proof of concept: PoC

    The MYieldFee and MEarnerManager extensions implement dual accounting, tracking both balance and principal. When transferring tokens between accounts or burning them, the principal subtracted from the sender is slightly overestimated. The recipient also receives this overestimated principal amount.

    These extensions use getSafePrincipalAmountRoundedUp to perform this overestimation. However, this can result in an account having a non-zero balance but a zero principal, especially after transfers involving very small amounts. Additionally, the transfer and burn functionality does not restrict zero-principal transfers.

    These non-zero balances can be moved between accounts even when the actual principal transferred is zero. More importantly, they can be used in swaps to another extension. During such swaps, a non-zero balance with zero principal is burned on extensionIn, reducing the mBalance of extensionIn, while both a non-zero balance and a non-zero principal are minted on extensionOut.

    While we couldn't identify a large amount of profit, it is possible to hold balances without any backing principal and use those balances to mint additional principal out of thin air. This introduces an insolvency risk in edge-case scenarios where all users attempt to claim their yields.

    Recommendation

    One option to consider is disallowing any transfer or burn when the transferred principal is zero. This would prevent swapping a balance with zero principal to another extension. However, this alone does not prevent accounts from holding a non-zero balance with zero principal. Additionally, consider clearing an account’s balance when the return value of getSafePrincipalAmountRoundedUp equals the sender’s principal balance.

    Another option is to use getPrincipalAmountRoundedUp and revert if the principal balance is insufficient, similar to the underlying M0 behavior. However, this approach may introduce unexpected reverts during wrap or unwrap operations.

    Resolution

    M0 Team: Acknowledged.

  25. L-20 Low Whitelist Check Limits Compatibility Validation Acknowledged
    Location
    MEarnerManager.sol: 279

    Description

    The _beforeTransfer function of the MEarnerManager contract does not only revert if the sender or recipient is not whitelisted, but also if the executor is not whitelisted.

    This limits compatibility with many DeFi protocols and may prevents things like gasless transactions, multisig transactions, etc.

    Recommendation

    Beware of this behavior, and revisit if not intended.

    Resolution

    M0 Team: Acknowledged.

  26. L-21 Low MExtension Implementations Can Be Initialized Logical Error Resolved
    Location
    MExtension.sol

    Description

    The swapFacility is an upgradeable contract and its implementation correctly calls _disableInitializers in the constructor.

    However, MExtensions will also be upgradeable, but there is no _disableInitializers to prevent someone initializing the implementation.

    Although no potential harm its evident at the contract level, these extensions could be widely known, and their contracts should not be manipulated in any way, as they may lose reputation.

    Recommendation

    Consider adding _disableInitializers in the constructor of MExtension.

    Resolution

    M0 Team: Resolved.

  27. L-22 Low Missing Fee Recipient Check In Blacklist Validation Acknowledged
    Location
    MYieldToOne.sol: 230

    Description

    The MYieldToOne manager can change the fee recipient using the setYieldRecipient function. This function reverts if yieldRecipient_ = address(0) but does not check if this address is blacklisted.

    Recommendation

    Validate if yieldRecipient address is blacklisted before updating the recipient.

    Resolution

    M0 Team: Acknowledged.

  28. L-23 Low MYieldOne: Claim Yield Logical Error Resolved
    Location
    MYieldToOne.sol: 94

    Description

    The setYieldRecipient function in the MYieldToOne contract changes the recipient without first claiming the yield for the previous recipient.

    As a result, all accumulated yield is attributed to the new recipient, even though it belongs to the previous one.

    Recommendation

    Consider claiming the yield before changing the recipient, similar to how it's handled when changing the fee recipient in the MYieldFee contract.

    Resolution

    M0 Team: Resolved.

  29. L-24 Low Interface feeRate Not Declared As View Informational Resolved
    Location
    IMYieldFee.sol: 164

    Description

    The IMYieldFee interface declares the feeRate function, but is not declared as view. Using this interface in a contract function declared as view will throw compilation errors.

    Recommendation

    Declare feeRate function as view in IMYieldFee.sol.

    Resolution

    M0 Team: Resolved.

  30. L-25 Low MSpokeYieldFee: Spoke Rate Update Logical Error Acknowledged
    Location
    MYieldFee.sol: 190

    Description

    The MSpokeYieldFee is an extension that will be deployed in L2s. Therefore, it relies on the latestUpdateTimestamp of $M contract in L2 (which is propagated every 1-2 hours) and earnerRate of an oracle contract, to compute its own extension index. The $M index is propagation to L2 takes about 15 minutes.

    This delay will cause some issues when the earner rate is updated. Consider the following, where t is in minutes:

    • t=0 $M index was propagated (current rate 4%)
    • t=100 Earner rate increased to 5% (also increased in L2 RateOracle)
    • t=100 Index propagated through Wormhole starts
    • t =115 $M index is updated on L2
    • t=115 call extension's updateIndex(). This will increase the current index, using 5% from [0,115] ,

    while the L1 real yield was 4% from [0,100].

    Therefore, the $M real yield will increase less than the yield calculated on the extension, creating unbacked tokens or affecting the protocol's fees.

    Even if the RateOracle is updated after the index is propagated, there will always be some accounting issues depending if the rate increase or decreases, the time it takes to propagate $M index, or the time where updateIndex is finally executed.

    Recommendation

    Current design prevents yield accruing to be completely in sync due to the Wormhole propagation delay. However, to avoid insolvency in extensions when rate increases, make sure $M index is propagated first, and update the RateOracle rate once the $M index updates in L2.

    Resolution

    M0 Team: Acknowledged.

  31. L-26 Low Stale balanceOf DoS DoS Acknowledged
    Location
    Global

    Description

    • The ERC20 functions balanceOf & totalSupply in the MEarnerManager & MYieldFee contract are

    usually stale as the correct values are only returned right after a yield claim.

    • The ERC20 and wrap/unwrap flows in the MExtension contract use the balanceOf function to check

    if the user owns enough funds to perform the wished action and revert otherwise. This can lead to DoS and makes it hard to unwrap all tokens (dust is most likely lost). For example:

    • User once wrapped 100 tokens and now owns 110 tokens at the current yield index
    • The user wants to unwrap all tokens and therefore calls the unwrap function with 110 tokens
    • The call reverts with an InsufficientBalance error as the system uses the balanceOf function

    instead of the balanceWithYieldOf function and therefore thinks the user only owns 100 tokens

    • Therefore the user needs to claim the yield and call unwrap again and now the call goes through

    but in the meantime more yield was accrued and a dust amt is left in the extension

    When users unwraps or transfers their current balance without claiming yield before it is even possible to reach a 0 balance and positive principal state.

    This can lead to users likely miss that they still own some stablecoins as their wallet will read 0 balanceOf and therefore not show the token anymore.

    Recommendation

    Always execute the normal yield claiming function for the given users before performing any action with them.

    Also consider allowing users to use their total balance for a operation by supplying type(uint256).max for example.

    Resolution

    M0 Team: Acknowledged.

  32. L-27 Low Blacklisted Users Accrue Yield Warning Acknowledged
    Location
    Global

    Description

    Blacklisted users continue to accrue yield.

    Recommendation

    Consider to change that if this behavior is not intended.

    Resolution

    M0 Team: Acknowledged.

  33. L-28 Low IMExtension Claim Function Informational Acknowledged
    Location
    IMExtension.sol

    Description

    IMExtension should have a common claim function, overridden by each extension (either to revert if claim is not available like in MYieldToOne or claiming for account). This will avoid different claim signatures like claimFor and claimYieldFor.

    Recommendation

    Consider creating a common claim function in IMExtension.

    Resolution

    M0 Team: Acknowledged.

  34. L-29 Low Incorrect Comments Informational Resolved
    Location
    Global

    Description

    • swapOutM(): mentions "exntesiom" in the comments
    • _revertIfInsufficientBalance : says that it reverts if account balance is below balance, but it should

    be amount.

    Recommendation

    Fix the incorrect comments.

    Resolution

    M0 Team: Resolved.

  35. L-30 Low Incorrect Return Param Name Informational Resolved
    Location
    ISwapFacility.sol: 143

    Description

    In the NatSpec for the ISwapFacility .swapAdapter() function, the @return parameter name is incorrect:

    function swapAdapter() external view returns (address registrar);

    Recommendation

    Update return param name from registrar to swapAdapter

    Resolution

    M0 Team: Resolved.

  36. L-31 Low Rounding Creates Insolvent Scenarios Informational Acknowledged
    Location
    Global

    Description

    In order to remain solvent, the $M balance should always be greater or equal to the extension's total supply (or projected total supply), so users are able to withdraw all funds, including recipient fees.

    However, when minting/wrapping, $M is transferred from the swap facility (non-earner) to the extension (earner).

    Depending on the amount and current index, this can yield to the extension receiving 1-2 wei less $M than expected, but the full amount of extension tokens are minted to the user.

    Due to the fact that the fees are calculated based on $M balance - extension token supply, this will affect extension owners directly.

    Recommendation

    Document this behavior so extension owners are aware of this fee reduction.

    Resolution

    M0 Team: Acknowledged.

  37. L-32 Low baseToken Fetched In Every Iteration Gas Optimization Resolved
    Location
    SwapFacility.sol

    Description

    Both swapInToken and swapOutToken fetches baseToken from the swapAdapter. The baseToken is an immutable param in the adapter, while the swapAdapter address is immutable in the swapFacility. These calls are unnecessary and increase the transaction gas costs.

    Recommendation

    Consider making baseToken immutable also in swap facility and avoid gas spent to fetch this address.

    Resolution

    M0 Team: Resolved.

  38. L-33 Low MYieldFee:Unnecessary Index Update Validation Resolved
    Location
    MYieldFee.sol: 194

    Description

    The MYieldFee.updateIndex() will update the index as long as the block.timestamp or earnerRate changed since the last update.

    Although this seems correct for L1 deployments, L2 deployments will not work as expected. This is due to the fact that the _latestEarnerRateAccrualTimestamp on L2 is the latestUpdateTimestamp of $M, but block.timestamp on L1.

    Therefore, every time the updateIndex is called on L2, the same variables will be assigned in storage, emitting the same IndexUpdated params.

    Recommendation

    Compare the latestUpdateTimestamp with the _latestEarnerRateAccrualTimestamp instead:

    if ($.latestUpdateTimestamp = _latestEarnerRateAccrualTimestamp() $.latestRate = rate_) return $.latestIndex;

    Resolution

    M0 Team: Resolved.

Invariants 11

The review's fuzzing suite asserted 11 invariants. 8 held and 3 did not.

Every invariant tested
IDInvariantResult
MYF-01MYieldFee extension mToken Balance must be greater or equal than projectedSupplyBroken
MYF-02MYieldFee extension mToken Balance must be greater or equal than projectedSupply +Broken
SWAP-01-00fee YTO-TO-YTO: MYieldToOne yield must not change after swapsHeld
SWAP-01-01YFEE-TO-YFEE: MYieldFee yield must not change after swapsHeld
SWAP-01-02MEARN-TO-MEARN: MEarnerManager yield must not change after swapsHeld
SWAP-02Swap facility M0 balance must be 0 after swap outHeld
SWAP-03Total M0 balance of all users must not change after swapHeld
SWAP-04Received amount of M0 must be greater or equal than slippageHeld
SWAP-05Received amount of USDC must be greater or equal than slippageHeld
MEARN-01MEarnerManager extension mToken Balance must be greater or equal thanBroken
ERR-01projectedTotalSupply Unexpected ErrorHeld

More from M0

All 10 reports
  1. Liquidity Delivery Updates

    4 findings 4 findings: 1 low, 3 informational
  2. PYUSDX

    21 findings 21 findings: 8 low, 13 informational
  3. Liquidity Delivery

    59 findings3 critical · 5 high 59 findings: 3 critical, 5 high, 10 medium, 14 low, 27 informational
  4. M Extensions Updates

    16 findings 16 findings: 1 medium, 5 low, 10 informational

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote