MorpheusAI

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

Missing minimum gas checks on Distribution.sol can cause messages to be stuck on the endpoint

Summary

LayerZero requires a bit of native gas token so the relayer can complete the message delivery on the destination chain. The gas which would require for a certain message to be delivered can be estimated by calling the estimateFees() function on the LayerZero endpoint. Distribution.sol doesn't have any logic that would prompt the user to estimate gas before calling the claim() function or no logical checks to see if the gas provided is more than the minimum. Providing a less-than-needed amount of gas for the transaction can result in OUT-OF-GAS errors which can get stuck on the endpoint (before reaching the destination chain and being able to be caught by the L2MessageReceiver). While Morpheus does employ a mechanism for storing unsuccessful messages for later retry on the receiver, and uses a non-blocking pattern for failed messages, this wouldn't account for messages that haven't reached destination chain.

Vulnerability Details

When a user calls the Distribution::claim() function it's expected out of the user to pass the amount of gas needed for the execution of the message delivery, although users are allowed to pass an arbitrary amount of gas which can be the less-than-needed amount for a successful delivery of the message.

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

L1Sender:

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")
);

Even though L2MessageReceiver does employ a non-blocking pattern which stores failed messages for a later retry, this wouldn't account for messages which didn't reach the destination chain and got stored in the storedPayload on the endpoint for a later payload retry. This would require for someone/anyone to retryPayload on the endpoint and pay for the remaining gas in order for the message to reach the destination chain.

Even though short term, it can still cause a storedPayload which isn't on the receiver.

LayerZero endpoints do not employ any logic to check whether the transaction has sufficient gas to be executed, as it can be examined from the contracts:

0x3c2269811836af69497E5F486A85D7316753cf62 (Arbitrum)
0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675 (Ethereum)

And in the LayerZero endpoint we can see that in order for the lzReceive to be called it requires a certain amount of gas:

try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) {
// success, do nothing, end of the message delivery
} catch (bytes memory reason) {
// revert nonce if any uncaught errors/exceptions if the ua chooses the blocking mode
storedPayload[_srcChainId][_srcAddress] = StoredPayload(uint64(_payload.length), _dstAddress, keccak256(_payload));
emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _nonce, _payload, reason);
}
}

If this fails, it's stored on the source chain which in Morpheus's case would be Ethereum, retrying messages and paying for gas can be costly, especially if it isn't done by the user who initiated the transaction in the first place.

Resources:

  • https://layerzero.gitbook.io/docs/faq/messaging-properties

  • https://layerzero.gitbook.io/docs/evm-guides/contract-standards/estimating-message-fees

  • https://layerzero.gitbook.io/docs/evm-guides/advanced/relayer-adapter-parameters

  • https://layerzero.gitbook.io/docs/ecosystem/relayer/overview

  • https://layerzero.gitbook.io/docs/evm-guides/master/how-to-send-a-message

  • https://layerzero.gitbook.io/docs/ecosystem/relayer/layerzero-relayer

  • https://layerzero.gitbook.io/docs/troubleshooting/layerzero-integration-checklist

Impact

Messages that would run out of gas before reaching the MessageReceiver on the destination chain will be stuck in the storedPayload on the source chain and the funds to be received won't be until the payload has been cleared and the remainder of the gas has been paid for.

Tools Used

Manual Review

Recommendations

Employ logic in the claim() function on Distribution.sol that would call the estimateFees() function on the endpoint to assess the gas and revert the claim() function if the gas provided is less than the expected one.

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.