MUX engaged Guardian to review the security of their margin trading protocol. From the 2nd of December to the 6th of January, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- December 2, 2024 to January 6, 2025
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Perpetuals
- 1 Critical
- 9 High
- 24 Medium
- 42 Low
- 0 Informational
Scope
Overview
MUX engaged Guardian to review the security of their margin trading protocol. From the 2nd of December to the 6th of January, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 12 High/Critical issues were uncovered and promptly addressed by the MUX team.
Security Recommendation Given the number of High and Critical issues detected Guardian recommends that an independent security review of the protocol at a finalized frozen commit is conducted before deployment.
Notice Further changes were made to the codebase after Guardian’s remediation review which changes existing functionality and introduces new functionality. Guardian cannot attest to the security of these latest changes and how that might affect the final deployment.
Findings 76
-
C-01 Critical Inflation Attack Steals First Deposit Inflation Attack Resolved
Description
Proof of concept: PoC
An inflation attack, commonly seen in ERC-4626 vaults, allows a malicious actor to steal deposits from the first depositor. This vulnerability exists in the collateral pool and can be executed as follows:
- The attacker deposits 1 wei of liquidity into the pool (e.g., ARB).
- The attacker observes a victim placing a liquidity order for 1000 ARB.
- The attacker front-runs the victim and donates 1000 ARB using the
donateLiquidityfunction,
inflating the
lpPrice.- The broker fills the victim's liquidity order, but due to the inflated
lpPrice, the victim receives 0
shares.
- The attacker withdraws their shares, reclaiming the donated amount along with the victim's
deposit.
The two-step order flow allows for 'front-running' which is usually not feasible on Arbitrum. This attack is also feasible only with 18-decimal tokens due to the
_toWadconversion inaddLiquidity.Recommendation
Introduce a minimum share issuance threshold to ensure that deposits always result in a non-zero share allocation. Consider restricting the
donateLiquidityfunction to privileged accounts.Resolution
MUX Team: Resolved in commit 547c9abc7ba99d18246cee178554b209ba9a8123.
-
H-01 High Possible DoS Vector In The Delegate Function DoS Resolved
Description
The
Delegatorcontract'sdelegatefunction allows users to delegate actions to a specificdelegatorby updating the_delegatorsmapping. However, this function lacks proper access control mechanisms, permitting any user to overwrite existing delegations for anydelegatoraddress.Consequently, an attacker can call
delegatewith a targetdelegatoraddress and replace the legitimate owner and action count with arbitrary values.This vulnerability enables a DoS attack, where the original
delegatoris obstructed from performing delegated actions because their delegation has been maliciously overwritten.Recommendation
Update the
delegatefunction so it can only be called if the_delegators[delegator].owneris equal to theaddress(0). Implement a newremoveDelegatefunction that allows thedelegatorto reset the_delegators[delegator]mapping.This way, the
delegatorcan accept new delegations as now_delegators[delegator].owneris equal to theaddress(0).Resolution
MUX Team: Resolved in commit 6eab6d3adda18223f558a2086e30f9c1b23a1f7d.
-
H-02 High Liquidity Providers Can Abuse Unrealized Gains To Push Pool Utilization Over One Hundred Configuration Resolved
Description
The contract’s AUM calculation logic incorrectly treats negative unrealized profit and loss from traders as an increase in the pool’s available collateral. This is because in the
removeLiquidityfunction the following check is performed.However, if we take a look at the
_aumUsdfunction, we can see that theupnlcan be negative in case that the traders are at a loss and owe value to the collateral pool.In that case,
_aumUsdWithoutPnl().toInt256()will be subtracted a negative number which will artificially increase the finalaumallowing liquidity providers to remove more liquidity than is genuinely available and, in some cases, even push the pool’s utilization above 100%.When utilization surpasses 100% or is very high, the borrowing rate curve causes dramatically increased costs for traders as it grows exponentially near full utilization, which severely disadvantages traders who are forced to pay these escalated costs.
Recommendation
Update the AUM computation to ensure that negative unrealized PnL owed by traders does not inflate the pool's available collateral. Instead of allowing negative unrealized PnL to translate into a higher AUM during the removal of liquidity, enforce a lower bound of zero on the
upnl =_traderTotalUpnlUsd(marketId)calculation or introduce a separate accounting mechanism to distinguish between actual collateral and unrealized trader losses.Resolution
Guardian: Resolved, however MUX team should ensure that in short markets:
- A collateral pool that has the same collateral token as the market’s underlying is never used.
- Collateral pools with non-stable assets are never used.
-
H-03 High Liquidity Providers Can Withdraw Right Away Configuration Resolved
Description
The current protocol design allows LPs to withdraw their liquidity at any time without any enforced delay. The
removeLiquidityfunction only checks that the total reserved USD is less than or equal to the computed AUM.As a result, LPs can instantly remove substantial amounts of collateral from a pool, increasing its utilization and causing borrowing rates to spike dramatically.
Traders relying on a stable borrowing environment are suddenly forced to pay higher borrowing fees or simply to close their positions as soon as possible.
Recommendation
Implement a withdrawal delay mechanism that prevents LPs from instantly removing large amounts of liquidity. By requiring a grace period before withdrawals are finalized, traders gain time to anticipate these changes by adding collateral or closing their positions.
MCO_LIQUIDITY_LOCK_PERIODshould be way higher than the 2 minutes set in the different test files.Resolution
MUX Team: Resolved in commit beefa4c11533e673211514b9743fa45724bc740e.
-
H-04 High Price Query Failure Due To Transient Storage In PricingManager Configuration Resolved
Description
The
PricingManagercontract relies on transient storage to store and read price data. As a result, once the transaction that callssetPricescompletes, the stored price data is cleared. Subsequent operations in a new transaction that rely on_priceOfas for examplewithdrawAllCollateralfail to retrieve valid prices, returning zero and reverting due to the require check.This leads to a DoS scenario where users will not be able to perform certain operations that rely on the
PricingManagerreturned price. Currently, the only operation that is affected iswithdrawAllCollateralas this is the only function in theOrderBookthat does not have to be called by a broker and uses the_priceOffunction, querying the transient storage.Recommendation
Switch to a permanent storage mechanism within
PricingManagerso that prices remain available across transactions. If the use of transient storage is still wanted, ensure all functions relying on_priceOfare executed within the same transaction that sets the price.Resolution
MUX Team: Resolved in commit 951504ecf8da56c735039b9f0edf82e25251f82c.
-
H-05 High Unauthorized Position Creation Configuration Resolved
Description
Proof of concept: PoC
The permissionless
OrderBook.depositCollateral()function allows anyone to deposit collateral for a givenpositionId. If the position account doesn't exist, it will be created.As we can see from
LibCodec, the first 20 bytes of thepositionIdare the address of its owner and the last 12 bytes show if the position supports one or many markets. If the last 12 bytes are 0, the account is a multimarket one, otherwise it supports only one market.Each address can have up to
MAX_POSITION_ACCOUNT_PER_TRADERdifferent position accounts (currently set to 64). A malicious user can calldeposit64 times and add 1 wei of collateral to anypositionId, which means they can create 64 single market accounts for a given address.In result, the victim address will have to either use only single market accounts or change their address. However, the attack can be executed for their new address again.
This can also be detrimental for smart contracts if they have used only single market accounts, but try to create a multimarket one - the contract will probably experience DOS.
Recommendation
Add one of the following restrictions:
depositCollateralis permissionless only if thepositionIdalready exists.depositCollateralcan be called only for positions owned by the caller.
Resolution
MUX Team: Resolved in commit 43bea77fee396cc2856cef7a19fec6856732dbe0.
-
H-06 High Incorrect reservedUsd Logic For Shorts Logical Error Resolved
Description
The
_reservedUsdfunction calculates the liquidity that must be reserved to ensure sufficient funds are available to pay out traders. The calculation is currently implemented as:reserved = marketPrice * positionsThis approach is valid for long positions, as their payouts increase with rising prices. However, the logic fails for short positions. For shorts, profits are inversely related to the price. As the price increases, shorts incur losses, and as the price decreases, shorts generate profits.
The current implementation over-reserves liquidity for shorts when prices rise and, more critically, under-reserves liquidity when prices fall.
Recommendation
Consider implementing a separate
reservedUsdlogic for short positions, taking into consideration that for shorts, the maximum amount they can profit is up to their cost-basis (e.g. when opening a 1 ETH short at $3000, the maximum profit is $3000).Resolution
Guardian: Resolved, however MUX team should ensure that in short markets:
- A collateral pool that has the same collateral token as the market’s underlying is never used.
- Collateral pools with non-stable assets are never used.
-
H-07 High Delegators Cannot Cancel All Orders Logical Error Resolved
Description
The
Delegator.cancel()function allows delegators to cancel orders on behalf of the delegation's owner. This works fine for position orders, becauseLibOrderBook._cancelPositionOrderallows the delegator to execute the certain action.However, both
_cancelLiquidityOrderand_cancelWithdrawalOrderrequire themsgSender(which is the delegator) to be equal to the position owner, which will always revert for delegated calls.Recommendation
Add the
isDelegatorcheck to_cancelLiquidityOrderand_cancelWithdrawalOrderfunctions.Resolution
Guardian: Resolved, however, liquidity orders can not be canceled as they can not be directly created through the Delegator.
-
H-08 High Multi-market Liquidations May Fail Logical Error Resolved
Description
When a position with multi-market exposure becomes liquidatable, the protocol intends to liquidate and close positions across all markets. However, the current implementation of the
liquidatefunction can only process one market at a time.This creates complications: 1. If the broker closes the profitable position in Market A first, the overall position may no longer be liquidatable due to lower margin requirements and increased collateral. This leaves the loss-making Market B active, which may be unexpected and undesirable for the trader. 2. Conversely, if the broker closes the loss-making position in Market B first, there may not be enough collateral to cover borrowing and position fees as the profits from Market A remain unrealized. This results in skipped and unpaid fees during liquidation.
Recommendation
Revise the
liquidatefunction to loop through all associated markets and close every position within a single transaction.Resolution
MUX Team: Resolved in commit 350a03bca14b6a91838e4849630d9e8afebf5dee.
-
H-09 High oneRound Function Potentially Allows Negative Allocations Logical Error Resolved
Description
Within the
oneRoundfunction, when a negative allocation is produced bycalculateXi, the inner loop is broken but the partial candidate array is still copied intobestXi.This allows negative allocations to be stored and subsequently used, totally breaking the purpose of the allocation algorithm logic.
Once a negative allocation is propagated into
mem.bestXiand then allocated it will corrupt the overall allocation logic, causing mismatched liquidity calculations.Recommendation
Update the
oneRoundfunction sobestCostandmem.bestXarray are only updated if there are no negative allocations.Resolution
MUX Team: Resolved in commit a346bf3d988eb7833bb58e36a9beba194472d164.
-
M-01 Medium ClosePosition Only Updates The Current Market’s Borrowing Fee Configuration Acknowledged
Description
When a position is closed, the contract updates only the borrowing fees for the currently closed market before verifying maintenance margin safety. This means that the margin check is performed without considering the borrowing fees pending to be paid in other still open markets.
As a result, once all borrowing fees eventually get updated for those other markets, the position may fall below the required maintenance margin and become liquidatable.
This could allow users to close a position on one market and leave the account appearing safe at the time of closure, despite actually being unsafe once all borrowing fees are updated.
Moreover, it also allows users to close a position that could be in a liquidated state but appears safe because the borrowing fees were not applied in the other markets.
Recommendation
Update all relevant market borrowing fees before performing the maintenance margin check by calling the
_updateBorrowingForAllMarketsfunction so that the position’s true value is evaluated.Resolution
MUX Team: Acknowledged.
-
M-02 Medium Inconsistent Borrowing Fee Calculation Configuration Acknowledged
Description
Proof of concept: PoC
The current borrowing fee implementation can lead to situations where certain users end up paying proportionally more in borrowing fees than others, depending on how frequently their position or market is updated. The protocol defines
_updateMarketBorrowingas a function that accumulates a per USD “borrow cost” (i.e.cumulatedBorrowingPerUsd) every time a funding interval elapses. However, the actual “charging” of that borrowing fee to a trader’s position happens only in_updateAndDispatchBorrowingFeeor_updateAccountBorrowingFeecalls. Practically speaking, a position pays the difference between the current market’s cumulatedBorrowingPerUsd and the position’s storedentryBorrowingwhenever the code calls_updateAccountBorrowingFee. In an idealized continuous interest model, it should not matter how many times you pay your borrowing fee, because the net owed over a specific time interval is the same. However, the code’s logic effectively runs each time_updateAccountBorrowingFeeis triggered for a position.Because the fee is calculated as:
feeUsd = positionValue * (cumulatedBorrowingPerUsd[i] -pool.entryBorrowing) / 1e18and immediately removed from the user’s collateral, if your position is “touched” or updated more frequently, you might (depending on how the position’s size or price changes between updates) accumulate fees in multiple smaller increments, each pulled from your collateral. Meanwhile, a position that is only “touched” at the very end of a long interval might remain unaffected until a single large settlement occurs. If Trader A’s position has_updateAccountBorrowingFeecalled multiple times over an interval, then Trader A is “paying down” incrementally. If that triggers more “positionValue” recalculations at times when the position might be large or at unfavorable spot prices, Trader A can end up paying more total borrowing fees across many increments. Trader B, who never triggers a fee update until the end of a long period, might in some scenarios come out behind or ahead, depending on how the position size or price environment changed. If the size is smaller or the price is favorable near the time of the eventual update, B may pay less over that same time interval.Proof of concept: PoC
Typically, in a continuous or properly time-based interest model, two traders borrowing the same amount of capital over the same timespan should pay the same interest, irrespective of how many times that interest is “settled.” The logic here is subtly different because the fee for each update depends on:
- The position’s value at that update moment
(positionValue = pool.size * price / 1e18). - The difference between
cumulatedBorrowingPerUsd[i]and the storedentryBorrowingfrom the last
time the fee was applied.
If your position value or the
feePerUsddiffers across multiple, intermediate calls, you can pay more or less than a user who sees only a single large update. Therefore, frequent updaters might end up paying more overall borrowing fees if the position’s size or mark price remains large each time an incremental fee is triggered and infrequent updaters could manage to pay less in total, if the price or position size shrinks by the time they do a final update. This difference becomes especially pronounced if the protocol or the broker calls_updateAccountBorrowingFeefor some positions more often than others. On the other hand, the protocol ties borrowing fees to the current market price rather than a fixed entry price. Consequently, short positions that are winning (as the price went down) see their borrowing fees shrink and long positions in profit see the opposite effect, facing higher charges if their position is marked up repeatedly.Recommendation
Consider migrating to a borrowing model that continuously accrues fees in a fair, time based manner, regardless of the number of updates. One option is to accumulate interest in a strictly pro rata fashion so that a user’s borrowing fee liability is locked in each second or each block rather than heavily influenced by how many times the position’s borrowing fee is updated. Another approach involves partially anchoring the fee to an initial or average notional to avoid large disparities caused by frequent or infrequent position touches. The ultimate goal is to ensure that two users with the same borrowed exposure for the same total time pay comparable amounts in borrowing fees, independent of how many times their fees happen to be updated.
Resolution
MUX Team: Acknowledged.
- The position’s value at that update moment
-
M-03 Medium setPrices Function Does Not Accurately Retrieve The Asset Prices Configuration Acknowledged
Description
The protocol’s current configuration for the oracle-driven pricing mechanism allows the price expiration to be set anywhere between 30 seconds and up to 24 hours which.
As a result, when a broker performs a
multicallthat begins with callingsetPricesto load price data and then proceeds to fill position orders, liquidity orders etc. the entirety of these operations may rely on prices that belong to different timestamps from the range:[block.timestamp - 24 hours,block.timestamp].Traders can use multiple collateral tokens to maintain their positions. Let’s imagine that a trader holds a position backed by USDC, WETH and WBTC and the
oracleIdsfor these assets have an expiration time of 24 hours and are using Chainlink Price Feeds with a heartbeat of 86400 seconds.When the broker calls
setPricesthe price received for USDC could be from second 86399, for WETH 40000 and for WBTC 1. Therefore, this could result in positions that might be incorrectly assessed as safe or unsafe leading to unfair liquidations, missed opportunities for profitable trades…Recommendation
Consider integrating with low-latency, high-frequency oracle solutions like Pyth, which provide more up-to-date prices with tighter heartbeat intervals.
Resolution
MUX Team: Acknowledged.
-
M-04 Medium Avoid Using _strictStableIds Configuration Acknowledged
Description
The
PricingManager’s pricing logic for stable assets includes a mechanism to use a hardcoded reference value of 1 USD unless there is a major depeg.While intended to maintain stability, this approach causes the protocol to rely on an artificially normalized value rather than accurate market data. This can cause positions backed by these stable assets to appear safer or riskier than they actually are, leading to incorrect margin calculations, mispriced positions, unexpected liquidations or unearned profits.
Essentially, traders and LPs might face unfair conditions due to the system forcibly ignoring genuine price signals and relying instead on an arbitrary stable reference price.
Recommendation
Rather than forcing a stable asset’s price to a hardcoded value, consider a dynamic safeguard that triggers protocol-level responses during actual depeg events.
For instance, if the asset’s price deviates too far from its peg, the protocol could pause certain trading operations, tighten collateral requirements, or invoke other protective measures until accurate prices are restored.
Resolution
MUX Team: Acknowledged.
-
M-05 Medium Lack Of A Grace Period For Configuration Changes Configuration Acknowledged
Description
The contract’s use of
setConfigcalls to modify critical parameters, such asMCP_ADL_MAX_PNL_RATEand other parameters, can impose immediate and drastic changes to the trading environment without providing users any time to adjust their positions.Sudden alterations to margin requirements, liquidation thresholds or other essential settings can leave traders caught off guard and result in unexpected position losses and forced liquidations.
Recommendation
Introduce a delayed activation mechanism for all configuration changes. When a
setConfigcall modifies a critical parameter, the new value should enter a pending state for a predefined buffer period, for example 24 hours, before it becomes effective.During this transitional time frame, traders can receive alerts or monitor upcoming changes, ensuring they have sufficient time to rebalance their portfolios, add collateral or close their positions.
Resolution
MUX Team: Acknowledged.
-
M-06 Medium Add And RemoveLiquidity Orders Are Missing Slippage Checks Configuration Acknowledged
Description
When a Liquidity Order is placed there is no way for the caller to specify a minimum acceptable output amount.
In typical AMM or liquidity pool scenarios, a user will include a
minAmountOutparameter to protect themselves from adverse price movements or sandwich attacks that may occur between the placement of the order and the fulfilment.Without this parameter, the liquidity providers are subject to price changes and certain actions that can occur between the placement of their order and the fulfilment and therefore the liquidity provider could receive far fewer tokens (in the case of removing liquidity) or far fewer shares (in the case of adding liquidity) than they would expect under stable conditions.
Recommendation
Consider adding a
minAmountOutparameter to theLiquidityOrderParamsstruct.Resolution
MUX Team: Acknowledged.
-
M-07 Medium No Post-Deposit Safety Check In depositCollateral Function Configuration Acknowledged
Description
The
depositCollateralfunction allows users to add collateral to an existing position but fails to verify whether the position is then sufficiently collateralized or safe from liquidation immediately afterward.A user might deposit too little collateral, resulting in a position that still remains under collateralized and can be liquidated right away.
Recommendation
Include a safety check after the collateral deposit to confirm that the position’s margin requirements are now satisfied. If the position is still unsafe, revert the transaction.
Resolution
MUX Team: Acknowledged.
-
M-08 Medium Position Fee Not Factored Into Margin Logical Error Acknowledged
Description
During liquidation, when
_isMaintenanceMarginSafeis called, pending borrow fees are included in the required Maintenance Margin (MM). However, the position fee is not factored into the MM calculation.Since this position fee depends on the position value, it can sometimes be substantial. In such cases,
_isMaintenanceMarginSafemight return true, even when the collateral is insufficient to cover the position fee.Despite this, the liquidation proceeds due to
shouldCollateralSufficient = falsein_dispatchPositionFee, leading to unpaid fees. This results in lost fee revenue for LPs and veMUX holders.Recommendation
Update
_isMaintenanceMarginSafeto account for the liquidation position fee as part of the collateral sufficiency check.Resolution
MUX Team: Acknowledged.
-
M-09 Medium Empty Collateral Can Be Activated Logical Error Resolved
Description
Market._realizeProfit()callsCollateralPool.realizeProfitwhich charges a given amount of the pool's collateral token from the LPs in order for the trader to be paid. After that, the charged collateral token is added towards theactiveCollateralsof that trader.This is done regardless of the charged amount. Because
CollateralPool.realizeProfitconverts from USD to the collateral token and divides multiple times, it's possible to have positive PnL in USD, but the result in collateral token to be 0.When this happens, LPs won't be charged and the trader won't profit, but the collateral token of the pool will still be added to their
activeCollaterals.This will result in users having tokens with 0 balance in their
activeCollaterals, which:- Increases unnecessarily their collateral tokens count, making it harder to add new tokens.
- Can block the fulfilment of a close order which withdraws USD because
withdrawUsdgoes over
each active collateral and tries to withdraw from it by passing the needed amount to
_withdrawFromAccount. Inside that internal function there is arequireassertion that will revert the transaction if the amount is 0.Recommendation
In
Market._realizeProfitadd thecollateralTokentoactiveCollateralsonly ifcollateralAmountis not 0.Resolution
MUX Team: Resolved in commit 4159688f031436f551b843a4d84c89b1e326f861.
-
M-10 Medium donateLiquidity Should Update Market Borrowing Logical Error Acknowledged
Description
Whenever a pool's liquidity changes, the
updateMarketBorrowingfunction must be called to snapshotcumulatedBorrowingPerUsd. This ensures the borrowing costs are accurately tracked and distributed across the pool participants.Currently, the
donateLiquidityfunction does not perform this update. While this omission may not pose a risk when fees are donated through the fee distributor—since borrowing is updated during add/remove liquidity steps—donateLiquiditycan also be called directly by external parties.This could lead to an inconsistency in
cumulatedBorrowingPerUsd, particularly if significant liquidity is donated without triggering the borrowing update, impacting the fair distribution of borrowing costs.Recommendation
Ensure that
updateMarketBorrowingis called within thedonateLiquidityfunctionResolution
MUX Team: Acknowledged.
-
M-11 Medium Broker Should Be Paid For Cancelling Orders Logical Error Acknowledged
Description
Brokers have the ability to cancel stale position and withdrawal orders and the gas fees incurred for these cancellations are refunded to the user who created the order.
This design leaves brokers unreimbursed for the gas costs of performing the cancellations. A malicious actor can exploit this by creating multiple orders with unrealistic limit prices, ensuring that the orders never get filled and eventually go stale.
The broker would then be forced to expend gas to cancel these orders repeatedly, incurring significant costs without compensation.
Recommendation
Deduct a portion of gas fees to reimburse the broker when orders are cancelled.
Resolution
MUX Team: Acknowledged.
-
M-12 Medium Borrowing State Not Updated Before Config Change Logical Error Acknowledged
Description
Borrowing params such as
baseApycan be changed by admin inFacetManagement.setConfig. However, when borrowing params are changed, it affects the interest accrued by users since the last update time.For example, if
baseApywas increased, users will be unfairly charged the higher rate since the last update.Consider this scenario: T0 (last updated timestamp):
baseApy= 1%
T5:
- Admin updates
baseApyto 2%
T10:
- Trader closes position, borrowing rate is based on
baseApy2%, when it should have been 1% from
T0 - T5 and 2% from T5-T10
Recommendation
Call
pool.updateMarketBorrowingbefore changing any borrowing params.Resolution
MUX Team: Acknowledged.
-
M-13 Medium Gaming Capped Pnl By Increasing Position Logical Error Acknowledged
Description
Proof of concept: PoC
Traders’ PnL is capped based on
_adlMaxPnlRate, and the capped PnL is used when realizing profits.uint256 maxPnlRate = _adlMaxPnlRate(marketId); uint256 maxPnlUsd = (size * entryPrice) / 1e18; maxPnlUsd = (maxPnlUsd * maxPnlRate) / 1e18;The capped PnL is calculated as above, with
maxPnlUsdbeing directly influenced by the average entry price and the position size.When a trader’s PnL is higher than the capped PnL, the trader can intentionally increase their position at the current price just before closing it, then immediately close the position.
This will result higher profit for the trader without any additional risk since both the
sizeandentryPriceare momentarily inflated by the trader, causingmaxPnlUsdto be higher.Recommendation
Consider implementing a grace period before allowing a position to be closed when a trader increases their initial position. This measure would discourage malicious traders by introducing an increased risk of loss during the grace period.
Resolution
MUX Team: Acknowledged.
-
M-14 Medium Imprecise PnL In realizeProfit Logical Error Resolved
Description
Market._realizeProfitaccepts apoolPnlUsdamount that should be realized as a profit in the collateral pool. The samepoolPnlUsdis being returned from the function asdeliveredPoolPnlUsdand will later be saved in the correspondingClosePositionResult/LiquidatePositionResultstruct.However, when
Market_.realizeProfit()callsCollateralPool.realizeProfit, the pool recalculates the wad amount of collateral for tokens with different decimal tokens.This will cause a discrepancy between the real USD value realized as profit and the value assigned to
deliveredPoolPnlUsdfor tokens with less than 18 decimals.This can cause some close orders with
isWithdrawProfit = trueto revert because thedeliveredPoolPnlUsdwill be added towards thewithdrawUsdthat the order book will try to withdraw from the user's collateral.Recommendation
Recalculate the actual USD profit value based on the
collateralAmountreturned fromCollateralPool.realizeProfitand assign that new value todeliveredPoolPnlUsd.Resolution
MUX Team: Resolved in commit b358c86d36239854f5ef8dffca1694a490009928.
-
M-15 Medium Wrong Pause Check For Liquidations Logical Error Resolved
Description
OrderBook.liquidate()liquidates a user by closing their position for a given market and potentially withdrawing their collateral if their MM is above their margin balance. This allows the Broker to keep the market in a healthy state.Currently,
OrderBook.liquidate()will execute thewhenNotPausedmodifier and revert ifLiquidityOrderis paused.LiquidityOrderhas nothing to do with liquidate even though their names are similar.In result, if the market has the liquidity orders paused to manage some type of risk, the broker won't be able to liquidate position accounts.
Recommendation
Consider removing the
whenNotPausedmodifier fromOrderBook.liquidate(). There is already a_marketDisableTrade()inFacetClose.liquidatePosition()which will stop the liquidation if this is a desirable feature.Resolution
MUX Team: Resolved in commit/s bd5511cc3be7515304dc67c9306585e5eee9aa54, 53dd46cab5f62b6f14808b389acb6fe0de0f358b.
-
M-16 Medium WithdrawUsd May Try To Withdraw 0 Logical Error Resolved
Description
FacetPositionAccount.withdrawUsdcalculates the amount ofpayingCollateralto withdraw from the user's account by calling_withdrawFromAccount. It's possible forpayingCollateralto result in 0 because of rounding if the user has small amount of collateral (may happen naturally by realizing negative PnL).If this happens, the whole transaction will revert since
_withdrawFromAccountfails if the amount to be withdrawn is 0. In result, the user won't be able to usewithdrawUsd.Recommendation
Skip the current iteration if the amount of collateral to be withdrawn is 0.
Resolution
MUX Team: Resolved in commit 8049b41bdce3e251e0660ecf5c614ba20a4c29a5.
-
M-17 Medium Brokers Can Be Gas Griefed Configuration Acknowledged
Description
Users place orders in the order book and pay a gas fee to compensate the broker that fulfils their orders. Users can also cancel their orders after a given time passes. When they do that, they will be refunded the whole gas amount they paid to compensate the broker.
This can be used to place multiple always reverting orders. For example, withdrawal orders with slippage over 100%, liquidity orders that exceed the liquidity cap, or just normal orders that happen to revert.
The broker will try executing them, which means it will pay gas for execution for the reverting transaction. The user can then cancel all their orders and get their funds back, resulting in lost funds for the broker.
Recommendation
Consider splitting the paid gas in two parts - send the first part back to the user and the second part to the broker to compensate them for any order they have tried fulfilling.
Resolution
MUX Team: Acknowledged.
-
M-18 Medium MAX_COLLATERALS_PER_POSITION_ACCOUNT Can Be Bypassed Logical Error Acknowledged
Description
When closing a position, collaterals are added to the user's account, without validation. If a user only partially closes their account, they will then have all of these collaterals as active.
When going to deposit collateral, the validation inside of
_depositToAccount()will see that the collateral is already active and not revert even though the user is using more collateral assets than allowed.Recommendation
If a user is not fully closing their position, validate that they are not exceeding the
MAX_COLLATERALS_PER_POSITION_ACCOUNT.Resolution
MUX Team: Acknowledged.
-
M-19 Medium Inaccurate aumUsd When Removing Liquidity Logical Error Acknowledged
Description
When removing liquidity from the collateral pool, a check is performed to ensure that the remaining asset value is sufficient to cover the
reservedUsdamount. This check is implemented usingrequire(reservedUsd = aumUsd).This
aumUsdvalue being compared toreservedUsdis calculated by subtracting the value of the removed asset from the pool’s previousaumUsdvalue:aumUsd = removedValue. However, after this calculation, a portion of theremovedValueis deposited back into the pool during_distributeFee.Normally, this deposited amount increases the pool’s
aumUsd. However, this increase is not accounted for, making theaumUsdvalue used in therequire(reservedUsd = aumUsd)statement inaccurate.As a result, liquidity removal may fail incorrectly with an
InsufficientLiquidityerror, even though the actualaumUsdis sufficient to coverreservedUsd.Recommendation
Re-added fees should be accounted for during the calculation.
Resolution
MUX Team: Acknowledged.
-
M-20 Medium Signature Replay In MuxPriceProvider Contract Configuration Resolved
Description
The
getOraclePricefunction in theMuxPriceProviderrequiresoracleData.sequenceto be greater than the sequence of the contract. It doesn’t specifically require it to be the next sequence, nor does it require it to be greater than the sequence of the last accepted oracle data.When a valid signature sequence is beyond the contract’s sequence, this signature can be used repeatedly by the broker until the sequences match. For example, if the current contract sequence is 4 and oracle data with a sequence of 10 is used, the contract sequence will be updated to 5.
Another oracle data with a sequence of 8 would still be considered valid. Assuming this is expected behavior, any broker can repeatedly call the
getOraclePricefunction, increment the contract's sequence to 10, and render the oracle data with a sequence of 8 invalid.Recommendation
If oracle data is expected to be ordered and the behavior described above is not acceptable, either enforce the oracle sequence to be the exact next sequence or update the contract sequence to the latest accepted oracle data.
This ensures that oracle data with a smaller sequence will not be valid. If unordered oracle data is the intended behavior, consider implementing access control for the
getOraclePricefunction.Resolution
MUX Team: Resolved in commit 57cd9a513eb7a9418903b5121592f14bcd54c283.
-
M-21 Medium ADL Is Triggered Per Pool Configuration Acknowledged
Description
When
fillADLOrderis called, it validates that the position is viable for an auto deleverage. This is accomplished inisDeleverageAllowedfunction when it checks if any of the pools that the user has their position in is above the trigger PnL threshold.The issue is that a user could have a position, in its entirety, not exceed the trigger threshold, but has exceeded the threshold in a singular pool. This will cause the entire position to be auto deleverage, even if their position has negative PnL.
Recommendation
Validate that the entire position is in a profitable enough state to trigger the auto deleverage.
Resolution
MUX Team: Acknowledged.
-
M-22 Medium Broker Can Abuse oracleSigner Signatures Configuration Resolved
Description
The
MuxPriceProvidercontract does not includeoracleData.oracleIdin the message that is signed and verified. The signed message is built as:bytes32 message = ECDSAUpgradeable.toEthSignedMessageHash(keccak256(abi.encodePacked( Block.chainid, address(this), oracleData.sequence, oracleData.price, oracleData.timestamp)));Because the
oracleIdis not part of the signed data, a malicious broker can exploit this by taking a valid signature intended for one asset and using it to update the price of another asset.By calling the
setPricesfunction with a differentoracleIdbut supplying the sameoracleDataand signature, they can manipulate the price feeds of other assets.Recommendation
The
oracleIdmust be included in the message that is signed by theoracleSignerand verified in thegetOraclePricefunction. This ensures that each signature is uniquely tied to a specific asset.Resolution
MUX Team: Resolved in commit 31473d705b8caaaa9a09d76e83b40a66920ad443.
-
M-23 Medium Zero Balance Active Collaterals Configuration Resolved
Description
The
_depositToAccountfunction checks the raw amount is not zero, converts raw amount towadamounts, and then adds collateral to the active collaterals. However, if the collateral's decimal value is higher than 18,wadamount can be zero while the raw amount is not zero.Since the balances are tracked as
wadin the protocol, this collateral will still be added to active collaterals even though thewadbalance is zero. Similarly, there will be some precision loss when withdrawing collaterals if the decimal value of the collateral is higher than 18.Recommendation
Check the
wadamount is not zero as well before adding collateral to the active collaterals.Resolution
MUX Team: Resolved in commit 34dc6509682c10b1a89269d5a74e9fa31ef4eb29.
-
M-24 Medium Profit Can Only Be Realized With The Pool’s Collateral Token Logical Error Partially resolved
Description
The
CollateralPoolcontract calculates its overall assets under management by summing balances of its primary collateral token and any additional tokens that arrive via fees, trader losses or rebalance operations. Although traders can open positions based on this total multi-token value, the code that settles profit only checks the availability of the main collateral, seeCollateralPool.realizeProfitfunction below.When traders attempt to realize profits, if there is insufficient balance of the primary collateral, even though other tokens in the pool collectively exceed the required amount, the function
realizeProfitwill revert, blocking some traders from closing and profiting from their positions due to the following require check:require(wad = _liquidityBalances[token], InsufficientLiquidity(wad, _liquidityBalances[token]));The expected approach would be for rebalancers to convert these other tokens into the main collateral token, but there is no guarantee that this can be done quickly as it requires that the pool1’s foreign token is held by any of the other pools. Therefore a significant time gap might occur before rebalancing is performed.
Furthermore, even if the protocol attempts to swap these foreign tokens for the main collateral on, for example through Uniswap, the pool would have to pay a high fee of around 5%, causing the
CollateralPoolto lose a portion of its AUM with each of those swaps.Recommendation
Consider updating the
realizeProfitfunction so trader’s PNL can be paid with multiple tokens. These tokens should be any supported collateral token by the protocol and held by the actual collateral pool.Resolution
MUX Team: Partially Resolved in commit ff0bd47258a1158414737c222e8164d031071cac.
-
L-01 Low Broker’s Multicall Order Might Affect BorrowingRates Configuration Acknowledged
Description
When a broker executes operations within a single
multicall, including setting token prices, fulfilling position orders, and then processing liquidity orders, the intended stability of borrowing rates can be compromised.Position order fulfillment involves an allocation/deallocation algorithm designed to maintain balanced borrowing rates across collateral pools.
However, if a liquidity removal order is executed at the end of the same
multicall, it immediately changes the utilization within a specific pool and thereby disrupts the previously established equilibrium.This abrupt shift in utilization causes the borrowing rate to deviate, in at least one of the pools, significantly from the rate conditions calculated and maintained by the allocation algorithm.
Recommendation
Every time a broker fulfills orders within a
multicall, the sequence of operations significantly affects the equilibrium of borrowing rates across collateral pools.If liquidity removal occurs after position order fulfillment, it can distort the carefully balanced borrowing conditions achieved through the allocation algorithm.
Therefore, consider executing position orders last in the
multicallarray to ensure that the utilization and borrowing rates stay as balanced as possible.Resolution
MUX Team: Acknowledged.
-
L-02 Low Unrecoverable Revert In Broker’s Multicall Execution Configuration Acknowledged
Description
The brokers will make use of the
OrderBook.multicallfunction to execute a sequence of order-related operations, including setting prices, filling position orders and processing liquidity or rebalance orders.However, if any single call within the
multicallsequence fails, the entire transaction reverts and all previously completed operations within the samemulticallare effectively rolled back.This all-or-nothing behavior may cause unnecessary transaction failures when certain orders depend on the outcomes or state changes from preceding calls.
Recommendation
Consider introducing an extra parameter in the
multicallfunction calledcanFail. If this parameter is set totruefor a certain call, do not revert in case of failure and continue with the execution of the remaining calls.Resolution
MUX Team: Acknowledged.
-
L-03 Low Tokens That Do Not Support IERC20Metadata Interface Can Not Be Added As Collateral Code Best Practices Acknowledged
Description
The
_retrieveDecimalsfunction unconditionally callsIERC20MetadataUpgradeable(token).decimalswithin a try/catch block.While this appears to handle errors gracefully, there are still some scenarios where the call will still revert:
- If the address called is not a smart contract.
- If the address called implements the
decimalsfunction but does not return anuint8.
Recommendation
Merely informative issue.
Resolution
MUX Team: Acknowledged.
-
L-04 Low Direct Delete Of EnumerableSetUpgradeable.UintSet Struct In _cancelActivatedTpslOrders Function Code Best Practices Resolved
Description
Within the
_cancelActivatedTpslOrders function, the contract explicitly calls deleteorderBook.tpslOrders[positionId][marketId]on anEnumerableSetUpgradeable.UintSetstructure.As per the Openzeppelin documentation: “Trying to delete such a structure from storage will likely result in data corruption, rendering the structure unusable.”
The exact impact would be:
- contains() returns true for previously stored values. It should not be a problem as it’s never used in
the codebase for the
tpslOrdersstruct.- add() behaves as if the set still contains values however only new
orderIds(in an ascending order)
will be added, so it is not an issue.
- Length of the set is reset to 0, even though there are still some elements that are part of it. In the
current implementation this is not a problem.
Recommendation
At present, no immediate security issues arise from this approach. However, for clean design and to prevent complications in future upgrades, it is safer to clear each item in the set via
removerather than deleting the structure outright.Resolution
MUX Team: Resolved in commit b4d1a45090f051ee896cfd13921e036f391bca8d.
-
L-05 Low Liquidity Can Be Still Added To Pools On IsDraining State Code Best Practices Resolved
Description
The
CollateralPoollogic currently permits users to calladdLiquidityeven for pools that have theirisDrainingflag set. This state indicates that the protocol aims to reduce usage or permanently wind down the pool by only allowing deallocations.Therefore, the borrowing rates there can just decrease as the pool’s utilization will only decrease as the time passes. As the pool will eventually be deprecated, it is recommended to restrict the addition of any new liquidity.
Recommendation
Enforce a condition in the
addLiquidityfunction to reject deposit attempts when a pool is flagged asisDraining.Resolution
MUX Team: Resolved in commit 22e464b5636ef5c4dbcd16836e8eab99a018769e.
-
L-06 Low Deallocate2 Function Does Not Prioritize Pools In IsDraining State Configuration Acknowledged
Description
In the current
deallocate2implementation, the algorithm calculates how much liquidityxTotalshould be proportionally removed from each pool. However, there is no consideration for whether a pool is in anisDrainingstate.When a pool is flagged as draining, the intention is that liquidity should be prioritized for removal or deallocation, allowing that pool to wind down more quickly.
By simply performing a proportional distribution based on
mySizeForPool, the function may remove only a minimal portion from a draining pool, rather than ensuring that the draining pool’s allocation is decreased as much as possible to effectively close it out or free up its usage.Recommendation
Similarly to how the allocation algorithm ignores pools in an
isDrainingstate, the deallocation algorithm should be updated to prioritize pulling liquidity from pools that are in anisDraningstate first.Resolution
MUX Team: Acknowledged.
-
L-07 Low CollateralPoolAumReader May Provide Divergent AUM Values Compared To The MUX3 Protocol Configuration Partially resolved
Description
The
CollateralPoolAumReadercontract is designed to let external projects (e.g., lending protocols) obtain an approximate “AUM” (Assets Under Management) for a given collateral pool.It accomplishes this by reading Chainlink price feeds, whereas the core of the MUX3 protocol itself uses Chainlink data streams, which can produce slightly different price values than standard Chainlink aggregator feeds.
Because these two separate sources may yield inconsistent quotes for the same token, there is a risk that external contracts relying on
CollateralPoolAumReaderobtain an AUM measurement that deviates from the actual in-protocol valuation.Inaccurate external references to the AUM can, in turn, lead to misguided lending rates, incorrect collateralization checks.
Recommendation
Projects that build on top of
CollateralPoolAumReadershould be aware of the possibility of discrepancies between theCollateralPoolAumReader’sAUM and the internal MUX3 protocol calculations.To minimize the resulting risk of mispricing or suboptimal collateral usage, external integrators are advised either to adopt the same data source as MUX3’s core or to introduce their own feed validation and tolerance checks, ensuring that any gap between the two sets of price data will not cause any impact on the protocol logic.
Resolution
MUX Team: Partially Resolved in commit 3056b7c351e554b1d3004d7d1584eaadeb54be4a.
-
L-08 Low Lack Of A Double Step TransferOwnership Pattern Code Best Practices Resolved
Description
The current ownership transfer process for all the contracts inheriting from the
OwnableUpgradeablecontract involves the current owner calling thetransferOwnershipfunction.If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the
onlyOwnermodifier.Recommendation
It is recommended to implement a two-step process transfer ownership process where the owner nominates an account and the nominated account needs to call an
acceptOwnershipfunction for the transfer of the ownership to fully succeed.This ensures the nominated EOA account is a valid and active account. This can be easily achieved by using OpenZeppelin’s Ownable2Step contract.
Resolution
MUX Team: Resolved in commit 8562e574bc92bcc2a6efa2f110fb55f1debd5b35.
-
L-09 Low Missing ReentrancyGuard Initialization Code Best Practices Resolved
Description
The
OrderBookcontract inherits fromOpenZeppelin’sReentrancyGuardUpgradeablebut never calls the__ReentrancyGuard_initfunction in itsinitializemethod.Recommendation
Within the
initializefunction, ensure to invoke theReentrancyGuardinitializer:__ReentrancyGuard_init();Resolution
MUX Team: Resolved in commit 3c25d0a70bc0b2fe24e28990bf8485d60dba9f11.
-
L-10 Low Updating MM_LOT_SIZE Via setConfig Risks Precision Loss In Deallocation Algorithm Configuration Acknowledged
Description
The
MM_LOT_SIZEparameter should never be updated throughsetConfigonce there are positions open/allocated in the protocol because it directly affects the precision in the deallocation logic withinLibExpBorrowingRate. Thedeallocate2function evenly distributes a total sizexTotalacross several pools based on each pool’s proportion.If
MM_LOT_SIZEis updated, it will disrupt the prior assumptions about distribution granularity, causing significant rounding discrepancies when deallocating and therefore, in many cases, the deallocation would revert in the following require check present in the_deallocateLiquidityfunction blocking the closure or partial closure of user positions.Recommendation
Never update
MM_LOT_SIZEif there are any position open in the protocol.Resolution
MUX Team: Acknowledged.
-
L-11 Low Redundant Sequence State Variable In OrderBook Contract Code Best Practices Acknowledged
Description
In the
OrderBookcontract, there is a_storage.sequencefield which is incremented by theupdateSequencemodifier in several functions.Despite these increments, the contracts never actually uses this
sequencevalue to validate or order user operations; nor does any other function consume it to enforce conditions like strict ordering or replay protection.Consequently, this sequence state variable becomes effectively useless, as it increments without being referenced.
Recommendation
Consider removing the sequence variable altogether or implement an actual usage of it as a required parameter for ordering or replay checks.
Resolution
MUX Team: Acknowledged.
-
L-12 Low Incorrect updatedAt Timestamp In SusdsOracleL2.latestRoundData Function Configuration Acknowledged
Description
In the
SusdsOracleL2contract, thelatestRoundDatamethod includes a field namedupdatedAtthat is set toblock.timestamp.This is misleading because it implies that the price data was updated at the current block time on L2, whereas in reality, the update transaction originates on L1 via
SusdsOracleL1.The correct timestamp of the price update is the moment the message was relayed to L2 from L1, not necessarily the local
block.timestampof the L2 block executinglatestRoundData.As a result, the users may rely on a timestamp that does not accurately reflect when the underlying data (chi, rho, ssr) was actually refreshed.
Recommendation
Update the
SusdsOracleL2contract to store the last updated timestamp passed from L1 whenupdateFromL1is called, rather than relying onblock.timestamp. The L1-provided timestamp can be included alongsidechi_,rho_andssr_.For example, extend the
updateFromL1arguments to accept and record alastUpdatedvalue. Then, inlatestRoundData, return that stored time as theupdatedAt.This ensures that any consumer reading the
SusdsOracleL2receives a consistent and correct notion of when the oracle data was actually updated.Resolution
MUX Team: Acknowledged.
-
L-13 Low Free Borrowing In Interval Logical Error Acknowledged
Description
Proof of concept: PoC
Traders must pay a borrowing fee for their positions, which is calculated based on the fee rate, duration, and position size. These borrowing rates and the cumulative borrowing per USD value are regularly updated when positions are opened or closed.
The
_updateMarketBorrowingfunction performs the necessary calculations based on theMC_BORROWING_INTERVAL(currently set to 1 hour).During these calculations, the
nextFundingTimevalue is rounded down to the nearest interval timestamp using the formulanextFundingTime = (blockTime / interval) * interval. This value is then assigned tomarket.lastBorrowingUpdateTime.If
market.lastBorrowingUpdateTime + interval = blockTime, the cumulative borrowing per USD will not be updated. This allows traders to exploit the system by opening and closing positions within the same interval without incurring any borrowing fees, regardless of the position size.With the current configuration, traders can freely borrow large amounts for approximately one hour without paying any borrowing fees by timing their actions.
T0 T0 + 1 hour |.......................................................................| | | Open position Close positionRecommendation
Charge every position at least one interval rate regardless of the position's duration. Alternatively, consider implementing a fixed borrowing fee that is independent of the duration, and then adding a time-based fee on top of this fixed fee.
Resolution
MUX Team: Acknowledged.
-
L-14 Low Some Orders Cannot Be Paused Configuration Resolved
Description
When orders are placed and filled in the
OrderBook, thewhenNotPausedmodifier is executed first. It will revert if the current order type is paused. Currently,_isOrderPauseddoesn't supportRebalanceandAdlorders.Because of this it will always return
falseand the modifier will always let the execution begin. In result,RebalanceandAdlorders can be placed or filled at any point in time, even though the associated functions use thewhenNotPausedmodifier.Recommendation
Make the
isOrderPaused()function fetch paused configuration forRebalanceandAdlorders as well.Resolution
MUX Team: Resolved in commit 53dd46cab5f62b6f14808b389acb6fe0de0f358b.
-
L-15 Low Rebalance Orders Cannot Be Canceled Configuration Resolved
Description
The
LibOrderBook.cancel()function should be able to cancel placed orders. Currently there is not support for canceling a rebalance order. Because of this any expired rebalance orders will stay forever in the contract state.Recommendation
Consider allowing the broker to cancel rebalance orders.
Resolution
MUX Team: Resolved in commit 70771d6ac481f4b40ab5ccb5829ef5a5034f4755.
-
L-16 Low Use Chainlink Stream's Bid/Ask Price Logical Error Acknowledged
Description
getOraclePricereturns price fromverifiedReportwhich is the mid-price (median of the bid and ask prices). While this is acceptable in stable market conditions, during periods of high volatility, the bid-ask spread can widen significantly.References: Chainlink recommends using liquidity-weighted price over mid-price:
https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices
Synthetix studies past data of bid-ask price spreads: https://sips.synthetix.io/sips/sip-398/
Recommendation
Use the liquidity-weighted
bid/askprices provided in theverifiedReport, especially for the settlement of trades.Resolution
MUX Team: Acknowledged.
-
L-17 Low Unfillable TP/SL Orders Remain In The List Logical Error Acknowledged
Description
When opening a position, users can create
tpslorders that will be filled in the future. The size of atpslorder is the same as the size of the position when it is opened.These
tpslorders will be cancelled in the_fillClosePositionOrderfunction only if the market is fully closed for that position account. When a user partially closes a position, thesetpslorders cannot be filled due to a mismatch between the remaining position size and thetpslorder size.However, these orders will still remain in the
tpsllist since the market is not fully closed. As a result, a position account might reach theMAX_TP_SL_ORDERSlimit as some of these orders can never be filled.Recommendation
Consider removing
tpslorders even when partially closing a position, document this behavior and request users to create newtpslorders if they update their positions along the way.Resolution
MUX Team: Acknowledged.
-
L-18 Low wrapNative Should Not Be Called Directly Configuration Resolved
Description
The function
wrapNativewas intended to be used as part of a multi-call to wrap ETH before it is deposited as gas or collateral. However, if it is called directly the user would end up losing that ETH as anyone can calldepositGasto take the WETH that was left in the contract.Recommendation
Consider documenting this risk for users.
Resolution
MUX Team: Resolved in commit 0f81979f86bb4c732d4dae5b7f21ec2a36136206.
-
L-19 Low Fees Collected Should Round Up Logical Error Acknowledged
Description
Fees calculated in
_collectFeeFromCollateralare rounded down after division. IffeeUsdis small enough, it may round down to zero and allow the user to avoid paying a fee. This also applies to_borrowingFeeUsdand_updatePositionFee, to a smaller extent.Recommendation
Always round up when calculating fees.
Resolution
MUX Team: Acknowledged.
-
L-20 Low Withdrawals Fail If Path Is Not Set Configuration Resolved
Description
When users withdraw their collateral, they can specify a
withdrawTokento receive. For example, USDC collateral may be withdrawn as DAI. To perform the swap,Swapperuses Uniswap.In
swapAndTransfer, if there is nopathconfigured for the given pair oftokenIn - tokenOut, the transaction will revert. This will cause any withdrawal request from token A to token B where such swaps are not supported, to fail.Recommendation
Consider sending the
tokenInto the receiver if no path is configured.Resolution
MUX Team: Resolved in commit 216f2ae29a60117a54959846ad5a4ccb989f41ca.
-
L-21 Low Borrowing Rate Loses Precision Configuration Resolved
Description
LibExpBorrowingRate.getBorrowingRate2calculates the variable apy ase^(k * utilization + b). Currently, both rounding down and division before multiplication happen when calculating the utilization.Because of this, the utilization ends up being less than it should actually be. When later multiplied by
k, this will lead to a smaller positive number to be added to the negativeb.The whole expression
k * utilization + bwill yield smaller negative number, therefore the variable apy will end up smaller than it should be.Recommendation
Consider multiplying before dividing.
For example:
fr = pool.poolSizeUsd > 0 = pool.k * pool.reservedUsd / pool.poolSizeUsd + pool.b : pool.b;Resolution
MUX Team: Resolved in commit 128aed0c42374f5cf5555e0d4ac388099fb22ded.
-
L-22 Low Delegators Can Not Place Liquidity Orders Configuration Acknowledged
Description
Currently, delegators can place position orders and withdraw orders, but there is no support for liquidity orders.
Recommendation
Consider adding support for placing liquidity orders as well.
Resolution
MUX Team: Acknowledged.
-
L-23 Low Lack Of Fee On Transfer Support Configuration Acknowledged
Description
The protocol doesn't support fee on transfer tokens - it takes it for granted that the received amount is always the transferred amount. If such tokens were to be added, MUX would be broken.
Recommendation
Don't use fee on transfer tokens with the current version of the project.
Resolution
MUX Team: Acknowledged.
-
L-24 Low Withdrawal Slippage May Exceed 100% Configuration Resolved
Description
When users place withdrawal orders in
LibOrderBookthey may specify a desiredwithdrawSwapSlippagein percents with 18 decimals precision. This parameter is not validated and can be set to any validuint256.If its value is greater than
1e18, the order will always revert on fulfillment.Recommendation
Consider adding a validation in
placeWithdrawalOrderthat the slippage must not exceed1e18.Resolution
MUX Team: Resolved in commit 3936e7fda7064e867ad3a875359eed0e5f13f92b.
-
L-25 Low Conditional Token Check DoS Resolved
Description
LibOrderBook.placeWithdrawalOrderchecks if the passedtokenAddressis a valid collateral token only if it's notaddress(0).This means
address(0)can be passed as token to be withdrawn. Such orders will always revert on fulfillment because there is the same check in_withdrawFromAccounthowever it's unconditional -the token must be a collateral token no matter if it'saddress(0).In result, users putting such orders will pay gas to the broker for executing an always reverting order.
Recommendation
Make the check in
LibOrderBookalways revert if the token to be withdrawn is not a collateral token, no matter if it'saddress(0).Resolution
MUX Team: Resolved in commit baadcd0eb79d1dc9ea664f5d1df5c09611364c72.
-
L-26 Low Collateral Can Be Added To 0 Address Code Best Practices Resolved
Description
There is a check in
PositionAccount._depositToAccount()that ensures thepositionIdis not 0. This will pass successfully even if the account passed is address(0), but one of the other 12 bytes is not 0. In result, users can deposit collateral for address(0).Recommendation
Be aware of this behavior.
Resolution
MUX Team: Resolved in commit 43bea77fee396cc2856cef7a19fec6856732dbe0.
-
L-27 Low Unnecessary Check In _traderTotalUpnlUsd Code Best Practices Resolved
Description
In the function
_traderTotalUpnlUsd, the check:require(maxPnlRate > 0,IErrors.EssentialConfigNotSet("MCP_ADL_MAX_PNL_RATE"));is unnecessary because it was already performed in_adlMaxPnlRate.Recommendation
Remove the check for
maxPnlRate > 0.Resolution
MUX Team: Resolved in commit 221e1e48f09ff6f199f682e8862c758b6d6f6c5a.
-
L-28 Low Incorrect Event Data In donateLiquidity Logical Error Resolved
Description
If an external party calls
donateLiquidity, thereceiveFeefunction inCollateralPoolis called which emits two events withcollateralPrice.However, this price would be inaccurate as it relies on the broker to update token price, which is stored transiently in each block. If the call did not originate from the broker, price would be zero.
Recommendation
Consider restricting calls to
donateLiquidity, so that it is certain that the call originated from the broker. Or else, document this risk for any external parties that rely on this event data.Resolution
MUX Team: Resolved in commit e8b69415c11222eb44060e5bf341a18b7f3cbe6d.
-
L-29 Low Excess Collateral Not Refunded Logical Error Acknowledged
Description
When adding liquidity, a user is expected to first transfer tokens into the order book before calling
placePositionOrder. However, when the action is filled viafillPositionOrder, there is no refund mechanism if the tokens transferred in exceeds the order'srawAmount.Any excess tokens will remain in the contract and can be taken by the next user.
Recommendation
In
_transferIn, consider refunding the difference betweenrealRawAmountandrawAmount. Or else, document this risk for users.Resolution
MUX Team: Resolved in commit 73d1534fb2587fcff5bd6f8dfcdb2a1c741df553.
-
L-30 Low Gaps Are Not Adding Up To 50 Code Best Practices Acknowledged
Description
There are upgradability gaps left in the MUX contracts. These gaps don't follow the common practice of adding up to 50.
Recommendation
Be aware of this
Resolution
MUX Team: Acknowledged.
-
L-31 Low Redundant Function Code Best Practices Resolved
Description
_isAccountExistfunction in thePositionAccountcontract is exactly the same as_isPositionAccountExist, and not used anywhere in the codebase.Recommendation
Remove redundant function.
Resolution
MUX Team: Resolved in commit 22ed68cb7deb2cc59e62d3cf2f513fa0371af728.
-
L-32 Low addLiquidity Revert Due To Divide By 0 Configuration Resolved
Description
When users want to add liquidity to a collateral pool, the LP share price of that pool is calculated. This calculation is performed based on
_aumUsd, which includes PnL._aumUsdreturns 0 when traders’ total PnL is greater than underlying USD value of the pool.In the
addLiquidityfunction,lpPricewill also be 0 when_aumUsdis 0. This will cause a revert with divide by 0 error later in the function since share amount to mint to the user is calculated withresult.shares = (collateralAmount * collateralPrice) / lpPrice.Recommendation
There is no security consideration as long as
adl_max_pnl_rate < 100%since the total PnL of all traders is also capped with this rate. However, be aware of this in case of updatingadl_max_pnl_rateconfiguration values in the future.Resolution
MUX Team: Resolved in commit 7515b499d4ac71cf6458dabec645c7f745fa9a0f.
-
L-33 Low Missing Withdraw Function In ChainlinkStreamProvider Code Best Practices Resolved
Description
One of the price providers in the codebase is
ChainlinkStreamProvider, which verifies oracle data using the Chainlink verifier. This verification requires a fee, which is paid using theLINKtoken.The
ChainlinkStreamProvidercontract must maintain a balance ofLINKtokens to cover these fee payments. However, the contract does not include a function that allows the owner to withdraw LINK balances if necessary.Recommendation
Consider implementing a withdraw function.
Resolution
MUX Team: Resolved in commit e3fd954a04921fbb279c355ef424818f1c6f8b00.
-
L-34 Low Incorrect View Functions In CollateralPool Logical Error Resolved
Description
The
getAumUsdWithoutPnlandgetAumUsdfunctions are external view functions in theCollateralPoolcontract. These functions read token prices from theFacetReaderto perform calculations.However, the prices are written to and read from transient storage. As a result, these functions will return incorrect values when called by external users.
Recommendation
These functions should be removed from the
CollateralPool. External users should instead utilize theCollateralPoolAumReader, where prices are fetched from oracles.Resolution
MUX Team: Resolved in commit d3657f5e4b12bd37779ef54103c0453a76f85eee.
-
L-35 Low Unused Internal Functions Code Best Practices Resolved
Description
The
_isMaintainerand_balancefunctions in theOrderBookGettercontract, as well as the_validateCollateralfunction in theMux3FeeDistributorcontract, are internal functions but never used.Recommendation
Consider removing unused functions.
Resolution
MUX Team: Resolved in commit e9a12903444d693631f0fcd01c420f7086305020.
-
L-36 Low Incorrect Comment In ChainlinkStreamProvider Code Best Practices Resolved
Description
The
Reportstruct in theChainlinkStreamProviderhas this comment: "DON consensus median price, carried to 8 decimal places". However, current Chainlink streams have 18 decimal places.Recommendation
Update the comment.
Resolution
MUX Team: Resolved in commit 626feebadd224dde3daf2a6d9f54800aa6e9c6ef.
-
L-37 Low Warning About isWithdrawAll Configuration Resolved
Description
The broker will pass the
isWithdrawAllboolean value during liquidations and adl orders. TheisWithdrawAllvalue must be set tofalseforadlorders if the position has multiple active markets (position index 0).Otherwise,
adlorders will fail, as the position will not be empty even after closing that market.Recommendation
The broker should keep this scenario in mind.
Resolution
MUX Team: The issue was resolved in commit 9383952.
-
L-38 Low Typo Code Best Practices Resolved
Description
"remove ths current order from tp/sl list" comment should be "remove the current order from tp/sl list" in
LibOrderBookcontractRecommendation
Update the comment
Resolution
MUX Team: Resolved in commit 24218462d453759c141b66ad2c6e039e13d1d7c0.
-
L-39 Low Some Transactions Update The Sequence Twice Configuration Resolved
Description
The
OrderBookhas a sequence as a storage value. Some transaction flows update this sequence twice along the way due to thedonateLiquidityfunction. This could cause issues if the off-chain part of the protocol expects the sequence to be updated one by one at all times.Recommendation
Be aware of this behavior.
Resolution
MUX Team: Resolved in commit 0e33a86d6a7952551cec822ef0a13f64c3f9cdf0.
-
L-40 Low Unexpected Fill Price When Trigger Open Configuration Resolved
Description
The fill price check is different for limit orders and trigger orders. Stop-loss orders are trigger and close orders, and the fill price check works correctly for these types of orders. However, users can also create trigger and open orders.
In this scenario, the current behavior and users’ expectations might differ. Currently, trigger open orders work in a way that triggers a long position when prices are already going up (expecting a momentum/breakout strategy).
However, users might expect a long order to trigger when prices are going down (expecting a bounce or reversal/dip-buying strategy). For example, the current price is 100, and the user expects the price to drop to 90 and then bounce back.
From the user’s perspective, a trigger open order in the long market at a price of 90 should create the order when the price reaches or goes below 90. However, this order would be immediately filled by the broker at the current price of 100.
Recommendation
Document this behavior and explain how trigger open orders work to prevent misunderstandings about these types of orders.
Resolution
MUX Team: Resolved in commit 92d115f8777e1abb5fcb88562d8ceb8b5bf90779.
-
L-41 Low Initial Leverage Is Mutable Configuration Acknowledged
Description
A user can change the initial leverage of their position once it is opened. This is accomplished by simply calling
setInitialLeverage()with a new value.This will allow users to withdraw more collateral than they would otherwise be allowed to via
placeWithdrawalOrder(), because the validation infillWithdrawalOrder()will use the leverage value passed after the position was created.Recommendation
Validate that the user is not modifying the initial leverage for a position that has already been opened.
Resolution
MUX Team: Acknowledged.
-
L-42 Low Referral Code Is Overwritten Configuration Acknowledged
Description
When users place position orders, they pass a
referralCodeparameter. After that, the code is passed to the referral manager'ssetReferrerCodeForfunction which will update the code of the trader in the manager contract.When position fees are to be paid, the code for the given trader is fetched from the referral manager and the recipient (therefore the tiers as well) are inferred from it.
There are 2 problems in the current implementation: 1. The code is being changed on each
placePositionOrder. 2. The fulfilment of the orders is asynchronous, so even if 1 didn't exist, the code may still have changed.For example, if a trader submits two position orders - the first with referral code A and the second with referral code B, at the time of fulfilment referral code B will be used for both orders.
Recommendation
Consider having the referral code encoded in the
orderParams.Resolution
MUX Team: Acknowledged.
No findings match.
Invariants 28
The review's fuzzing suite asserted 28 invariants. 22 held and 6 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
MUX-01 | Filling a position should not fail due to rounding | Broken |
MUX-02 | Collateral pool's aum is always greater or equal to reservedUsd | Held |
MUX-03 | aumUsd should never be 0 in a collateral pool when adl_max_pnl_rate < 1e18 | Held |
MUX-04 | The sum of the reserved USD of each pool backing a given market should be greater than or equal to the total size in USD of the market | Broken |
MUX-05 | multiplied by its reserve rate Since nextFundingTime is adjusted according to the interval, the timeSpan which is nextFundingTime - | Held |
MUX-06 | market.lastBorrowingUpdateTime should always be a multiple of the interval If a position account is fully closed, it should also be closed for any given market | Held |
MUX-07 | After opening a position, increase in market size should be equal to order's size | Held |
MUX-08 | withdrawUsd function should never revert with InsufficientCollateralBalance error when user's total collateral balance is sufficient (multiple | Held |
MUX-09 | active collaterals). withdrawUsd function should never revert with Invalid Amount error when user's total collateral balance is sufficient (multiple active collaterals). | Broken |
MUX-10 | A Liquidity order cannot be filled before the lock period has elapsed | Held |
MUX-11 | PositionAccount._positionPnlUsd function should never revert with AllocationPositionMismatch when closing a | Held |
MUX-12 | position Reserved USD in a pool should never exceed the pool's collateral value | Held |
MUX-13 | Market size should remain unchanged after reallocation operations | Broken |
MUX-14 | Total open interest in USD for a market must not exceed its configured cap | Broken |
MUX-15 | Position PnL must not exceed the maximum PnL rate cap when positive | Held |
MUX-16 | Changes in pool token supply must be reflected in liquidity changes | Held |
MUX-17 | Market total size must always be aligned with the configured lot size | Held |
MUX-18 | Pool parameters (size, k, reserve rate) must remain positive and allocations must not | Held |
MUX-19 | exceed pool size Pool allocations must stay within capacity limits and deallocation must not exceed | Held |
MUX-20 | previous allocation Borrowing rate parameters must be positive and utilization must not exceed 100% | Held |
MUX-21 | Total pool allocations must approximately match the market's total size in USD | Broken |
MUX-22 | Market lot sizes must remain constant during operations | Held |
MUX-23 | Market trade enabled status must remain constant during operations | Held |
MUX-24 | Market total allocations must match the sum of individual pool allocations | Held |
MUX-25 | Market size and pool allocations must be properly updated when positions are closed | Held |
MUX-26 | Position PnL must not exceed the pool's Assets Under Management (AUM) | Held |
MUX-27 | Position leverage must not exceed the maximum allowed leverage based on initial | Held |
MUX-28 | margin rate Position collateral must meet or exceed the required maintenance margin | Held |
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.