MorpheusAI

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

L2MessageReceiver::lzReceive allows to mint tokens multiple times using the same nonce

Summary

L2MessageReceiver::lzReceive allows to mint tokens multiple times using the same nonce because the nonce is not checked in the _nonblockingLzReceive subcall, leading to multiple minting

Vulnerability Details

lzReceive uses a nonce for call tracking as shown next

function lzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
uint64 nonce_,
bytes memory payload_
) external {
//...
_blockingLzReceive(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
}

And calls _blockingLzReceive:

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

Who calls nonblockingLzReceive
But Observe NONCE is not used anymore

function nonblockingLzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
bytes memory payload_
) public {
require(_msgSender() == address(this), "L2MR: invalid caller");
_nonblockingLzReceive(senderChainId_, senderAndReceiverAddresses_, payload_);

Which finally allow to use _nonblockingLzReceive multiple times to mint

function _nonblockingLzReceive(
uint16 senderChainId_,
bytes memory senderAndReceiverAddresses_,
bytes memory payload_
) private {
//...
(address user_, uint256 amount_) = abi.decode(payload_, (address, uint256));
IMOR(rewardToken).mint(user_, amount_);
}

Impact

Because nonce is not checked arbitrary amount of tokens can be minted reusing the nonce
To show this add the following test case in test/L2MessageReceiver.test.ts in #lzReceive testcase

it('REPORT mint tokens multiple times same nonce', async () => {
const address = ethers.solidityPacked(
['address', 'address'],
[await OWNER.getAddress(), await l2MessageReceiver.getAddress()],
);
const payload = ethers.AbiCoder.defaultAbiCoder().encode(
['address', 'uint256'],
[await SECOND.getAddress(), wei(1)],
);
var txx;
txx = await l2MessageReceiver.connect(THIRD).lzReceive(2, address, 5, payload);
txx = await l2MessageReceiver.connect(THIRD).lzReceive(2, address, 5, payload);
txx = await l2MessageReceiver.connect(THIRD).lzReceive(2, address, 5, payload);
txx = await l2MessageReceiver.connect(THIRD).lzReceive(2, address, 5, payload);
txx = await l2MessageReceiver.connect(THIRD).lzReceive(2, address, 5, payload);
console.log(await mor.balanceOf(await SECOND.getAddress()));
});

Observe the token balance is inflated using the same nonce

Tools Used

Manual review

Recommendations

Implement a nonce used list to avoid the nonce reuse

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.