MorpheusAI

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

MOR can be minted to incorrect address on Arbitrum

Summary

Currently the protocol assumes that the MOR tokens will be minted to the correct user address. This is not true for all cases:

  1. If the user is a Multisig (Contract Account), the same address on the destination chain (Arbitrum) might not be controlled/owned by the same user.

  2. If the user is a contract/dapp, the same address on the destination chain is not controlled/owned by the same user.

Impact

MOR tokens would be minted to an address not owned by the user, leading to loss of funds.

Vulnerability Details

Here is the whole process:

Let us understand how MOR tokens would be lost

  1. The user (i.e. using multisig/contract/dapp) calls the claim() function below by passing in the poolId_ and user_ (user's address).

  • Consider the function works as expected from Lines 156-172 (this is independent of the issue)

  • On Line 176, the function sendMintMessage() is called on the L1Sender.sol contract with user_, the pending rewards (amount of MOR that will be minted) and the refund address (user_ is the msg.sender).

File: Distribution.sol
155: function claim(uint256 poolId_, address user_) external payable poolExists(poolId_) {
156: Pool storage pool = pools[poolId_];
157: PoolData storage poolData = poolsData[poolId_];
158: UserData storage userData = usersData[user_][poolId_];
159:
160: require(block.timestamp > pool.payoutStart + pool.claimLockPeriod, "DS: pool claim is locked");
161:
162: uint256 currentPoolRate_ = _getCurrentPoolRate(poolId_);
163: uint256 pendingRewards_ = _getCurrentUserReward(currentPoolRate_, userData);
164: require(pendingRewards_ > 0, "DS: nothing to claim");
165:
166: // Update pool data
167: poolData.lastUpdate = uint128(block.timestamp);
168: poolData.rate = currentPoolRate_;
169:
170: // Update user data
171: userData.rate = currentPoolRate_;
172: userData.pendingRewards = 0;
173:
174: // Transfer rewards
175:
176: L1Sender(l1Sender).sendMintMessage{value: msg.value}(user_, pendingRewards_, _msgSender());
177:
178: emit UserClaimed(poolId_, user_, pendingRewards_);
179: }
  1. In function sendMintMessage(), the following occurs:

  • On Line 129, the user_ and the pending rewards amount is encoded into bytes memory payload variable.

  • On Line 132, the send() function is called on the LayerZero endpoint contract

124: function sendMintMessage(address user_, uint256 amount_, address refundTo_) external payable onlyDistribution {
125: RewardTokenConfig storage config = rewardTokenConfig;
126:
127:
128: bytes memory receiverAndSenderAddresses_ = abi.encodePacked(config.receiver, address(this));
129: bytes memory payload_ = abi.encode(user_, amount_);
130:
131:
132: ILayerZeroEndpoint(config.gateway).send{value: msg.value}(
133: config.receiverChainId, // communicator LayerZero chainId
134: receiverAndSenderAddresses_, // send to this address to the communicator
135: payload_, // bytes payload
136: payable(refundTo_), // refund address
137: address(0x0), // future parameter
138: bytes("") // adapterParams (see "Advanced Features")
139: );
140: }
  1. After the send() function call, LayerZero relays the cross-chain transaction to call the receivePayload() function on the Endpoint contract (Arbitrum), which ultimately calls the lzReceive() function on the L2MessageReceiver.sol contract.

  • On Line 40, function _blockingLzReceive() is called internally.

File: L2MessageReceiver.sol
32: function lzReceive(
33: uint16 senderChainId_,
34: bytes memory senderAndReceiverAddresses_,
35: uint64 nonce_,
36: bytes memory payload_
37: ) external {
38: require(_msgSender() == config.gateway, "L2MR: invalid gateway");
39:
40: _blockingLzReceive(senderChainId_, senderAndReceiverAddresses_, nonce_, payload_);
41: }
  1. Function _blockingLzReceive() calls the nonBlockingLzReceive() function here, which ultimately calls the function _nonBlockingLzReceive() here.

  2. In function _nonBlockingLzReceive() here, the following occurs:

  • Lines 97 to 103, the function checks the source id and sender are correct.

  • On Line 105, the payload is decoded to variables user_ and amount_.

  • On Line 107, the mint() function is called on the MOR token contract on Arbitrum to mint tokens to the user_. But since the user_ address on the destination chain is not owned by the actual user, the MOR tokens would be sent to someone else or no one in case the address is not being used. This would mean permanent loss of MOR tokens for the user.

File: L2MessageReceiver.sol
092: function _nonblockingLzReceive(
093: uint16 senderChainId_,
094: bytes memory senderAndReceiverAddresses_,
095: bytes memory payload_
096: ) private {
097: require(senderChainId_ == config.senderChainId, "L2MR: invalid sender chain ID");
098:
099: address sender_;
100: assembly {
101: sender_ := mload(add(senderAndReceiverAddresses_, 20))
102: }
103: require(sender_ == config.sender, "L2MR: invalid sender address");
104:
105: (address user_, uint256 amount_) = abi.decode(payload_, (address, uint256));
106:
107: IMOR(rewardToken).mint(user_, amount_);
108: }

Through this we can see how the issue arises.

Tools Used

Manual Review

References

Similar issues were also found in the C4 Maia contest:

  1. First issue

  2. Second issue

Recommendations

Encode an extra address field into the payload on the source chain. This address field would be the recipient of the MOR tokens on the destination chain.

Additionally, note that the claim() function currently allows anyone to call claim() on behalf of any user since the user_ address is taken in as a parameter (see here). Make sure to remove this user_ parameter and use msg.sender instead so that an attacker cannot provide his own recipient address and call claim() on user's behalf.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Users that interact through smart contracts, account abstaction or multisig wallets lose all rewards because they are not the owners of the same addresses on L2

Support

FAQs

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