MorpheusAI

MorpheusAI
Foundry
22,500 USDC
View results
Submission Details
Severity: medium
Invalid

`L2MessageReceiver._nonblockingLzReceive` function: invalid check will result in users not being able to get their `MOR` rewards

Summary

L2MessageReceiver._nonblockingLzReceive function: invalid check will result in users not being able to get their MOR rewards

Vulnerability Details

  • The Distribution contract will call the L1Sender contract when the stakers of pools claim their rewards via Distribution.claim function, where a mint message is going to be sent to the L2MessageReceiver contract on Arbitrum chain, and this operation is done via the layer zero endpoint on the Ethereum chain (via send function implemented by the layer zero endpoint):

    //@@notice : from L1Sender.sendMintMessage function
    bytes memory receiverAndSenderAddresses_ = abi.encodePacked(config.receiver, address(this));
    bytes memory payload_ = abi.encode(user_, amount_);
    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 seen; the receiverAndSenderAddresses_ that is going to be sent to the L2Receiver encodes the config.receiver address first, then the address of the L1Sender contrat.

  • The mint message is going to be received and executed by the L2MessageReceiver contract on Arbitrum; where it will check if the sender of the message is the authorized L1Sender address before minting the MOR tokens to the claimer address:

    //@@notice : from L2MessageReceiver._nonblockingLzReceive function
    assembly {
    sender_ := mload(add(senderAndReceiverAddresses_, 20))
    }
    require(sender_ == config.sender, "L2MR: invalid sender address");
  • But as can be noticed: the L2MessageReceiver._nonblockingLzReceive function will extract the first encoded address from the senderAndReceiverAddresses_ argument, which is the address of config.receiver in the L1Sender contract, while it should extract the address of the L1MessageSender contract itself (encoded address(this) in senderAndReceiverAddresses_):

    //@@notice : IL2MessageReceiver.Config strcut definition
    /**
    * The structure that stores the config data.
    * @param gateway The address of token's gateway.
    * @param sender The address of sender (L1Sender).
    * @param senderChainId The chain id of sender (L1).
    */
    struct Config {
    address gateway;
    address sender; //@audit-issue : L1Sender address, which is the second encoded address
    uint16 senderChainId;
    }
  • So the check on the sender address will always revert, resulting in failing messages being saved in the failedMessages mapping (since a non-blocking mechanism is implemented by the L2MessageReceiver contract), where these messages will never be retried via L2MessageReceiver.retryMessage function as it will always revert on the same check.

Impact

This will result in users losing their entitled rewards, as the Distribution contract will reset their uncalimed rewards to zero when they claim them, optimistically assuming that their rewards will be successfully minted on L2, and the L2MessageReceiver will never be able to execute their failed rewards minting due to the invalid extracted sender address.

Proof of Concept

Code Instances:

L1Sender.sendMintMessage function/ L127-L137

bytes memory receiverAndSenderAddresses_ = abi.encodePacked(config.receiver, address(this));
bytes memory payload_ = abi.encode(user_, amount_);
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")
);

L2MessageReceiver._nonblockingLzReceive function/ L97-L101

address sender_;
assembly {
sender_ := mload(add(senderAndReceiverAddresses_, 20))
}
require(sender_ == config.sender, "L2MR: invalid sender address");

Coded PoC:

  1. Add the following test to an empty sol file in the online Remix IDE.
    The test shows that L2MessageReceiver._nonblockingLzReceive function will extract the first address of the senderAndReceiverAddresses_ instead of the second address:

    • first encode the receiver and the sender addresses via encodeAddresses

    • then call testExtractAddress with the encoded data from the first call

    • the result will be equal to the receiver address

    //SPDX-License-Identifier: BUSL-1.1
    pragma solidity >0.8.0;
    contract TestMe {
    address public result;
    function encodeAddresses(address receiver, address sender) external view returns (bytes memory) {
    // @@notice : as coded in LiSender::sendMintMessage function
    bytes memory receiverAndSenderAddresses_ = abi.encodePacked(receiver, sender);
    return receiverAndSenderAddresses_;
    }
    function testExtractAddress(
    uint16 senderChainId_,
    bytes memory senderAndReceiverAddresses_,
    bytes memory payload_
    ) external {
    address sender_;
    assembly {
    sender_ := mload(add(senderAndReceiverAddresses_, 20))
    }
    result = sender_;
    }
    }
  2. Test result:
    Result

Tools Used

Manual Review and Remix.

Recommendations

Update L2MessageReceiver._nonblockingLzReceive function to extract the correct sender address:

address sender_;
assembly {
- sender_ := mload(add(senderAndReceiverAddresses_, 20))
+ sender_ := mload(add(senderAndReceiverAddresses_, 40))
}
require(sender_ == config.sender, "L2MR: invalid sender address");
Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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