MorpheusAI

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

No check for sufficient layerzero fee amount

Summary

Users send an arbitrary amount of fees when trying to claim their MOR rewards token. This might result in failed transactions if they send in value that is not enough. Also, this can be used to bypass paying fees by purposefully making the claim transaction fail, and reclaiming through the retryMessage function.

Vulnerability Details

To claim their rewards, users call the claim function, while sending ETH. The claim function sends the mint message to the L1Sender through the sendMintMessage function.

function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {
...
// Transfer rewards
L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender()); //@note
emit UserClaimed(poolId_, user_, pendingRewards_);
}

The send mint function packages the message and sends it over to layer zero endpoint.

function sendMintMessage(address user_, uint256 amount_, address refundTo_) external payable onlyDistribution {
...
ILayerZeroEndpoint(config.gateway).send{value: msg.value}( //@note
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")
);
}

For context, when sending a message to the layer zero endpoint, enough gas must for the transaction to succeed. If fees aren't enough, the gas, the transaction will fail.
As can be seen from the claim function, the user is allowed to send any arbitrary amount of ETH, hence as fees.

First, allowing users to send any value might result in them not sending enough for the fees, hence their transactions will always fail. It's leads to poor user experience for the most part.

The other issue here comes from the lzReceive which is how the message will be received.
Following the function chain, the transaction goes from lzReceive to _blockingLzReceive which calls the nonblockingLzReceive function wrapped in try catch.

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_);
}
}

Due to the fees not being enough, the _blockingLzReceive will fail, causing that the message the be added to the failedMessages.
The failed message can then be retried by calling the retryMessage function to clear up the message queue, which calls the _nonblockingLzReceive function through which the tokens are finally minted.

function retryMessage(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
uint64 nonce_,
bytes memory payload_
) external {
...
_nonblockingLzReceive(senderChainId_, senderAndReceiverAddresses_, payload_);
delete failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
emit RetryMessageSuccess(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}
function _nonblockingLzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
bytes memory payload_
) private {
require(senderChainId_ == config.senderChainId, "L2MR: invalid sender chain ID");
address sender_;
assembly {
sender_ := mload(add(senderAndReceiverAddresses_, 20))
}
require(sender_ == config.sender, "L2MR: invalid sender address");
(address user_, uint256 amount_) = abi.decode(payload_, (address, uint256));
IMOR(rewardToken).mint(user_, amount_);
}

Thus, a user can call the claim function, sending little or no ETH as fees, causing the transaction to intentionally fail. Upon message reception failure, the user can then retry the message to claim the reward tokens.

Impact

  1. No exact knowledge of fees can cause that users who unknowingly don't send enough will have their transactions fail, and therefore cause poor user experience.

  2. Users can intentionally send little or no fees, making the transaction fail, so that upon retrying the message, they'll be able to claim their rewards without paying fees.

Tools Used

Manual code review

Recommendations

To fix this, layerzero has an estimateFees function as described here. This should be implemented, as well as a check that msg.value sent when claiming == the fees from the estimate.

Updates

Lead Judging Commences

inallhonesty Lead Judge almost 2 years 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.