MorpheusAI

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

Missing CEI could cause a reentrancy into minting multiple rewards

Summary

Vulnerability Details

L2MessageReceiver.sol::retryMessage() checks the transaction has a failed message as follow:

bytes32 payloadHash_ = failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
require(payloadHash_ != bytes32(0), "L2MR: no stored message");

Then, goes ahead to call the _nonblockingLzReceive before deleting the payload from the failed message:

_nonblockingLzReceive(senderChainId_, senderAndReceiverAddresses_, payload_);
delete failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];

However, in the _nonblockingLzReceive() there is an external call to the token contract to mint to the user

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

If this token is changed in the future to a token that uses a _safeMint or implements its before/afterTransfer callbacks, then an attacker would be able to receive an infinite amount of reward by reentering the retryMessage before the failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_].

Proof Of Concept:

Attack Scenario:

  • Users deposit into the protocol and are now eligible for rewards

  • Time passes and protocol makes changes to the rewardToken address to a contract that uses _safeMint in the mint function call

  • An attacker notices this and deploys a contract that deposits into the protocol and later calls claim() which sends the mint message to the l2

  • lzReceive gets triggered on l2 and call to IL2MessageReceiver(address(this)).nonblockingLzReceive() fails

  • The attacker then calls retryMessage which mints to the attacker contract, however, the payload isn't yet before making the external call.

  • So the attacker contract makes call into the retryMessage and performs the same thing again.

Impact

If this token is changed in the future to a token that uses a _safeMint or implements its before/afterTransfer callbacks, then an attacker would be able to receive an infinite amount of reward by reentering the retryMessage before the failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_].

Recommendations

Make this change

function retryMessage(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
uint64 nonce_,
bytes memory payload_
) external {
bytes32 payloadHash_ = failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
require(payloadHash_ != bytes32(0), "L2MR: no stored message");
require(keccak256(payload_) == payloadHash_, "L2MR: invalid payload");
+ delete failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
_nonblockingLzReceive(senderChainId_, senderAndReceiverAddresses_, payload_);
- delete failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
emit RetryMessageSuccess(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}
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.