Banked: Cycle 192 (Morpho Midnight saturated-target mining) Sources (cross-corroborated): - Spearbit 5.4.5 ("Trust assumptions for buyerCallback"). - Blackthorn L-19 ("The absence of actor binding lets attackers drain third-party callback funds across maker-buyer, taker-buyer, and seller orders"), L-20 ("Zero-unit take() can replay callbacks"), L-26 ("Liquidator callbacks cannot gate exposure, so extra allowance can be exploited"), L-14/L-25 (onFlashLoan/onLiquidate missing initiator/receiver). - TrustSec TRST-L-4/L-5/L-6 (callback data insufficient to bound continuous-fee / debt slippage), TRST-R-17 (force-approve threshold exhaustible by reentrant callbacks). Severity: Low across reports (acknowledged), but the CLASS is high-yield against integrators. Status: NOVEL for the no-capital-lock / offer-at-settlement liquidity model. Not previously banked.
Midnight's headline feature: makers do NOT lock capital when posting an offer. Instead, an offer may name a callback contract that executes AT settlement (take time) to source the funds/collateral just-in-time (e.g. pull from Morpho Blue, or roll a position). This is the "offer-based, liquidity-sourced-at-settlement" model. The danger: the core contract calls the callback as the PAYER in the transfer path, but does NOT bind the callback to the economic actor it is supposed to represent.
take() accepts a maker-signed offer.callback AND a caller-supplied takerCallback, maps them to buyerCallback/sellerCallback, then uses whichever as the PAYER. The reference callback only checks msg.sender == Midnight (or obligation id) - it does NOT verify buyer == maker or that the ID/assets match the flow it intended to fund. So a malicious taker can point takerCallback at a VICTIM maker's funded callback; Midnight calls onBuy(..., buyer = attacker, ...), pulls loan tokens from the victim's callback to fund the attacker's purchase. Drain vectors: loan tokens via buyer-leg, collateral via seller-leg, and arbitrary third-party balances by making flashLoan() invoke a chosen callback with assets = 0.units/buyerAssets but NOT the credit/debt split, the resulting debt, or the continuous-fee component. So a maker callback that wants to approve "only if I net X credit" cannot, because pendingFeeIncrease is computed from buyerCreditIncrease not all units (some units may repay existing debt). Liquidator callbacks (L-26): the only lever a liquidator callback has is per-call allowance == exactly repaidUnits; any EXTRA standing allowance can be stolen by a third party who seeds the liquidator with a dust amount of the seized token and calls liquidate(receiver=attacker, callback=liquidator), forcing the liquidator to pay for a liquidation it cannot refuse.onLiquidate/onRepay/onFlashLoan run BEFORE the safeTransferFrom pull, and _forceApproveMax tops approval up at the START of the function - so a maker-controlled callback can reenter, consume the approval, and the outer context then reverts on an exhausted approval (DoS), or batches can widen the attack window by summing concurrent liquidation allowances.take(0,...) passes all offer/session/ratifier checks, computes zero assets, does NOT advance consumed, still emits a Take event, and still invokes both callbacks with zero amounts. A fully-consumed offer can still be hit with zero-unit calls -> off-chain DoS / spam griefing against entities that do heavy work in their callbacks.msg.sender (initiator), so the callback could trust them. New design lets them be arbitrary and does not pass the initiator, so the callback cannot implement access control on who is really driving the call.msg.sender == Midnight.buyerAssets but not buyer == maker, or units but not the credit/debt split / resulting debt, flag actor-confusion + exposure-bounding gaps.takerCallback) and maker-signed callback addresses (offer.callback) are EVER routed into the same payer slot. If a taker can name a victim's funded callback as the payer, you have a drain.When a protocol hands authentication of the trade context to an external callback, it MUST pass that callback enough to authenticate: the bound economic actor (buyer/seller/initiator), the receiver, and the full economic delta (credit/debt split, resulting debt, fee component) - not just raw fill amounts. A callback that can only see msg.sender == Protocol and a raw amount is exploitable by any party who can route their trade through it. Audit the callback CONTRACT-AS-VICTIM angle, not only the core-as-victim reentrancy angle.