`aggregationPayload.fromAmount` of an ERC20 token can be lost when calling `_transferOutAndCallV5` function for such token if target aggregator's `swapOutV5` function call reverts
Audit: 2024-06-thorchain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/5b91b5c6683a222b0ce046533515e301c9699d74/chain/ethereum/contracts/THORChain_Router.sol#L304-L389
https://github.com/code-423n4/2024-06-thorchain/blob/0e02d3c221d7e546a73f25183e29500eaa63f4cf/chain/ethereum/contracts/THORChain_Aggregator.sol#L144-L216


# Vulnerability details

## Impact
Calling the following `_transferOutAndCallV5` function for an ERC20 token executes `aggregationPayload.fromAsset.call(abi.encodeWithSignature("transfer(address,uint256)", aggregationPayload.target, aggregationPayload.fromAmount))`, which transfers `aggregationPayload.fromAmount` of such token to the target aggregator. The `_transferOutAndCallV5` function then calls the target aggregator's `swapOutV5` function below but such `swapOutV5` function call can revert, such as when the `swapRouter.swapExactTokensForETH` function call cannot satisfy `aggregationPayload.amountOutMin`. In this situation, calling the `_transferOutAndCallV5` function would not revert even though `_dexAggSuccess` would be false. Since `aggregationPayload.fromAmount` of such token has been transferred to the target aggregator, failing to use these transferred tokens in the target aggregator's `swapOutV5` function call and not reverting the `_transferOutAndCallV5` function call cause the vault to send these transferred tokens and the recipient to receive nothing in return. As a result, the recipient loses `aggregationPayload.fromAmount` of such token that she or he deserves.

https://github.com/code-423n4/2024-06-thorchain/blob/5b91b5c6683a222b0ce046533515e301c9699d74/chain/ethereum/contracts/THORChain_Router.sol#L304-L389
```solidity
  function _transferOutAndCallV5(
    TransferOutAndCallData calldata aggregationPayload
  ) private {
    if (aggregationPayload.fromAsset == address(0)) {
      ...
    } else {
      _vaultAllowance[msg.sender][
        aggregationPayload.fromAsset
      ] -= aggregationPayload.fromAmount; // Reduce allowance

      // send ERC20 to aggregator contract so it can do its thing
      (bool transferSuccess, bytes memory data) = aggregationPayload
        .fromAsset
        .call(
          abi.encodeWithSignature(
            "transfer(address,uint256)",
            aggregationPayload.target,
            aggregationPayload.fromAmount
          )
        );

      require(
        transferSuccess && (data.length == 0 || abi.decode(data, (bool))),
        "Failed to transfer token before dex agg call"
      );

      // add test case if aggregator fails, it should not revert the whole transaction (transferOutAndCallV5 call succeeds)
      // call swapOutV5 with erc20. if the aggregator fails, the transaction should not revert
      (bool _dexAggSuccess, ) = aggregationPayload.target.call{value: 0}(
        abi.encodeWithSignature(
          "swapOutV5(address,uint256,address,address,uint256,bytes,string)",
          aggregationPayload.fromAsset,
          aggregationPayload.fromAmount,
          aggr