M0 engaged Guardian to review the security of their M0 M-Portal-Lite. From the 11th of August to the 12th of August, a team of 4 auditors reviewed the source code in scope.
- Published
- Review window
- August 11 to 12, 2025
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Linea
- Sector
- Stablecoins
- 0 Critical
- 0 High
- 1 Medium
- 1 Low
- 2 Informational
Scope
Overview
M0 engaged Guardian to review the security of their M0 M-Portal-Lite. From the 11th of August to the 12th of August, a team of 4 auditors reviewed the source code in scope.
Findings 4
-
M-01 Medium Happy Path: Normal Users Would Receive M Tokens Logical Error Acknowledged
Description
The original intent for the M token was to be permissionless, allowing anyone to hold it. However, based on recent discussions with the M0 team, the model has changed:
- New intent: Only selected role holders are allowed to handle the native M token directly.
- Regular users should instead interact with M extensions (wrapped versions of M) for DeFi and other use
cases.
However, the current Portal implementation does not reflect this new intent. If the destination token in a spoke/hub transfer is set to the native M token address, the Portal will issue or transfer native M tokens directly.
- The
swapInMfunction in the Swap Facility only allows approved swappers to swap native M into wrapped
M.
- This means users who receive native M cannot wrap it themselves.
- Since wrapped M is the expected token for DeFi integrations, users would be stuck holding the native token,
unable to use it in the intended ecosystem.
Under M0’s original (permissionless) deployment assumptions, this flow was fine. But with the new restricted model, this scenario can occur even in the
happy path.Example:
- On Mainnet Hub (
0x36f586A30502AE3afb555b8aA4dCc05d233c2ecE), the destination token for Linea is
currently set to native M.
- As a result, if a user calls
transferon the Portal, they receive native M on Linea instead of the wrapped
version.
Recommendation
Set the default destination token to the wrapped M (
wM) instead of native M, so that by default:- The Portal wraps into
wMbefore delivering to the user. - If a different extension specified, wrap into that instead.
Resolution
M0 Team: Acknowledged. This is acceptable behavior and will be addresses by configuration of approved bridging paths for Portals.
-
L-01 Low Error Path : Normal User May Receive M Tokens Validation Acknowledged
Description
When a user makes a cross chain transfer with a M extension token the flow looks like this:
- On the source chain the
swapOutMfunction is called to convert the M extension tokens to $M
tokens
- The $M tokens are transferred to the destination chain
- On the destination chain the system tries to wrap the $M tokens to M extension tokens with the
swapInMfunction- But in case the
swapInMcall fails the user just receives the $M tokens instead
This is problematic as normal users should never receive $M tokens and as a normal user is not able to use the
swapInMfunction the user might not be able to do anything with these tokens.A malicious actor might also be able to do this on purpose (in case the actor sees any benefit of doing so).
For example if the M extension token has a minimum amount restriction for the wrap function of $100 the user could perform multiple $99.99 cross chain transfers to load himself up with $M tokens.
Recommendation
Consider leaving the tokens inside the contract or transferring them to another one and let an admin handle the situation if this edge case occurs.
Resolution
M0 Team: Acknowledged. This is acceptable behavior for now.
- On the source chain the
-
I-01 Informational Incorrect Comment Informational Acknowledged
Description
The comment above the
_revertIfUnsupportedDestinationChainfunction on line 128 ofSpokePortalcontract is incorrect. It should be: "Reverts if the destination chain is not the Hub chain".Recommendation
Update the comment.
Resolution
M0 Team: Acknowledged. Will fix.
-
I-02 Informational Earning Cannot Be Re-Enabled Once Disabled Warning Acknowledged
Description
The
_isEarningEnabledfunction is implemented as:function _isEarningEnabled() internal view returns (bool) { return wasEarningEnabled & disableEarningIndex = IndexingMath.EXP_SCALED_ONE; }Given this logic:
Once
disableEarningIndexis set to current Index indisableEarning, earning can not be enabled again.Recommendation
Be aware of this constraint.
Resolution
M0 Team: Acceptable behavior.
No findings match.
More from M0
All 10 reports-
Liquidity Delivery Updates
4 findings 4 findings: 1 low, 3 informational -
PYUSDX
21 findings 21 findings: 8 low, 13 informational -
Liquidity Delivery
59 findings3 critical · 5 high 59 findings: 3 critical, 5 high, 10 medium, 14 low, 27 informational -
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.
