Organization
- @morpho
Engagement Type
Spearbit Web3
Period
-
Repositories
Researchers
Findings
Low Risk
3 findings
1 fixed
2 acknowledged
Informational
12 findings
7 fixed
5 acknowledged
Low Risk3 findings
buyerAssetsBound reports unusable liquidity for entry-gated buyers
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Saw-mon and Natalie
Description
Let be the assets withdrawable by
BlueBuyCallback. The function returns while ignoringid,market, andbuyer. It therefore also reports a positive bound forbuyer != OWNER, althoughonBuyalways rejects that buyer.Assume the buyer has no debt and the market entry gate rejects or reverts for
canIncreaseCredit(buyer). For every :Consequently,
Midnight.takereverts for every positive-unit purchase, butbuyerAssetsBoundstill returns . A router using the advertised bound can therefore select an unexecutable take.The current API also cannot return the exact gate-aware bound when the buyer has debt. Such a buyer may purchase up to its debt without entering, but converting that unit limit into
buyerAssetsrequires the offer direction and price. For the same reason, the endpoint cannot account for remaining asset/unit caps or maker-sidereduceOnly.Recommendation
At minimum, return
- zero when
buyer != OWNER, or - when the buyer has zero debt and
market.enterGaterejects or reverts forcanIncreaseCredit(buyer).
For an exact executable bound, change the API to accept the
Offer. Bound the result by Blue liquidity, remaining offer capacity, and the assets corresponding to the buyer's debt whenever entry is denied or a maker-buy offer isreduceOnly.NoteIn general the exact purpose for this queryable function is not clear since the specifications are not provided and most of the input parameters are ignored.
Footnote
buyerAssetsBounddoes not also consider inconsistencies between IRM accounting when:borrowRateborrowRateView
buyerAssetsBound should return zero for non loan tokens
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Saw-mon and Natalie
Description/Recommendation
If the Midnight market loan token does not match the decoded Blue market's loan token ,
buyerAssetsBoundshould return0.buyerAssetsBound reverts when the provided data is short
Severity
- Severity: Low
Submitted by
Saw-mon and Natalie
Description
If
datadoes not occupy enough memory so, decoding it intoMarketParamscan revert. An example test would be:diff --git a/test/BlueBuyCallbackTest.sol b/test/BlueBuyCallbackTest.solindex ab34dbb1..9fb19fcd 100644--- a/test/BlueBuyCallbackTest.sol+++ b/test/BlueBuyCallbackTest.sol@@ -204,6 +204,11 @@ contract BlueBuyCallbackTest is Test { assertEq(result, 0); } + function testBuyerAssetsBoundRevertsWhenDataIsSmall() public {+ uint256 result = callback.buyerAssetsBound(bytes32(0), market, owner, hex"");+ assertEq(result, 0);+ }+ function testOnBuyRevertsIfCallerIsNotMidnight(address caller) public { vm.assume(caller != address(midnight)); vm.expectRevert(IBlueBuyCallback.NotMidnight.selector);Recommendation
If in
buyerAssetsBoundthe length ofdatais less than the memory required to decodeMarketParams(5 words) return0. One could also explicitly revert depending on the design decision.Morpho
Further clarification has been added to the NatSpec:
/// @dev Reverts if data is not well formed.
Informational12 findings
BlueBuyCallback lacks a skim endpoint for stranded assets
State
- Fixed
PR #1096
Severity
- Severity: Informational
Submitted by
Saw-mon and Natalie
Description
Let be the native-token balance of
BlueBuyCallback, and let be its balance of an arbitrary ERC-20 token .Native tokens can be forced or prefunded to the callback address, and any ERC-20 can be transferred to it. However, the callback's only asset-related flow is
onBuy, which withdraws the exact requested loan-token amount from Blue and approves Midnight to pull it. Neither this flow nor the owner-controlled endpoints can transfer an existing balance toOWNER.Consequently, accidental or forced balances satisfy
These assets remain stranded in the callback.
Recommendation
Add an
OWNER-onlyskimendpoint that transfers the callback's full balance of either the native token or an arbitrary ERC-20 to a specified receiver. Emit the token, receiver, and amount so the recovery is reconstructible from events. Use a safe ERC-20 transfer helper and bubble or explicitly handle native-transfer failure.BlueBuyCallbackFactory.createBlueBuyCallback should track the msg.sender
Description
Anyone can call the
BlueBuyCallbackFactory.createBlueBuyCallbackfunction and deploy aBlueBuyCallbackcontract on behalf of an arbitraryowner.The function should track in the
CreateBlueBuyCallbackevent thecallerof the function given that it could be different from theownerinput parameter.Recommendation
Morpho should add
address indexed callerto the parameters of theCreateBlueBuyCallbackevent definition.BlueBuyCallbackFactory should revert when incorrectly configured
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
StErMi
Description
The
BlueBuyCallbackFactorydoes not perform any sanity checks in the constructor. If theMIDNIGHTandBLUEimmutable variables are left uninitialized, any calls to thecreateBlueBuyCallbackwould generate "useless"BlueBuyCallbackcontracts.Recommendation
Morpho should consider adding basic sanity checks to the
BlueBuyCallbackFactoryconstructorrequire(_midnight != address(0) && _midnight.code.length != 0);require(_blue != address(0) && _blue.code.length != 0);
Morpho: see Morpho Protocols Audit Guidelines
setAuthorization and setAuthorizationWithSig should not allow the OWNER to remove self-unauthorize
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
StErMi
Description
Both the
setAuthorizationandsetAuthorizationWithSigare allowing theOWNERto self-unauthorize itself on theBLUEcontract as the authorized user to interact on Morpho Blue on behalf of theBlueBuyCallbackcontract.The owner should always be authorized to be able to withdraw from the positions that are owned by the callback's contract (in addition to executing other actions that could require the authorization).
Recommendation
Morpho should:
- revert
setAuthorizationwhenauthorized == OWNER && newIsAuthorized == FALSE - revert
setAuthorizationWithSigwhenauthorization.authorized == OWNER && authorization.isAuthorized FALSE
Consider using on demand exact allowance approval when onBuy is executed
Description
The current implementation of
BlueBuyCallback.onBuyis providing theMIDNIGHTcontract an infinite allowance for themarket.loanTokentoken even if the exact amount pulled byMIDNIGHT(after the callback return) is already known.Recommendation
Morpho should consider replacing
forceApproveMax(market.loanToken, MIDNIGHT);withsafeApprove(market.loanToken, MIDNIGHT, buyerAssets);Execute allowance approval only when tokens will be actually pulled by Midnight
Description
When
buyerAssetsis zero, theMidnight.takefunction will pull an empty amount of tokens from theBlueBuyCallbackcontract (thepayerin this case)if (buyerCallback != address(0)) { bytes memory buyerCallbackData = offer.buy ? offer.callbackData : takerCallbackData; require( IBuyCallback(buyerCallback) .onBuy(id, offer.market, buyerAssets, units, buyerPendingFeeIncrease, buyer, buyerCallbackData) == CALLBACK_SUCCESS, WrongBuyCallbackReturnValue() ); } SafeTransferLib.safeTransferFrom(offer.market.loanToken, payer, address(this), buyerAssets - sellerAssets); SafeTransferLib.safeTransferFrom(offer.market.loanToken, payer, receiver, sellerAssets);This means that no allowance will be consumed and there's no reason for the
BlueBuyCallbackcontract to provide infinite allowance to theMidnightcontract.Recommendation
Morpho should consider moving
forceApproveMax(market.loanToken, MIDNIGHT);inside theif (buyerAssets > 0)branch of theBlueBuyCallback.onBuyfunction.buyerAssetsBound should document if the callback contract can be configured as a BLUE fee recipient
Description
If the
BlueBuyCallbackis configured as the fee recipient for theBLUEmarket, the value returned byIMorpho(BLUE).position(marketParams.id(), address(this)).supplySharescould not yet include the shares minted byMorpho._accrueInterest.Recommendation
Morpho should document in the
buyerAssetsBoundnatspec if theBlueBuyCallbackcan be configured as aBLUEfee recipient.If that's the case the
buyerAssetsBoundshould explicitly disclose that the internal logic could underestimate thesupplyAssetsvalue and so the final value returned.BlueBuyCallback natspec should be further expanded
Description
Other Morpho's contracts like the BlueBundlesV1 or Midnight have very extensive and detailed natspec documentation that covers which is the contract's scope, the explicit or implicit assumptions and implementation behaviors like no-op operations, zero checks, and so on.
Recommendation
Morpho should rewrite the
BlueBuyCallbacknatspec to cover the following aspects- Contract's scope
- Explicit and Implicit assumptions
- Expected requirements
- General implementations adopted standards like no-op operations, zero checks and so on
- "Custom" implementation behaviors relevant only to
BlueBuyCallback
Consider reducing the complexity of BlueBuyCallback flows derived by unrestricted delegation on Blue positions
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
StErMi
Description
The main goal of
BlueBuyCallbackis to be able to allowOWNERto park tokens in theBLUEmarkets to yield interest while waiting for possibleMIDNIGHT.takeactions (where theOWNERis the buyer).The "main" reason to set
OWNERas an authorized user inBLUEfor theBlueBuyCallbackinstance is to allow theOWNERto withdraw the callback's supply position (+ yield).But because
BlueBuyCallback.setAuthorizationandBlueBuyCallback.setAuthorizationWithSigallow theOWNERto add/remove new authorized users onMORPHOfor theBlueBuyCallbackcontracts it means that multiple users could manage the callback contract onBLUEwhich could include executingborrowflows (supplyCollateralcan be executed by anyone on behalf ofBlueBuyCallback).Given the specific core scope of
BlueBuyCallbackMorpho should consider to reduce what can be done with the contract's positions on Morpho Blue enabled by the authorization system.- remove
IMorpho(BLUE).setAuthorization(OWNER, true);fromconstructor - remove the
setAuthorizationfunction - remove the
setAuthorizationWithSigfunction - Add a
withdrawfunction callable only by theOWNERthat will callBLUE.withdraw - Add a
withdrawCollateralfunction callable only by theOWNERthat will callBLUE.withdrawCollateral - (optional) If there's a need for the
OWNERto allow delegation, implement a "local" delegation system to allow the owner to delegate the execution ofBlueBuyCallbackwithdrawandwithdrawCollateralfunctions.
Recommendation
Morpho should consider reducing the complexity of
BlueBuyCallbackflows derived by unrestricted delegation on Blue positionsMorpho: indeed that would have been an other path. we are ok with the fact that someone could indeed borrow / deposit collateral etc through the callback, so we decided to ack the issue.
Consider documenting the token assumptions made by safeApprove
Severity
- Severity: Informational
Submitted by
StErMi
Description
The
BlueBuyCallback.safeApprovedoes not validate the token's existence on purpose assuming that such validation will be done by theBlueBuyCallback.onBuycaller.Recommendation
Morpho should explicitly document this assumption like it has been already done in the bundler's TokenLib contract.
The strong assumption is that
Midnight.takewill perform that validation onceSafeTransferLib.safeTransferFromwill be executed after the callback returns the flow to the caller contract.createBlueBuyCallback() reverts if an owner already has BlueBuyCallback deployed
Description
In
BlueBuyCallbackFactory,createBlueBuyCallback()always attempts to deploy a newBlueBuyCallbackfor the specifiedownerwithout checking if the contract already exists.As such, if
BlueBuyCallbackis already deployed for anowner, callingcreateBlueBuyCallback()for the sameownerwill revert.If a user batches
createBlueBuyCallback()with other operations (eg.setAuthorization(),BLUE.supply()), an attacker can force the entire batch to revert by front-running and callingcreateBlueBuyCallback()first.Recommendation
Consider checking if
BlueBuyCallbackwas deployed first and perform a no-op if so:address callback = callbackOf[owner];if (callback != address(0)) return callback;Offers in midnight using BlueBuyCallback can be forced to revert by draining Morpho Blue liquidity
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
MiloTruck
Description
In
BlueBuyCallback, theonBuy()callback always withdrawsbuyerAssetsfrom Morpho Blue:if (buyerAssets > 0) IMorpho(BLUE).withdraw(marketParams, buyerAssets, 0, address(this), address(this));This introduces a front-running attack vector, where an attacker could force a call to
take()to revert by temporarily consuming liquidity inBLUE(eg. by borrowing enough such thattotalSupplyAssets - totalBorrowAssets < buyerAssets).Recommendation
Consider adding a comment that bundles/routers should call
buyerAssetsBound()andtake()in the same transaction to avoid this.