MorpheusAI

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

All cross-chain mint messages can be blocked if the `L2MessageReceiver` contract is not properly initialized

Summary

The L2MessageReceiver contract can be left uninitialized with the necessary parameters: rewardToken and config. Subsequently, all cross-chain mint messages will be blocked.

Vulnerability Details

The L2MessageReceiver contract can be initialized via the L2MessageReceiver__init(). However, while executing the L2MessageReceiver__init(), the function does not configure the parameters rewardToken and config.

Specifically, an admin is required to execute the setParams() to configure them explicitly. If this step is overlooked, the rewardToken and config parameters will be left uninitialized. Subsequently, a cross-chain message will be blocked in the lzReceive().

// FILE: https://github.com/Cyfrin/2024-01-Morpheus/blob/07c900d22073911afa23b7fa69a4249ab5b713c8/contracts/L2MessageReceiver.sol
@1 function L2MessageReceiver__init() external initializer {
@1 __Ownable_init();
@1 __UUPSUpgradeable_init();
@1 } //@audit -- The L2MessageReceiver contract is initialized without setting the 'rewardToken' and 'config' params
@2 function setParams(address rewardToken_, Config calldata config_) external onlyOwner {
@2 rewardToken = rewardToken_;
@2 config = config_;
@2 } //@audit -- An admin is required to execute the setParams() to configure the 'rewardToken' and 'config' params
function lzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
uint64 nonce_,
bytes memory payload_
) external {
@3 require(_msgSender() == config.gateway, "L2MR: invalid gateway"); //@audit -- If the step 2 is overlooked, a cross-chain message will be blocked here
_blockingLzReceive(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}
  • @1 -- The L2MessageReceiver contract is initialized without setting the 'rewardToken' and 'config' params: https://github.com/Cyfrin/2024-01-Morpheus/blob/07c900d22073911afa23b7fa69a4249ab5b713c8/contracts/L2MessageReceiver.sol#L21-L24

  • @2 -- An admin is required to execute the setParams() to configure the 'rewardToken' and 'config' params: https://github.com/Cyfrin/2024-01-Morpheus/blob/07c900d22073911afa23b7fa69a4249ab5b713c8/contracts/L2MessageReceiver.sol#L26-L29

  • @3 -- If the step 2 is overlooked, a cross-chain message will be blocked here: https://github.com/Cyfrin/2024-01-Morpheus/blob/07c900d22073911afa23b7fa69a4249ab5b713c8/contracts/L2MessageReceiver.sol#L37

To elaborate on the vulnerability, while the lzEndpoint contract invokes the L2MessageReceiver::lzReceive(), the transaction will be reverted. Then, the message payload will be cached in the lzEndpoint's storage. As a result, the cached message will block all upcoming cross-chain mint messages.

// FILE: https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol
function receivePayload(uint16 _srcChainId, bytes calldata _srcAddress, address _dstAddress, uint64 _nonce, uint _gasLimit, bytes calldata _payload) external override receiveNonReentrant {
// assert and increment the nonce. no message shuffling
require(_nonce == ++inboundNonce[_srcChainId][_srcAddress], "LayerZero: wrong nonce");
LibraryConfig storage uaConfig = uaConfigLookup[_dstAddress];
// authentication to prevent cross-version message validation
// protects against a malicious library from passing arbitrary data
if (uaConfig.receiveVersion == DEFAULT_VERSION) {
require(defaultReceiveLibraryAddress == msg.sender, "LayerZero: invalid default library");
} else {
require(uaConfig.receiveLibraryAddress == msg.sender, "LayerZero: invalid library");
}
// block if any message blocking
StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress];
@6 require(sp.payloadHash == bytes32(0), "LayerZero: in message blocking"); //@audit -- The cached message will block all upcoming cross-chain mint messages
@4 try ILayerZeroReceiver(_dstAddress).lzReceive{gas: _gasLimit}(_srcChainId, _srcAddress, _nonce, _payload) { //@audit -- While the lzEndpoint contract invokes the L2MessageReceiver::lzReceive(), the transaction will get reverted in step 3
// 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
@5 storedPayload[_srcChainId][_srcAddress] = StoredPayload(uint64(_payload.length), _dstAddress, keccak256(_payload)); //@audit -- Consequently, the message payload will be cached in the lzEndpoint's storage
emit PayloadStored(_srcChainId, _srcAddress, _dstAddress, _nonce, _payload, reason);
}
}
  • @4 -- While the lzEndpoint contract invokes the L2MessageReceiver::lzReceive(), the transaction will get reverted in step 3: https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol#L118

  • @5 -- Consequently, the message payload will be cached in the lzEndpoint's storage: https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol#L122

  • @6 -- The cached message will block all upcoming cross-chain mint messages: https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol#L116

To remedy the issue, an admin must execute the L2MessageReceiver::setParams() to configure the rewardToken and config parameters. Then, they must invoke the lzEndpoint::retryPayload() to re-submit the cached message. Lastly, all prior blocked messages must be re-submitted via the lzRelayer's validateTransactionProofV1() or validateTransactionProofV2().

As you can see, this remediation step can require a lot of gas (which can be prevented; see the Recommendations section for the solution). Moreover, this vulnerability also breaks the reward-claiming feature (i.e., Distribution::claim()) which is one of the core protocol's features.

// FILE: https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol
function retryPayload(uint16 _srcChainId, bytes calldata _srcAddress, bytes calldata _payload) external override receiveNonReentrant {
StoredPayload storage sp = storedPayload[_srcChainId][_srcAddress];
require(sp.payloadHash != bytes32(0), "LayerZero: no stored payload");
require(_payload.length == sp.payloadLength && keccak256(_payload) == sp.payloadHash, "LayerZero: invalid payload");
address dstAddress = sp.dstAddress;
// empty the storedPayload
sp.payloadLength = 0;
sp.dstAddress = address(0);
sp.payloadHash = bytes32(0);
uint64 nonce = inboundNonce[_srcChainId][_srcAddress];
@7 ILayerZeroReceiver(dstAddress).lzReceive(_srcChainId, _srcAddress, nonce, _payload); //@audit -- To remedy the issue, an admin must execute the L2MessageReceiver::setParams() to configure the 'rewardToken' and 'config' params, then invoke the lzEndpoint::retryPayload() to re-submit the cached message, later all prior blocked messages must be re-submitted via the lzRelayer's validateTransactionProofV1() or validateTransactionProofV2()
emit PayloadCleared(_srcChainId, _srcAddress, nonce, dstAddress);
}
  • @7 -- To remedy the issue, an admin must execute the L2MessageReceiver::setParams() to configure the 'rewardToken' and 'config' params, then invoke the lzEndpoint::retryPayload() to re-submit the cached message, later all prior blocked messages must be re-submitted via the lzRelayer's validateTransactionProofV1() or validateTransactionProofV2(): https://github.com/LayerZero-Labs/LayerZero/blob/48c21c3921931798184367fc02d3a8132b041942/contracts/Endpoint.sol#L140

Impact

After the issue occurs, an admin must execute the L2MessageReceiver::setParams() to configure the rewardToken and config parameters. Then, they must invoke the lzEndpoint::retryPayload() to re-submit the cached message. Lastly, all prior blocked messages must be re-submitted via the lzRelayer's validateTransactionProofV1() or validateTransactionProofV2().

As you can see, this remediation step can require a lot of gas. Moreover, this vulnerability also breaks the reward-claiming feature (i.e., Distribution::claim()) which is one of the core protocol's features.

Tools Used

Manual Review

Recommendations

Initialize the rewardToken and config parameters while executing the L2MessageReceiver__init(), like the below snippet.

- function L2MessageReceiver__init() external initializer {
+ function L2MessageReceiver__init(address rewardToken_, Config calldata config_) external initializer {
__Ownable_init();
__UUPSUpgradeable_init();
+ setParams(rewardToken_, config_);
}
- function setParams(address rewardToken_, Config calldata config_) external onlyOwner {
+ function setParams(address rewardToken_, Config calldata config_) public onlyOwner {
rewardToken = rewardToken_;
config = config_;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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