Summary
retryMessage
deletes a failedMessage permanently which may lead to loss of rewards/funds/pending-rewards for a staker/user.
Vulnerability Details
The gateway will call this function to run a payload/message:
function lzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
uint64 nonce_,
bytes memory payload_
) external {
require(_msgSender() == config.gateway, "L2MR: invalid gateway");
_blockingLzReceive(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}
Then it goes to this function:
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_);
}
}
This calls the nonblockingLzReceive
in a try-catch block, it means if it is unable to mint the MOR tokens for staker, then that message/payload will be added into failedMessages
,
Now the user is able to call this function to retry to run that message:
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");
_nonblockingLzReceive(senderChainId_, senderAndReceiverAddresses_, payload_);
delete failedMessages[senderChainId_][senderAndReceiverAddresses_][nonce_];
emit RetryMessageSuccess(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}
The problem is that, if the _nonblockingLzReceive
again fails, that payload/message will be deleted without any other chance for user to execute his/her mint-message and the user will lose the pending MOR rewards.
due to that the Distribution#claim
is setting the pending reward to zero (before sending the message), then if the user is unable to run his message in retry, then the user will lose that pending rewards:
userData.pendingRewards = 0;
L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());
Impact
User may lose the rewards in the retryMessage
.
Tools Used
Manual Review
Recommendations
Consider adding a try-catch block inside the retryMessage
like the _blockingLzReceive
, and consider not deleting the message/payload while the code jumps to catch block.