xanoutas/solidity-security-findings huggingface dataset:
Gas Price Overestimation Due to Incorrect Standard Deviation Calculation.
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go#L281-L289


# Vulnerability details

## Impact
If the standard deviation is underestimated due to the missing `(n - 1)` division, the calculated gas price (`mean + 3x standard deviation`) may be higher than necessary. This can result in overpaying for gas and wasting resources.
[@>/bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go#L282-L289](https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go#L281-L289)
```solidity
// compute the standard deviation of cache
std := new(big.Int)
for _, fee := range e.gasCache {
    v := new(big.Int).Sub(fee, mean)
    v.Mul(v, v)
    std.Add(std, v)
}
std.Quo(std, big.NewInt(int64(e.cfg.GasCacheBlocks)))
std.Sqrt(std)
```
The issue is that the standard deviation is being calculated incorrectly. The correct formula for standard deviation is.
```solidity
std = sqrt(sum((x - mean)^2) / (n - 1))
```
However, in the code, the division by `(n - 1)` is missing. Instead, it divides by `n` (which is `e.cfg.GasCacheBlocks`).

## Proof of Concept
Suppose the gas prices in the cache are as follows: [100, 120, 110, 130, 90]. The correct calculations would be:
```solidity
mean = (100 + 120 + 110 + 130 + 90) / 5 = 110
variance = ((100 - 110)^2 + (120 - 110)^2 + (110 - 110)^2 + (130 - 110)^2 + (90 - 110)^2) / (5 - 1) = 250
standard deviation = sqrt(250) ≈ 15.81
```
However, with the missing (n - 1) division, the incorrect calculation would be:
```solidity
variance = ((100 - 110)^2 + (120 - 110)^2 + (110 - 110)^2 + (130 - 110)^2 + (90 - 110)^2) / 5 = 200
standard deviation = sqrt(200) ≈ 14.14
```
As a result, the estimated gas price would be:

   - Correct: `110 + 3 * 15.81 ≈ 157.43`
   - Incorrect: `110 + 3 * 14.14 ≈ 152.42`
The difference in the estimated gas prices can lead to the impacts mentioned above, such as overpaying or underpaying for gas and inconsistent behavior.

## Tools Used
Vs
## Recommended Mitigation Steps
By dividing by `(e.cfg.GasCacheBlocks - 1)` instead of `e.cfg.GasCacheBlocks`, the standard deviation will be calculated correctly using the unbiased estimator.
```solidity
// compute the standard deviation of cache
std := new(big.Int)
for _, fee := range e.gasCache {
    v := new(big.Int).Sub(fee, mean)
    v.Mul(v, v)
    std.Add(std, v)
}
std.Quo(std, big.NewInt(int64(e.cfg.GasCacheBlocks-1))) // Divide by (n - 1)
std.Sqrt(std)
```


## Assessed type

Math
Calling `_transferOutAndCallV5` function is always DOS'ed for all vaults when corresponding ERC20 token is a fee-on-transfer token, such as STA
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# 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
According to this protocol's [token whitelist](https://gitlab.com/thorchain/thornode/-/blob/develop/common/tokenlist/ethtokens/eth_mainnet_latest.json?ref_type=heads#L1938-1945), this protocol intends to support fee-on-transfer tokens, such as STA. As shown by STA's `transfer` and `cut` functions below, `_balances[to]` would be increased by a value that is less than the token amount that is intended to be transferred.

https://etherscan.io/address/0xa7de087329bfcda5639247f96140f9dabe3deed1#code#L115
```solidity
  function transfer(address to, uint256 value) public returns (bool) {
    ...
    uint256 tokensToBurn = cut(value);
    uint256 tokensToTransfer = value.sub(tokensToBurn);

    _balances[msg.sender] = _balances[msg.sender].sub(value);
    _balances[to] = _balances[to].add(tokensToTransfer);

    _totalSupply = _totalSupply.sub(tokensToBurn);
    ...
    return true;
  }
```

https://etherscan.io/address/0xa7de087329bfcda5639247f96140f9dabe3deed1#code#L109
```solidity
  function cut(uint256 value) public view returns (uint256)  {
    uint256 roundValue = value.ceil(basePercent);
    uint256 cutValue = roundValue.mul(basePercent).div(10000);
    return cutValue;
  }
```

For such token, when calling the following `_transferOutAndCallV5` function, executing `aggregationPayload.fromAsset.call(abi.encodeWithSignature("transfer(address,uint256)", aggregationPayload.target, aggregationPayload.fromAmount)` would transfer a token amount that is less than `aggregationPayload.fromAmount` to the aggregator. Yet, when calling the aggregator's `swapOutV5` function below, `aggregationPayload.fromAmount` is used as the `amountIn` input for calling the `UniswapV2Router02.swapExactTokensForETH` function below. Because the aggregator's balance of such token is less than `aggregationPayload.fromAmount`, executing `TransferHelper.safeTransferFrom(path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0])` that attempts to transfer `aggregationPayload.fromAmount` from the aggregator in the `UniswapV2Router02.swapExactTokensForETH` function reverts. Hence, when the corresponding ERC20 token is a fee-on-transfer token like STA, calling the `_transferOutAndCallV5` function always reverts, and such functionality is always DOS'ed for all vaults.

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 {
`_transferOutAndCallV5` Doesn't Return Funds to Recipient with Failing Aggregator
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L346-L376


# Vulnerability details

## Impact
The `_transferOutAndCallV5` function in the `THORChain_Router` contract fails to return funds to the recipient if the transfer fails due to a failing aggregator. This occurs because the function does not handle the failure of the aggregator swap correctly, leaving the funds in the failing aggregator contract. This can result in a loss of funds for the sender, as the tokens are not returned to them despite the transfer failing.

## Proof of Concept
The following proof of concept demonstrates how the `transferOutAndCallV5` function fails to return funds to the recipient when the aggregator swap fails:

### Test Case (Foundry)

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

import {Test, console} from "forge-std/Test.sol";
import {THORChain_Router} from "../contracts/THORChain_Router.sol";
import {THORChain_Aggregator} from "../contracts/THORChain_Aggregator.sol";
import {SushiRouterSmol} from "../contracts/sushiswap/SushiRouterSmol.sol";
import {WETH} from "../contracts/sushiswap/WETH.sol";
import {ERC20Token} from "../contracts/Token.sol";



contract RouterPOC is Test {
    THORChain_Router public router;
    THORChain_Aggregator public aggregator;

    SushiRouterSmol public sushiRouter;
    WETH public weth;
    ERC20Token public token;

    address public vault;

    function setUp() public {
        router = new THORChain_Router();
        weth = new WETH();
        sushiRouter = new SushiRouterSmol(address(weth));
        aggregator = new THORChain_Aggregator(address(weth), address(sushiRouter));
        token = new ERC20Token();

        vault = makeAddr("Vault");

        // deal tokens for sushiRouter and WETH
        deal(address(weth), 10e18);
        deal(address(weth), address(sushiRouter), 10e18);
    }

    function testPOC_transferOutAndCallV5_doseNotReturnToTheRecipientInCaseOfFailingAggregator() public {
        address recipient = makeAddr("recipient");
        uint256 swapOutAmount = 1e18;
        deal(address(token), vault, swapOutAmount);

        // setup a transferOutAndCallV5 payload
        THORChain_Router.TransferOutAndCallData memory aggrPayload;
        aggrPayload = THORChain_Router.TransferOutAndCallData(
            payable(address(aggregator)),
            address(token), 
            swapOutAmount,
            address(0), // native token
            payable(recipient),
            swapOutAmount * 2, // this will make the aggergator fails
            "",
            bytes(""),
            ""
        );

        vm.startPrank(vault);
        token.approve(address(router), swapOutAmount);
        router.depositWithExpiry(payable(vault), address(token), swapOutAmount, "", type(uint256).max);
        // The underlnine call of `aggregator.swapOutV5` will fail,
        // since the sushi router f
`batchTransferOutAndCallV5` Fails Due to Out of Funds
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L307-L328


# Vulnerability details

## Impact
The `batchTransferOutAndCallV5` function in the `THORChain_Router` contract fails when trying to process multiple `TransferOutAndCallData` payloads due to an "out of funds" error. This occurs because the function does not correctly handle the total Ether amount required for all payloads, leading to a situation where the transaction reverts even if sufficient funds were initially provided. This prevents the function from executing the batch transfer and can disrupt operations that rely on batch processing.

## Proof of Concept
The following proof of concept demonstrates how the `batchTransferOutAndCallV5` function fails when trying to process two payloads with a combined Ether amount that matches the initial balance but causes an "out of funds" error.

### Test Case (Foundry)

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

import {Test, console} from "forge-std/Test.sol";
import {THORChain_Router} from "../contracts/THORChain_Router.sol";
import {THORChain_Aggregator} from "../contracts/THORChain_Aggregator.sol";
import {SushiRouterSmol} from "../contracts/sushiswap/SushiRouterSmol.sol";
import {WETH} from "../contracts/sushiswap/WETH.sol";
import {ERC20Token} from "../contracts/Token.sol";



contract RouterPOC is Test {
    THORChain_Router public router;
    THORChain_Aggregator public aggregator;

    SushiRouterSmol public sushiRouter;
    WETH public weth;
    ERC20Token public token;

    address public vault;

    function setUp() public {
        router = new THORChain_Router();
        weth = new WETH();
        sushiRouter = new SushiRouterSmol(address(weth));
        aggregator = new THORChain_Aggregator(address(weth), address(sushiRouter));
        token = new ERC20Token();

        vault = makeAddr("Vault");
    }

    function testPOC_batchTransferOutAndCallV5_outOfFunds() public {
        uint256 allSwapOutAmounts = 10e18;
        deal(vault, allSwapOutAmounts);

        // setup a batchTransferOutAndCallV5 payloads
        THORChain_Router.TransferOutAndCallData[] memory aggrPayloads =
            new THORChain_Router.TransferOutAndCallData[](2);
        aggrPayloads[0] = THORChain_Router.TransferOutAndCallData(
            payable(address(aggregator)),
            address(0), // native token
            allSwapOutAmounts/2, // half of the value
            address(token),
            payable(makeAddr("recipient1")),
            0,
            "",
            bytes(""),
            ""
        );
        aggrPayloads[1] = THORChain_Router.TransferOutAndCallData(
            payable(address(aggregator)),
            address(0), // native token
            allSwapOutAmounts/2, // half of the value
            address(token),
            payable(makeAddr("recipient2")),
            0,
            "",
Use of msg.value in _transferOutAndCallV5 loop breaks batchTransferOutAndCallV5 batch functionality.
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L399
https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/ethereum/contracts/THORChain_Router.sol#L401


# Vulnerability details

## Impact
The payable external `THORChain_Router.batchTransferOutAndCallV5` function uses the internal `_transferOutAndCallV5` function for batch TransferOutAndCallV5 operations, which uses `msg.value` when calling the aggregator's `swapOutV5` function on line 399. All ether sent using `msg.value` will effectively be used in the first iteration of the `_transferOutAndCallV5` loop on line 401, and only 1 call to `swapOutV5` takes place:

```solidity
    //File:ethereum/contracts/THORChain_Router.sol

    function _transferOutAndCallV5(
        TransferOutAndCallData calldata aggregationPayload
    ) private {
        if (aggregationPayload.fromAsset == address(0)) {
        // call swapOutV5 with ether
        (bool swapOutSuccess, ) = aggregationPayload.target.call{
            value: msg.value
        }(
            abi.encodeWithSignature(
            "swapOutV5(address,uint256,address,address,uint256,bytes,string)",
            ...
        }
    }    

    ...
    function batchTransferOutAndCallV5(
        TransferOutAndCallData[] calldata aggregationPayloads
    ) external payable nonReentrant {
        for (uint i = 0; i < aggregationPayloads.length; ++i) {
        _transferOutAndCallV5(aggregationPayloads[i]);
        }
    }
```

It is understood that `THORChain_Router.batchTransferOutAndCallV5` is intended to be called by Bifrost components. Assuming normal usage of `THORChain_Router.batchTransferOutAndCallV5` (nonzero `msg.value` amounts and an `aggregationPayloads` argument with more than 1 element), this will cause all subsequent loop iterations to revert, breaking `batchTransferOutAndCallV5` functionality which may constitute as DoS. The same will occur if any other contract/EOA ever needs to call `THORChain_Router.batchTransferOutAndCallV5` to process batch transactions.

## Proof of Concept
The following Forge PoC `BatchTransferCallV5.t.sol` shows the impact of this finding:

<details>

```solidity
// SPDX-License-Identifier: NONE
pragma solidity >=0.6.0 <0.9.0;

import "lib/forge-std/src/Test.sol";
import "lib/forge-std/src/Vm.sol";
import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "contracts/THORChain_Router.sol";

contract BatchTransferOutAndCallV5Test is Test {

    THORChain_Router router1;
    address deployer;
    address bifrost;

    function setUp() public {
        deployer = makeAddr("deployer");
        bifrost = makeAddr("bifrost");
        vm.startPrank(deployer);
        router1 = new THORChain_Router();
        vm.stopPrank();
    }

    function test_audit_batchTransferOutAndCallV5() public {
        MockAggregator mag = new MockAggregator();
        vm.startPrank(bifrost
Due to incomplete check, in some cases, Router contracts may hold some ETH
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L240
https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L247


# Vulnerability details

## Impact
The Router contract should not hold ETH, which will be sent to the vault. However, due to incomplete function verification, the Router contract may hold ETH

## Proof of Concept
When calling transferOutV5 function, there is no check to see if the sum of the amount field in the TransferOutData[] array is equal to msg.vaule. If msg. vaule is greater than sum (amount), there will be some ETH stuck in the contract
github:[[1](https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L240)]
```solidity
  function batchTransferOutV5(
    TransferOutData[] calldata transferOutPayload
  ) external payable nonReentrant {
    for (uint i = 0; i < transferOutPayload.length; ++i) {
      _transferOutV5(transferOutPayload[i]);
    }
  }

  function transferOutV5(
    TransferOutData calldata transferOutPayload
  ) public payable nonReentrant {
    _transferOutV5(transferOutPayload);
  }

  function _transferOutV5(TransferOutData memory transferOutPayload) private {
    if (transferOutPayload.asset == address(0)) {
      bool success = transferOutPayload.to.send(transferOutPayload.amount); // Send ETH.
      if (!success) {
        payable(address(msg.sender)).transfer(transferOutPayload.amount); // For failure, bounce back to vault & continue.
      }
    }else{
        ...
    }

```
Assuming the following scenario:
1. there is a transferOutV5 call with an assign of ETH and an amount of 1e18. 
2. If an msg.vaule of 2e18 is passed in during the call
3. there will be 1e18 of ETH stuck in the contract
## Tools Used
Manual review
## Recommended Mitigation Steps
Check if msg.vaule is equal to amount before calling _transferOutV5
```solidity
  function transferOutV5(
    TransferOutData calldata transferOutPayload
  ) public payable nonReentrant {
+    if(transferOutPayload.assert == address(0)){
+        require(transferOutPayload.amount == msg.vaule);
+    }
    _transferOutV5(transferOutPayload);
  }
```
```solidity
  function batchTransferOutV5(
    TransferOutData[] calldata transferOutPayload
  ) external payable nonReentrant {
+    uint256 totalETHAmount = 0;
+    for (uint i = 0; i < transferOutPayload.length; ++i) {
+       if(transferOutPayload[i].assert == address(0)){
+          totalETHAmount += transferOutPayload[i].amount;
+       }
+    }
+    require(totalETHAmount == msg.sender);
    for (uint i = 0; i < transferOutPayload.length; ++i) {
      _transferOutV5(transferOutPayload[i]);
    }
  }
```






## Assessed type

Access Control
The batchTransferOutAndCallV5 function can never handle batch operations with ETH
Audit: 2024-06-thorchain-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L397


# Vulnerability details

## Impact
The batchTransferOutAndCallV5 function is used for batch processing of _transferOutAndCallV5 calls. However, due to the implementation of _transferOutAndCallV5 not supporting batch calls for ETH, batch processing of batchTransferOutAndCallV5 with ETH may fail.

## Proof of Concept
If the asset is ETH,  _transferOutAndCallV5 will pass in the entire msg.value to target.This may result in insufficient ETH for payment in the following ETH processing
github:[https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L310](https://github.com/code-423n4/2024-06-thorchain/blob/e3fd3c75ff994dce50d6eb66eb290d467bd494f5/chain/ethereum/contracts/THORChain_Router.sol#L310)
```solidity
function _transferOutAndCallV5(
    TransferOutAndCallData calldata aggregationPayload
  ) private {
    if (aggregationPayload.fromAsset == address(0)) {
      // call swapOutV5 with ether
      (bool swapOutSuccess, ) = aggregationPayload.target.call{
        value: msg.value
      }(
        abi.encodeWithSignature(
          "swapOutV5(address,uint256,address,address,uint256,bytes,string)",
          aggregationPayload.fromAsset,
          aggregationPayload.fromAmount,
          aggregationPayload.toAsset,
          aggregationPayload.recipient,
          aggregationPayload.amountOutMin,
          aggregationPayload.payload,
          aggregationPayload.originAddress
        )
      );
```
1. Assuming the following situation, it is necessary to batch process two ETH requests
2. request1 fromAmount = 1e18 toAsset=USDC recipient = addr1
   request2 fromAmount = 2e18 toAsset=DAI  recipect = addr2
3. call batchTransferOutAndCallV5 msg.value = 3e18
4. When executing request1, all 3e18 ETH will be passed in aggregationPayload.target, swaped to USDC, and request2 will not have enough ETH to pass in.

## Tools Used
Manual audit
## Recommended Mitigation Steps
```solidity
function _transferOutAndCallV5(
    TransferOutAndCallData calldata aggregationPayload
  ) private {
    if (aggregationPayload.fromAsset == address(0)) {
      // call swapOutV5 with ether
      (bool swapOutSuccess, ) = aggregationPayload.target.call{
-        value: msg.value
+        value: aggregationPayload.fromAmount
      }(
        abi.encodeWithSignature(
          "swapOutV5(address,uint256,address,address,uint256,bytes,string)",
          aggregationPayload.fromAsset,
          aggregationPayload.fromAmount,
          aggregationPayload.toAsset,
          aggregationPayload.recipient,
          aggregationPayload.amountOutMin,
          aggregationPayload.payload,
          aggregationPayload.originAddress
        )
      );
```


## Assessed type

ETH-Transfer