Guardian's review of M Extensions Updates for M0, published December 2025. The report records 16 findings across 2 review rounds, including 1 medium and 5 low.
- Published
- Review window
- November 10 to December 7, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Linea, Unichain, Solana
- Sector
- Stablecoins
- 0 Critical
- 0 High
- 1 Medium
- 5 Low
- 10 Informational
Scope
3 files in scope · 191 nSLOC
| File | nSLOC | Lines |
|---|---|---|
common/src/libs/TransferHelper.sol | 32 | 58 |
src/components/pausable/Pausable.sol | 17 | 44 |
src/projects/jmi/JMIExtension.sol | 142 | 343 |
Findings 16
Main Review
11 findings · November 10 to 12, 2025-
M-01 Medium Trusted Routers Cannot Execute Actions DoS Acknowledged
Description
Whenever a function guarded with the
isNotLockedmodifier is called,Locker.set(caller_)is executed. Ifmsg.senderis a trusted router,caller_is set to whatever address the router returns. Otherwise, the realmsg.senderis used.However,
SwapFacilityusesmsg.senderin all of its functions. For example, it will try transferring assets out ofmsg.sender, etc... This will result in reverts when the function is called by a trusted router since it's expected to only forward the call, not pay for the transactions. Furthermore,msg.senderis passed to thepermit()calls as well, meaning it has to be the signer of the message, which is impossible for the router.There is also asymmetry existing when calling
JMI.wrap(), because it usesISwapFacility(msg.sender).msgSender(), which will be the correct forwarded sender.Recommendation
Replace the
msg.senderusage inSwapFacilitywithmsgSender(). -
L-01 Low Missing _beforeApprove Overwrite Validation Acknowledged
Description
The
JMIExtensionoverwrites all theMYieldToOnehooks which reverts if a given account is frozen to also add the_requireNotPausedcheck, except for the_beforeApprovehook.Recommendation
Consider to also overwrite and add the
_requireNotPausedto the_beforeApprovehook. -
L-02 Low Asset Donations Can Lead To Underflow DoS Resolved
Description
The
JMIExtensioncontract usesbalanceOfto perform checks related to the asset amounts deposited instead of a internal mapping. On the other hand it uses the variabletotalAssetsto store the total amount of all deposited non-M-token assets.This can lead to DoS and stuck funds if someone would transfer assets into the contract directly. For example:
totalAssets= 0- Two assets are whitelisted (Asset1 & Asset2)
- Bob deposits 100 Asset1 and 100 Asset2
totalAssets= 200
- Eve transfers 50 Asset1 into the contract
- Eve calls
replaceAssetWithMto get 150 Asset1 out of the contracttotalAssets= 200 - 150 = 50
- Now it’s not possible to get the 100 Asset2 out of the contract as
totalAssetswould underflow in that case
Another point is that asset caps can be consumed without wrapping.
For a wrap to succeed, the asset cap of the asset being wrapped should not be exceeded.
assetCap(asset) >= (IERC20(asset).balanceOf(address(this)) + amount)Because
balanceOf()is used, anyone can transfer asset tokens to the contract and consume the cap. The tokens cannot be utilized sincetotalAssetswon't be modified.Recommendation
Consider to implement a internal mapping instead to track asset amounts.
-
L-03 Low _replaceAssetWithM and _wrapCalc Can Round To 0 Validation Resolved
Description
The
_fromExtensionToAssetAmountcalculation in the_replaceAssetWithMfunction could round to 0 in the edge case that the given asset has less decimals than the $M token and a dust amount is given.This would result in the user losing $M tokens and decreasing the
totalAssetswithout receiving any assets in return.The same behavior is also present in the
_wrap()flow, where users can send assets, but have 0 extension tokens minted for them.uint256 jmiAmount_ = _fromAssetToExtensionAmount(asset, amount); ... _mint(recipient, jmiAmount_);Recommendation
Consider reverting in both cases if the resulting amount is 0.
-
L-04 Low No Extension Validation Unexpected Behavior Acknowledged
Description
Before interacting with the system, an extension has to be either approved by an admin or be an approved earner.
function isApprovedExtension(address extension) public view returns (bool) { return _isApprovedEarner(extension) || isAdminApprovedExtension(extension); }The
_isApprovedEarner()function will returntruefor any address if the earners list is being ignored.function _isApprovedEarner(address extension) private view returns (bool) { return IRegistrarLike(registrar).get(EARNERS_LIST_IGNORED_KEY) != bytes32(0) || IRegistrarLike(registrar).listContains(EARNERS_LIST_NAME, extension); }Therefore, whenever the earners list is ignored, any arbitrary address can be used for an extension. This allows emitting
Swapped,SwappedOutMandJMIAssetReplaced()events with arbitrary addresses and poses risks if the code changes in future.Recommendation
Consider excluding the
EARNERS_LIST_IGNORED_KEYpart from_isApprovedEarner()and managing the swappers via approvals. -
L-05 Low
MTokenCan Be Wrapped Unexpected Behavior ResolvedDescription
To wrap an
mTokenintoJMIExtensionusers must callSwapFacility.swap()and specify mToken as tokenIn and JMIExtension as tokenOut. This will execute the first branch in_swap():if (tokenIn == mToken) return _swapInM(tokenOut, amount, recipient);The
_swapInM()function is not adjusted to supportJMIExtension, instead it used the genericMExtensioninterface to callwrap()IMExtension(extensionOut).wrap(recipient, amount);Because
JMIExtensioninherits fromMExtension, the call will succeed, butMExtension.wrap()will be executed instead ofJMIExtension.wrap()There the internal_wrap(address,address,uint256)function is executedfunction wrap(address recipient, uint256 amount) external onlySwapFacility { // NOTE: `msg.sender` is always SwapFacility contract. // `ISwapFacility.msgSender()` is used to ensure that the original caller is passed to `_beforeWrap`. _wrap(ISwapFacility(msg.sender).msgSender(), recipient, amount); }This function is different than the
_wrap(address,address,address,uint256)function defined inJMIExtension. From there on, the whole wrap is executed by the logic defined inMExtensioninstead ofJMIExtension, which means the empty_beforeWrap()function ofMExtensionis executed and the following validation, including the pause check, is not performed:function _beforeWrap(address asset, address account, address recipient, uint256 amount) internal view virtual { _requireNotPaused(); if (!isAllowedAsset(asset)) revert AssetNotAllowed(asset); if (!isAllowedToWrap(asset, amount)) revert AssetCapReached(asset); super._beforeWrap(account, recipient, amount); }Since
assethere ismToken, skipping the two if statements is fine, but not calling_requireNotPaused()means the code doesn’t respect thepausedflag at all.In result, wrapping mTokens into the
JMIExtensionwill be possible even when it’s in a paused state.Recommendation
Call the correct function If
tokenOutinSwapFacility.swapInM()is theJMIExtension. -
I-01 Informational Arbitrage Opportunity On Depeg Warning Acknowledged
Description
If one of the allowed assets (stablecoins) in the
JMIExtensionwould lose in value a arbitrage opportunity arises:- Traders buys assets for $0.9
- Trader deposit the assets into the
JMIExtension - Trader swaps the
JMIExtensioninto a safe one - Repeat
Recommendation
Be aware of this risk and react as quickly as possible in that case by pausing the contracts and setting the cap of the given asset to zero.
-
I-02 Informational replaceAssetWithM Can Be Misused Warning Resolved
Description
The
replaceAssetWithMflow does not check if the given asset was whitelisted (cap > 0).This function can therefore be used to swap any M0 extension to any token in the
JMIExtension.If by accident a token with higher value would land inside the contract any user could claim it by for example performing a $1 M0 Extension against 1 WETH swap.
Recommendation
Be aware that this function can be used in that way and document this behavior.
Or consider to only allow the withdraw of whitelisted tokens to decrease potential attack surface. In this case however a new whitelist would make more sense than the current cap logic as otherwise it would no longer be possible to prevent further deposits while still allowing withdraws.
-
I-03 Informational Precision Loss On Transfers Can Be Improved Rounding Acknowledged
Description
Whenever an extension is wrapping mTokens, it pulls
amountof these mTokens from the sender and mints the exact same amount of extension tokens. However, due to the index tracking behavior of theMToken, transferringXamount can result in less thanXtokens received.IMTokenLike(mToken).transferFrom(msg.sender, address(this), amount); _mint(recipient, amount);In the case of
wrap(), the system will mint more extension tokens that the received mTokens, which means some of the extension tokens will not be redeemable. The same is true for_replaceAssetWithM().The opposite rounding problem exists in
_unwrap(), there the amount being subtracted is rounded up and because of that the system will be potentially losing value with each transfer.Recommendation
There are already comments in the
MExtensionacknowledging the problem. While it’s hard to be solved for the_unwrap()case, you can add balance delta tracking forwrap()and_replaceAssetWithM()to avoid some of the losses. -
I-04 Informational Approvals For Frozen Account Cannot Be Cleared DoS Acknowledged
Description
The approve hook reverts if the spender is frozen, regardless of the allowance amount. This prevents users from setting allowance to zero for spenders that later became frozen, effectively trapping their prior approvals until the spender is unfrozen.
Recommendation
Allow
approve(owner, spender, 0)to succeed even if the spender is frozen. -
I-05 Informational Recipient Of Replaced Assets May Be Frozen Validation Acknowledged
Description
The
JMIExtensionenforces freeze checks for wrap/unwrap/transfer via MYieldToOne hooks, but_replaceAssetWithM()does not validate whether the recipient is frozen. A frozen account can therefore receive allowed assets directly throughreplaceAssetsWithM()Recommendation
If this is not expected, revert if the recipient is frozen.
Remediation Review
5 findings · December 7, 2025-
I-01 Informational Decimals Can't Be Refetched Warning Acknowledged
Description
The
setAssetCapsaves the given assets decimals and it is not possible to update them in case the decimals of the given ERC20 token would ever change.Recommendation
Be aware of that and consider to refetch the decimals every time the
setAssetCapfunction is called to be able to react on such a change. -
I-02 Informational Accidental Asset Transfers Are Lost Now Warning Acknowledged
Description
Now asset balances are tracked with a internal balance. Therefore if now non $M token assets are accidentally transferred into the
JMIExtensioncontract they are lost.Recommendation
Consider to add a sweep function to be able to withdraw these assets.
-
I-03 Informational
canSwapViaPath()May Revert Warning AcknowledgedDescription
A
try/catchwas added toSwapFacility.canSwapViaPath()to fetch the paused status of the two tokens.// If contracts are paused, return false try Pausable(tokenIn).paused() returns (bool tokenInPaused) { isTokenInPaused = tokenInPaused; } catch {} try Pausable(tokenOut).paused() returns (bool tokenOutPaused) { isTokenOutPaused = tokenOutPaused; } catch {}Because the addresses are arbitrary, any contract (or EOA with code) can be passed as a token, return a data that cannot be decoded to a
booland make the call tocanSwapViaPath()to revert. This can lead to DOS for external integrators that expect the function to never revert.Recommendation
Document the risk of reverting.
-
I-04 Informational Unnecessary Assignments Best Practices Resolved
Description
The following two variables in the
SwapFacility.canSwapViaPath()are assigned their default values (false for bool), which is unnecessary.bool isTokenInPaused = false; bool isTokenOutPaused = false;Recommendation
Consider removing the assignments.
-
I-05 Informational Unsafe
uint240Casting Informational ResolvedDescription
The balance of the tokens in the
Assetstruct is stored asuint240. The comment states that M token's supply won't exceeduint240, so balance should be safe.// M token's supply can't exceed uint240, so uint240 is safe to use. uint240 balance;However, this balance doesn't track M assets, but every other enabled asset. In the
_wrap()function theamountof tokens transferred is unsafely cast touint240. If the token is non-standard , i.e has very big precision, and the asset cap allows it, the amount used may exceed uint240 and result in wrong accounting.Recommendation
- Fix the comment to explain the right reason why using
uint240is safe - Make sure to not use assets which can have such big supplies
- Configure asset caps with this in mind
- Fix the comment to explain the right reason why using
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 -
USD8
10 findings1 high 10 findings: 1 high, 1 low, 8 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.
