MorpheusAI

MorpheusAI
Foundry
22,500 USDC
View results
Submission Details
Severity: low
Valid

`Distribution.claim` function: callers can grief the protocol by claiming without providing bridging fees

Summary

Distribution.claim function: callers can grief the protocol by claiming without providing bridging fees

Vulnerability Details

  • Morpheus protocol allows users to stake/deposit stETH in Distribution contract that is going to be deployed on the Ethereum mainnet, and claim their MOR token rewards on Arbitrum network (L2).

  • The protocol allows any user to claim rewards onbehalf of other stakers via Distribution.claim function, where the caller should provide the cost of layer zero transaction that is required to send the mint message to the L2MessageReceiver contract on Arbitrum :

    //@notice: claim function on L1:
    // Transfer rewards
    L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());
    //@notice: L1Sender.sendMintMessage function
    ILayerZeroEndpoint(config.gateway).send{value: msg.value}(
    config.receiverChainId, // communicator LayerZero chainId
    receiverAndSenderAddresses_, // send to this address to the communicator
    payload_, // bytes payload
    payable(refundTo_), // refund address
    address(0x0), // future parameter
    bytes("") // adapterParams (see "Advanced Features")
    );
  • As can be noticed; there's no check on the provided call value (msg.value), so if the caller calls claim function with a dust amount that is not enough to execute the call; the call will fail and will be catched by the L2MessageReceiver.lzReceive function (L2MessageRceiver contract implements a non-blocking-lz-message-receive):

    function _blockingLzReceive(
    uint16 senderChainId_,
    bytes memory senderAndReceiverAddresses_,
    uint64 nonce_,
    bytes memory payload_
    ) private {
    try
    IL2MessageReceiver(address(this)).nonblockingLzReceive(
    senderChainId_,
    senderAndReceiverAddresses_,
    payload_
    )
    {
    emit MessageSuccess(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
    } catch (bytes memory reason_) {
    failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_] = keccak256(payload_);
    emit MessageFailed(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_, reason_);
    }
    }

Impact

Since any failed message can be retried to be executed again via L2MessageReceiver.retryMessage function; this will incur the protocol team the transaction fees to re-execute these failed mesages again.

Proof of Concept

Distribution.claim function/ L173

// Transfer rewards
L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());

Tools Used

Manual Review.

Recommendations

Add a function to estimate messaging fees (lz.estimateFees()), and check that the caller of the claim function provides this estimated fees as a minimum (msg.value >= estimateFees()).

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

LayerZero Integration: `sendMintMessage` doesn't verify the `msg.value` sent by the user facilitating failed transactions.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.