Organization
- @morpho
Engagement Type
Spearbit Web3
Period
-
Repositories
Researchers
Findings
Low Risk
7 findings
2 fixed
5 acknowledged
Informational
6 findings
1 fixed
5 acknowledged
Gas Optimizations
3 findings
1 fixed
2 acknowledged
Low Risk7 findings
Zero-value transfers revert MidnightBundlesV2 operations for nonstandard tokens
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Saw-mon and Natalie
Description
Let be the credit withdrawn before selling, where is the target, the caller's credit, and the market's withdrawable liquidity. Both
MidnightBundlesV2sell entrypoints callMIDNIGHT.withdraw(..., w, ...)unconditionally. For a borrower with no credit, and hence , even when the sell target is positive and valid offers are available. Midnight'swithdrawforwards this amount to the loan token. A token rejecting zero transfers therefore reverts the bundle before any offer is taken.The buy entrypoints likewise call
MIDNIGHT.repaywheneverrepayEnabledis true, even if offers completely filled the target or the caller has no debt. The computed repayment is zero; Midnight'srepayforwards it totransferFrom, reverting the entire bundle, including successful prior fills. These calls are outside the offer loop'stry/catch.Additional zero-value calls occur when the buy input (
maxBuyerAssetsortargetBuyerAssets) is zero, when a sell payout is zero, or when an entry incollateralSupplieshas zero assets. The maker and both sell entrypoints pull each collateral entry without a zero check. Unlike a zero initial credit withdrawal, callers can avoid zero collateral entries by omitting them.The consequence is failed bundle execution for the affected token and input combinations; no fund loss or failure of all direct Midnight operations is established.
NoteERC-20 requires zero-value transfers to be treated as normal transfers and emit
Transfer. The following deployments violate that requirement.Token Contract address / source Chain (ID) Market cap (USD) Result for both methods BNB, legacy ERC-20 0xb8c77482e45f1f44de1745f52c74426c631bdd52Ethereum (1) $103,583,805,704 (BNB-wide) Reverts without a reason string; source explicitly rejects _value <= 0.ETHLend / LEND, legacy token 0x80fb784b7ed66730e8b1dbd9820afd29931aab03Ethereum (1) N/A (reported supply unavailable) Reverts without a reason string; also documented in weird-erc20. HOGE 0xfad45e47083e4607302aa43c65fb3106f1cd7607Ethereum (1) $1,381,564 Transfer amount must be greater than zeroFLOKI 0xcf0c122c6b73ff809c693db761e7baebe62b6a2eEthereum (1) $275,799,677 FLOKI:_transfer:ZERO_AMOUNT: Transfer amount must be greater than zero.Baby Doge Coin / BabyDoge 0xc748673057861a797275cd8a068abb95a902e8deBNB Smart Chain (56), BEP-20 $76,854,601 Transfer amount must be greater than zeroSPX6900 / SPX 0xe0f63a424a4439cbe457d80e4f4b51ad25b2c56cEthereum (1) $408,965,974 Transfer amount must be greater than zeroLiquid Staked ETH / LsETH 0x8c1bed5b9a0928467c9b1341da1d7bd5e10b6549Ethereum (1) $847,024,814 NullTransfer()(0xdac85b6c)NGI+ 0xf252c5bd43907a6cab079e990845a37a7c5730d9Ethereum (1) N/A (CoinGecko contract lookup unavailable) Invalid amount; source requiresamount > 0.Collateral token Chain Dated markets Loan token Zero-transfer condition FLOKI Ethereum 5 USDC Zero amount rejected SPX6900 Ethereum 5 USDC Zero amount rejected LsETH Ethereum 5 USDC Zero amount rejected NGI+ Ethereum 5 USDC Zero amount rejected - View these fixed rate markets here: link
Recommendation
In
MidnightBundlesV2, callMIDNIGHT.withdrawonly whenwithdrawUnits > 0, and callMIDNIGHT.repayonly when the computed repayment is positive. Skip zero-value direct pulls and payouts, and omit zero-asset collateral supply operations. Preserve target, slippage, and authorization requirements. Add regression coverage with a token that rejects zero transfers for selling without existing credit and buying withrepayEnabledwhen offers fully fill the target.Morpho
We document that bundles inherits token requirements from midnight and blue. Midnight excludes tokens reverting on zero-transfer.
Speabit
But some of the Morpho listed markets use tokens that break those assumptions.
Morpho
Thanks for noticing, we will flag it to the engineering team. Even if we add checks here, there are no such checks in midnight itself, hence does not fully solve the issue. We prefer bundles to work under the assumption of standard ERC-20 tokens.
Spearbit
This is partially fixed in the final commit
07f293b383824b35adc745454ac2622a27eb0502for therepayandwithdrawendpoints to only calls those endpoints when the amount provided is non-zero.Ratifier Midnight instance is not checked
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Medium Submitted by
Saw-mon and Natalie
Description
Let be the bundle's
MIDNIGHTand the suppliedratifier. For a nonzeronewRoot,midnightBundlesV2CancelAndMakeauthorizes on without checkingR.MIDNIGHT() == M. The root setter's success value does not establish this equality.Suppose is a supported ratifier configured for another instance . If the maker has also authorized the bundle on , root registration succeeds. Operators authorized by the maker on can then register further roots in , including offers executable on : the ratifier's
isRatifieddoes not bind execution to its configured instance. Thus permissions on can control offers on without corresponding operator authorization on .Without bundle authorization on , the root setter instead reverts and rolls back the transaction. Exploitation therefore requires both the mismatched ratifier selection and the stated cross-instance authorization; an arbitrary caller cannot select a ratifier for another maker through this bundle.
Recommendation
Expose
MIDNIGHT()inIRatifiersV1Commonand, inside the nonzero-root branch before granting authorization, requireIRatifiersV1Common(ratifier).MIDNIGHT() == MIDNIGHT, reverting withInconsistentMidnight()otherwise.Morpho
Acknowledged. The natspec here already mentions that users should verify the ratifier (includes the right midnight instance. Further, in the Ratifier contract, the natspec mentions that ratifier should only be used with only
MIDNIGHTinstance.Stale sell-all credit targets revert or open debt
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: High Submitted by
Saw-mon and Natalie
Description
Let be the caller's credit when quoting a full exit through
midnightBundlesV2SupplyCollateralAndSellWithUnitsTarget, withtargetUnits = C. Before execution, a liquidation realizing bad debt reduces the caller's effective credit to through market-wide loss sharing. Continuous-fee accrual can produce the same change without an intervening transaction.The bundle reads the updated credit but retains the fixed target. With
reduceOnly = true, selling the remaining credit cannot satisfy : execution reverts withNotReduceOnlyorOutOfOffers. WithreduceOnly = false, sufficient collateral, eligible offers, and execution before maturity, the bundle can instead create debt of to complete the target. The existing fee-accrual test explicitly asserts this debt creation.The assets-target sell function likewise retains a fixed output after credit decreases. Debt creation is permitted by
reduceOnly = false; the limitation is that neither entrypoint expresses a full-credit exit using the execution-time balance.Recommendation
Add a
type(uint256).maxsentinel fortargetUnits, resolved to the credit returned byupdatePositionViewbefore withdrawal and offer execution. RetainminSellerAssetsas the caller's output bound. Direct full-credit exits to this mode rather than a quoted exact-assets target.Stale callback liquidity causes executable partial fills to be skipped
Severity
- Severity: Low
Submitted by
Saw-mon and Natalie
Description
Let a sell bundle target 100 units, with quoted fills of 100 units from offer and 1 unit from offer . Assume unit prices, zero fees, no initial withdrawable credit, and sufficient collateral. Offer is funded by a Blue callback; has independent funding.
Before execution, another user borrows or withdraws Blue liquidity, reducing 's executable size to 99 units without changing its consumption cap. The bundle sizes each take from the remaining target,
fill.units, and remaining offer consumption. It does not cap the take against the callback's live funding bound.The 100-unit take from therefore reverts during the callback's Blue withdrawal. The bundle catches the revert and skips entirely. It fills 1 unit from , then reverts with
OutOfOffers, although the executable partial fills meet the target. Both sell entrypoints use this sizing pattern. The callback already exposesbuyerAssetsBoundto address stale off-chain routing amounts.Recommendation
For supported funding callbacks, query the execution-time asset bound and convert it conservatively to a unit cap before taking the offer. Include this cap in take sizing, retaining the existing handling of failed takes and final user bounds.
Spearbit
Fixed in
07f293b383824b35adc745454ac2622a27eb0502by adding the following constraints when deriving theunitsToTakein the taker sell endpoints:TakeAmountsLib.buyerAssetsToUnits(MIDNIGHT, id, fill.offer, buyerAssetsBound(id, fill.offer))where the auxiliary
buyerAssetsBoundfunction is:/// @dev Returns the maker's callback funding cap in buyer assets for a buy offer./// @dev Buy offers already limit buyer assets to uint128.max through maxAssets or maxUnits and buyerPrice <= WAD.function buyerAssetsBound(bytes32 id, Offer memory offer) internal view returns (uint256) { if (offer.callback == address(0)) return type(uint128).max; try IBuyerAssetsBound(offer.callback) .buyerAssetsBound(id, offer.market, offer.maker, offer.callbackData) returns ( uint256 bound ) { return UtilsLib.min(bound, type(uint128).max); } catch { return type(uint128).max; }}Moreover, the following constraint has also been added to avoid skipping reduce only offers that might have been skipped due to the units provided being greater than the current maker's live debt:
fill.offer.reduceOnly ? debt(id, fill.offer.maker) : type(uint256).maxSettlement-fee changes can revert bundles before offer skipping
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Medium Submitted by
Saw-mon and Natalie
Description
Let be a buy offer's price and the settlement fee. An assets-target sell bundle includes this offer while . Before execution, the authorized fee setter raises the applicable fee to , within the protocol's fee limits. Assume later offers can satisfy the target at the new fee.
sellerAssetsToUnitscomputes while sizing the first take. This subtraction underflows outside thetry/catcharoundMidnight.take, reverting the entire bundle instead of skipping the invalid offer. At , the conversion instead divides by zero.The assets-target buy function has the analogous failure when a sell offer's price plus the updated settlement fee exceeds
WAD:buyerAssetsToUnitsreverts outsidetry/catch, preventing fallback to later offers. These scenarios require an applicable fee change; arbitrary callers cannot invoke the fee setter.Recommendation
Make offer sizing return a non-executable result for invalid live prices and skip those offers before conversion. In particular, skip seller conversions with nonpositive seller price and buyer conversions with buyer price above
WAD. Apply the same handling to conversions reached throughconsumableUnits.Another design method would be to move inner blocks of the offer fill loops into a
publicfunction inMidnightBundlesV2where this contract self-calls into in atry/catchblock, thus skipping all possible ways that an offer fill might make the execution revert.Morpho
We acknowledge this issue. Note that it was pre-existing on the midnight bundles v1, and that this is a liveness issue that can be mitigated by changing the arguments of the functions (lowering the fill amount, or removing offers).
To give more details, here are the scenarios in which the
TakeAmountsLibfunctions could revert when called in the bundles:- if
buyerPrice > WADinbuyerAssetsToUnits. Note that this can't happen in the case where the buyer is the maker, because thetickToPricefunction never gives prices > WAD. So we consider the case where the taker is the buyer. But the taker would probably prefer that the bundles revert in that case: it makes no sense for buying new credit (negative rate) and neither for repaying debt (always possible to have something better with repayEnabled). Prices too close to WAD for the taker-buyer should be filtered out by the router. offer.buyandsettlementFee >= offerPrice. Then it reverts for underflow or potentially division by 0. Note that if the offers are sorted by price then as soon assettlementFee >= offerPricefor one offer, that stays true for following offers. So even if the iteration is skipped, the function would eventually revert. Also it would mean that the taker is selling at a loss for these offers. Prices too close to 0 for the taker-seller should be filtered out by the router.- price is 0 which produces a division by 0. Because of case 2, we can restrict ourselves to
!offer.buy. In that case the offer price is 0 (looking at what TakeAmountsLib computes). The rate is infinite here, which also isn't very likely except if the maker-seller wants to transfer its credits. It's ok if this is not supported by the bundler, this can be done on Midnight directly if needed. Offer price 0 to sell should be filtered out by the router.
As a summary: the cases where one take-iteration reverts are bad cases for the taker, or unsupported. The router could & should take some buffer to ensure to not hit those, and the bundler reverts if the buffer is not enough
Withdrawal in sell-side functions can be forced to have zero liquidity
State
- Acknowledged
Severity
- Severity: Low
Submitted by
MiloTruck
Description
Both sell-side functions,
midnightBundlesV2SupplyCollateralAndSellWithUnitsTarget()andmidnightBundlesV2SupplyCollateralAndSellWithAssetsTarget(), callMidnight.withdraw()to withdraw available credit before filling buy order.For example, in
midnightBundlesV2SupplyCollateralAndSellWithUnitsTarget():(uint128 takerCreditBefore,,) = IMidnight(MIDNIGHT).updatePositionView(market, id, msg.sender);uint256 withdrawUnits = min(targetUnits, takerCreditBefore, IMidnight(MIDNIGHT).withdrawable(id)); IMidnight(MIDNIGHT).withdraw(market, withdrawUnits, msg.sender, address(this));uint256 filledUnits = withdrawUnits;uint256 filledSellerAssets = withdrawUnits;However, an attacker can front-run a sell transaction and use up
withdrawablein a market. For example, a buyer can calltake()and callwithdraw()during theonBuy()callback to use upwithdrawable. This would cause the user's sell call to always withdraw zero (ie. entire target amount has to be filled via offers).This is harmful to the user since withdrawing is always preferred to filling offers to reduce credit/increase debt. The ratio of assets to units is 1:1 during withdrawals, in contrast, when filling a buy offer, users receive less assets for the same amount of units since they incur interest and the settlement fee.
Although, note that there is no incentive for an attacker to perform this, since they would just incur the settlement fee while consuming
withdrawable.A similar argument can be made for the buy-side functions (ie. forcing
take()to revert when filling offers so thattargetUnits/targetAssetsis consumed viarepay()), but the likelihood there is much lower since:- It doesn't make sense for makers to force
take()to revert when filling their own offers (eg. through the maker callback). They would just cancel the offer. - A third party (ie. not the taker or maker) could cause
take()to revert via competing fills (eg. consuming the same offer, or an offer in the same group, first), but this should be allowed since it is a legitimate use of the protocol.
Recommendation
minSellerAssetsandmaxUnitsseems sufficient to protect the user, since it allows the user to bound the minimum assets they are willing to receive for units (and vice versa).Therefore, consider simply documenting this attack vector. Additionally, the user should always set
minSellerAssets/maxUnitswith the expectation thatwithdrawUnitswill be zero and the entire target units/assets will be filled via offers.Morpho
We acknowledge this issue. There is no guarantee that the withdrawable liquidity is available at execution time, neither from the bundles contract or when calling withdraw directly. This is similar to the fact that there is no guarantee that the offers passed are not taken before execution. So we feel that it is expected, and doesn't require an additional comment
Uncaught reachable panics outside of take call
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Om Parikh
Description
unitsToTakeis an argument totake, so it is computed before thetry. Computing it callsTakeAmountsLib, which duplicates Midnight's price arithmetic including its reverts (:26). Those reverts land outside the try and abort the whole bundle.bundle's natspec says:
// The buy/sell functions below skip the offer if the take reverted. This avoids reverting the whole call when other offers passed as argument still have liquidity.while, there could be cases that it panics in mirroring calculations before reaching
takereverting the whole bundle.for e.g in
sellerAssetsToUnitsIMidnight(midnight).settlementFee(id, UtilsLib.zeroFloorSub(offer.market.maturity, block.timestamp)); uint256 sellerPrice = offer.buy ? offerPrice - settlementFee : offerPrice; return offer.buy ? targetSellerAssets.mulDivUp(WAD, sellerPrice) : targetSellerAssets.mulDivDown(WAD, sellerPrice);- will panic when
settlementFee>offerPricewhenbuy = true - will panic when
sellerPrice= 0 (division-by-zero)
same is true for
buyerAssetsToUnits.Moreover, it reduces to condition
tickToPrice(offer.tick) < settlementFee(id, ttm).The qualifying tick band is state-dependent, not fixed.
settlementFeescales with time to maturity and with the market's configured breakpoints, so the band is widest on long-dated markets and contracts toward the bottom of the tick range as maturity approaches.Any
feeSetterchange moves it again. An offer can cross into the band with no action by its maker, and raising a market's fee turns every low-tick offer already resting in it into the band.Recommendation
- try experimenting with sizing return 0 instead of reverting from lib, also, alternate pair of functions can be created such as
consumableUnitsOrZero,buyerAssetsToUnitsOrZeroandsellerAssetsToUnitsOrZero MidnightBundlesV2TakerTestalways setssettlementFeeto 0, so these interactions are never tested, considering testing withsettlementFee != 0and adding regression tests to fill coverage gap
Morpho
We acknowledge this issue. Note that it was pre-existing on the midnight bundles v1, and that this is a liveness issue that can be mitigated by changing the arguments of the functions (lowering the fill amount, or removing offers).
To give more details, here are the scenarios in which the
TakeAmountsLibfunctions could revert when called in the bundles:- if
buyerPrice > WADinbuyerAssetsToUnits. Note that this can't happen in the case where the buyer is the maker, because thetickToPricefunction never gives prices > WAD. So we consider the case where the taker is the buyer. But the taker would probably prefer that the bundles revert in that case: it makes no sense for buying new credit (negative rate) and neither for repaying debt (always possible to have something better with repayEnabled). Prices too close to WAD for the taker-buyer should be filtered out by the router. offer.buyandsettlementFee >= offerPrice. Then it reverts for underflow or potentially division by 0. Note that if the offers are sorted by price then as soon assettlementFee >= offerPricefor one offer, that stays true for following offers. So even if the iteration is skipped, the function would eventually revert. Also it would mean that the taker is selling at a loss for these offers. Prices too close to 0 for the taker-seller should be filtered out by the router.- price is 0 which produces a division by 0. Because of case 2, we can restrict ourselves to
!offer.buy. In that case the offer price is 0 (looking at what TakeAmountsLib computes). The rate is infinite here, which also isn't very likely except if the maker-seller wants to transfer its credits. It's ok if this is not supported by the bundler, this can be done on Midnight directly if needed. Offer price 0 to sell should be filtered out by the router.
As a summary: the cases where one take-iteration reverts are bad cases for the taker, or unsupported. The router could & should take some buffer to ensure to not hit those, and the bundler reverts if the buffer is not enough
Informational6 findings
Redundant continuous fee checks on sell paths
State
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description
Both
midnightBundlesV2SupplyCollateralAndSellWithUnitsTargetandmidnightBundlesV2SupplyCollateralAndSellWithAssetsTarget
require
continuousFee(id) <= maxContinuousFeebefore each fill. Both take buy offers, so the taker is the seller.The current continuous fee determines the buyer's new pending fee. Seller proceeds depend on the offer price and settlement fee; selling reduces existing credit and pending fees or increases debt. Any fee accrued on the seller's existing credit derives from its recorded
pendingFee, independently of the current continuous fee.Thus, a market fee above
maxContinuousFeereverts the bundle even when the maker's fee cap and the taker's asset/unit bounds permit the sale. The checks add gas without protecting the seller's execution terms.Recommendation
Remove these checks and the corresponding
maxContinuousFeeparameters from both sell functions and their interfaces.Midnight taker fills leave the user's offer groups unconsumed
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description
Let for a user's offer group with
maxUnitsandmaxAssets = 0. Filling units of the user's own offer increases by . Executing the same direction and quantity as a taker throughMidnightBundlesV2leaves unchanged:Midnight.takecharges onlyoffer.maker, and none of the four taker entrypoints accepts or updates the caller's group.Assume and the user has an outstanding, non-reduce-only sell offer for units. The user borrows units through a sell bundle by taking another maker's buy offer. Their own sell offer remains fillable for all units. If collateral and the other execution conditions permit, a subsequent fill raises total borrowing to units, although the user intended the taker execution to consume part of the same -unit budget. With an equivalent maker fill first, only units would remain available. Buy offers and asset-denominated groups have the same accounting asymmetry.
This does not violate Midnight's per-offer checks: group consumption bounds maker fills, not aggregate execution across both roles. However, users combining outstanding offers with taker bundles can exceed their intended shared budget unless they separately update or cancel those offers. The taker call's quantity and slippage bounds do not constrain subsequent maker fills.
Recommendation
Consider adding a caller-owned
groupand eithermaxUnitsormaxAssetsto all four taker entrypoints. Check that the group's current consumption plus the executed amount does not exceed the supplied maximum, then atomically updateconsumed[msg.sender][group]throughMidnight.setConsumed. Use the same denomination and asset-accounting convention as the user's maker offers so both execution paths consume the same budget.Morpho
We acknowledge this issue. The goal of the
consumedmapping is to constrain offers, and directtakes are left out of this constraint (otherwise it would have been implemented at the Midnight level). This stays true here.Minor Issues
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description/Recommendation
-
For better readability use alias for
MarketandMarketParams:import {IMidnight, Market as MidnightMarket} from "../../lib/midnight/src/interfaces/IMidnight.sol";...import {IMorpho, MarketParams as BlueMarket} from "../../lib/morpho-blue/src/interfaces/IMorpho.sol";The above suggestion can be applied to other codebases as well when markets from different protocols are used.
-
MidnightBundlesV2.sol#L109, MidnightBundlesV2.sol#L255, MidnightBundlesV2.sol#L406, MidnightBundlesV2.sol#L216, MidnightBundlesV2.sol#L370: fetching the
collateralTokenmight revert due to out of bound element access. -
- Finding 16, item 2 still applies.
- Finding 9 still applies.
Morpho
We acknowledge this issue:
- the parameter name
blueMarketalready makes that explicit - it is expected that it reverts
- for the same reasons as in v1 review
Small fills can revert cancellation and reposting
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description
Let be a group's consumption and its submitted
maxConsumed. A fill increasing consumption by before inclusion reverts the entire cancel-and-repost transaction, including cancellation. When , any positive fill suffices.The NatSpec already documents the bound, its purpose of preventing oversized replacements, and
type(uint128).maxas the opt-out. This is an intentional safety/liveness tradeoff: a taker can front-run the bundle with a small valid fill, leaving the old offers active. It is not an unconditional cancellation DoS.Recommendation
Extend the existing NatSpec to state that exceeding the bound also reverts cancellation, leaving old offers takeable. Mention the existing cancellation-only option with the bound disabled when cancellation must proceed regardless of intervening fills.
Morpho
The consumption limit is enforced by a require before each cancellation. If any check fails, standard Solidity revert semantics roll back the entire call, including cancellations. Hence, we feel an explicit natspec is not necessary.
MidnightBundlesV2 implementation changes
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description
The earlier Midnight review covered commit
8de7076. The implementation was subsequently imported intobundlesat2840bb7, matching Midnight at012e94dexcept for import paths and the copyright year. Midnight later removed the contract atd268814.This comparison uses the imported implementation at
2840bb7as its baseline andMidnightBundlesV2at4f7ea16as its endpoint.The four buy/sell paths retain ordered offer processing, per-offer fill limits, skipping reverted
takecalls, exact-target requirements, slippage bounds, and referral-fee formulas. The principal changes are:- Account and market selection. Explicit
takerandonBehalfparameters are removed; operations apply tomsg.sender. An explicitmarketreplaces derivation from the first offer, permitting empty offer lists where repayment, credit withdrawal, or collateral operations suffice. - Repayment and withdrawal. Buy paths optionally repay the unfilled target at face value, capped by the caller's debt, after processing offers. Sell paths first withdraw credit at face value, capped by the target, updated credit, and available liquidity. Referral fees include these amounts. The standalone
repayAndWithdrawCollateralentry point is removed; buy paths provide this operation with an empty offer list and repayment enabled. - Execution guards. All entry points enforce a deadline. Trading paths check a continuous-fee cap before each take. Taker-side
reduceOnlyrejects a calculated take exceeding current debt on buys or updated credit on sells; it does not clip the take to the position. - Funding. ERC-2612 and Permit2 inputs are removed from these entry points; ERC20 transfers require prior allowance. All entry points accept native tokens, wrap
msg.value, transfer the wrapped tokens to the caller, and then perform ordinary ERC20 pulls. Wrapped-token allowance is therefore still required. - Maker operations.
midnightBundlesV2CancelAndMakechecks group consumption limits, cancels groups, optionally supplies collateral, and optionally parks loan assets on Blue for a derivedBlueBuyCallback. A nonzero root additionally grants the selected ratifier account authorization, ratifies the root directly or by signature, and publishes the supplied payload throughLOG. The bundle does not verify correspondence between the root, payload, funding, and collateral. - Dependencies and interface. Deployment adds Blue, callback-factory, and log addresses, with factory consistency checks.
Takebecomes the reorderedOfferFill; collateral structs are unified asCollateralTransfer. The newer Midnight interface adds market chain/address fields, narrows offer limits touint128, and adds an offer continuous-fee cap. Approval helpers move toTokenLib; the allowance threshold, zero-reset approval behavior, and direct calculation logic inTakeAmountsLibandConsumableUnitsLibare retained.
midnightBundlesV2CancelAndMake() cannot be called for a new blueMarket
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
MiloTruck
Description
In
midnightBundlesV2CancelAndMake(), whenassetsToParkis non-zero,Blue.supply()is called to supply assets into the specifiedblueMarket.if (assetsToPark > 0) { address blueBuyCallback = IBlueBuyCallbackFactory(BLUE_BUY_CALLBACK_FACTORY).createBlueBuyCallback(msg.sender, callbackSalt); SafeTransferLib.safeTransferFrom(blueMarket.loanToken, msg.sender, address(this), assetsToPark); TokenLib.forceApproveMax(blueMarket.loanToken, BLUE); IMorpho(BLUE).supply(blueMarket, assetsToPark, 0, blueBuyCallback, "");}However,
supply()reverts whenblueMarkethasn't been created yet:require(market[id].lastUpdate != 0, ErrorsLib.MARKET_NOT_CREATED);So
midnightBundlesV2CancelAndMake()cannot be called to park assets in a new blue market.Recommendation
If this functionality is needed, consider checking if the market has been created, otherwise call
createMarket()beforesupply().Morpho
Acknowledged. For the use cases we envision, users park assets in existing Blue markets. We therefore consider automatic market creation an optional convenience that is not worth supporting in this bundles.
Gas Optimizations3 findings
Call repay and withdraw only when positive units are provided
Severity
- Severity: Gas optimization
Submitted by
Saw-mon and Natalie
Description/Recommendation
Just like other gas-optimisations in this contract call the
repayandwithdrawendpoints ofMIDNIGHTwhen non-zero units are provided. This might add slight gas on the paths that would require the call but it will save a lot for paths that don't.Fetch and set MIDNIGHT and BLUE from _blueBuyCallbackFactory
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Saw-mon and Natalie
Description
Fetch and set
MIDNIGHTandBLUEfrom_blueBuyCallbackFactory.Recommendation
constructor(address _blueBuyCallbackFactory, address _log) { MIDNIGHT = IBlueBuyCallbackFactory(_blueBuyCallbackFactory).MIDNIGHT(); BLUE = IBlueBuyCallbackFactory(_blueBuyCallbackFactory).BLUE(); BLUE_BUY_CALLBACK_FACTORY = _blueBuyCallbackFactory; LOG = _log;}Moroever
_blueBuyCallbackFactorycan be defined asIBlueBuyCallbackFactoryto avoid casting fromaddressto this type.Morpho
The gas gains are negligible and only at deployment. We would rather have blue and midnight explicit and make consistency checks for readability rather than use BlueBuyCallbackFactory for deriving.
Hoist continuous fee cap check out of the offer fill loop
State
- Acknowledged
Severity
- Severity: Gas optimization
Submitted by
Saw-mon and Natalie
Description
Given an array of offer fills for the buy endpoints, if this array has more than 1 element the following same check is performed per loop iteration:
require(IMidnight(MIDNIGHT).continuousFee(id) <= maxContinuousFee, ContinuousFeeAboveMax());Even though one would only need to perform this once and only if new atomic credits are acquired by the user. The check also gets performed for reduce only bundles.
Recommendation
Hoist continuous fee cap check out of the offer fill loop and only check this requirement when
- the offer fill array is non-empty
targetUnitsis non-zero.- new atomic credits are acquired by the user (this might be tricky to check)
Morpho
For simplicity, we acknowledge this issue.