xanoutas/solidity-security-findings huggingface dataset:
EVM RPC errors may lead to missed inbound transactions
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L846
https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L883


# Vulnerability details

## Impact

Inbound transactions may be missed and are not voted upon by observers, potentially leading to a loss of funds.

## Proof of Concept

ZetaChain observers watch external EVM chains via the `ExternalChainWatcher` function that internally [calls the `observeInTX` function](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L725) on each `ob.GetCoreParams().InTxTicker` ticker.

The `observeInTX` function performs multiple tasks:

1. Query for zeta sent (`ZetaSent`) logs
2. Query for ERC-20 deposited logs
3. Query tx's that are sent to the TSS address

The queried blocks are bound by the range of `startBlock` and `toBlock` , which are set in lines `809-810`. The `startBlock` is the previously processed `toBlock` (i.e., retrieved via `ob.GetLastBlockHeightScanned()`), incremented by 1.

At the end of the function, [in line `988`, the `toBlock` is set as the new `lastBlockHeightScanned`](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L988).

However, in lines [`846`](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L846) and [`883`](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/node/zetaclient/evm_client.go#L883), an early `return` statement would skip the current tasks and would still proceed to store the `toBlock` as the new `lastBlockHeightScanned`, even though the blocks (and their logs) have not been fully processed.

This can be the case if the used RPC has temporary issues and the `FilterZetaSent` or `FilterDeposited` functions return an error.

```go
774: func (ob *EVMChainClient) observeInTX() error {
...   // [...]
808: 	lastBlock := ob.GetLastBlockHeightScanned()
809: 	startBlock := lastBlock + 1
810: 	toBlock := lastBlock + config.MaxBlocksPerPeriod // read at most 10 blocks in one go
...   // [...]
823: 	//task 1:  Query evm chain for zeta sent logs
824: 	func() {
...     // [...]
838: 		logs, err := connector.FilterZetaSent(&bind.FilterOpts{
839: 			// #nosec G701 always positive
840: 			Start:   uint64(startBlock),
841: 			End:     &tb,
842: 			Context: context.TODO(),
843: 		}, []ethcommon.Address{}, []*big.Int{})
844: 		if err != nil {
845: 			ob.logger.ChainLogger.Warn().Err(err).Msgf("observeInTx: FilterZetaSent error:")
846: ❌			return
847: 		}
...   // [...]
863: 	}()
864:
865: 	// task 2: Query evm chain for deposited logs
866: 	func() {
867: 		// #nosec G701 always positive
868: 		toB := uint64(toBlock)
869: 		erc20custo
Wrong trident swap method used for getZetaFromEth in ZetaTokenConsumerTrident.strategy.sol, causing all swaps to revert. 
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerTrident.strategy.sol#L93


# Vulnerability details

## Impact
`getZetaFromEth()` in ZetaTokenConsumerTrident.strategy.sol will always revert, due to using the wrong trident router method.

## Proof of Concept
In ZetaTokenConsumerTrident.strategy.sol, when swapping zeta token to Eth, `getZetaFromEth()` will be called, which first transfer zeta token from caller and then approve `tridentRouter` to manage the transferred `zetaTokenAmount`. Then it calls `tridentRouter.exactInput(params)` to perform the swap. However, `tridentRouter.exactInput()` will not transfer the raw zeta token, but will only transfer bento share balance before performing the swap.

```solidity
//contracts/evm/tools/ZetaTokenConsumerTrident.strategy.sol
    function getEthFromZeta(
        address destinationAddress,
        uint256 minAmountOut,
        uint256 zetaTokenAmount
    ) external override returns (uint256) {
...
        IERC20(zetaToken).safeTransferFrom(msg.sender, address(this), zetaTokenAmount);
        IERC20(zetaToken).safeApprove(address(tridentRouter), zetaTokenAmount);
...
        //@audit exactInputsingle is the wrong method to call, because it will only transfer bento share balance, not raw zeta token, which will cause the transaction to revert.
|>      uint256 amountOut = tridentRouter.exactInputSingle(params);
...
```
(https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerTrident.strategy.sol#L93)
```solidity
//https://github.com/sushiswap/trident/blob/master/contracts/TridentRouter.sol
    function exactInput(ExactInputParams calldata params) public payable returns (uint256 amountOut) {
        // Pay the first pool directly.
        //@audit-info this calls bento box vault to transfer bento shares instead of actual raw ERC-20
|>      bento.transfer(params.tokenIn, msg.sender, params.path[0].pool, params.amountIn);
...
```
```solidity
//https://etherscan.io/address/0xf5bce5077908a1b7370b9ae04adc565ebd643966#code
//@audit-info allowed(from) modifier checks if caller(tridentRouter) has approval to manage from's bento shares, not raw erc-20.
    function transfer(
        IERC20 token,
        address from,
        address to,
        uint256 share
    ) public allowed(from) {
        // Checks
        require(to != address(0), "BentoBox: to not set"); // To avoid a bad UI from burning funds
        // Effects
        //@audit-info note: this modifies bento shares not raw erc-20. Since `ZetaTokenConsumerTrident.strategy.sol` will not have means to deposit into bento, nor approve `tridentRouter` for bento shares, transaction will revert at `allowed(from)`
|>      balanceOf[token][from] = balanceOf[token][from].sub(share);
        balanceOf[token][to] = balanceOf[token][to].add(share);
lack of proper input validation
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerUniV2.strategy.sol#L34
https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerUniV2.strategy.sol#L58


# Vulnerability details

 lack of proper input validation and checks in the getZetaFromEth and getZetaFromToken functions. Specifically, there is no validation to ensure that the provided destinationAddress is a valid address.

Here is the relevant code snippet:

solidity

if (destinationAddress == address(0)) revert ZetaCommonErrors.InvalidAddress();

While this check ensures that the destinationAddress is not the zero address, it does not verify whether the address is a valid and potentially malicious contract address. An attacker could exploit this by providing a malicious contract address, leading to unexpected behavior or vulnerabilities.

To mitigate this risk, consider implementing a more comprehensive validation for the destinationAddress. You can use the Address library from OpenZeppelin to check if the address is a contract:

solidity

import "@openzeppelin/contracts/utils/Address.sol";

// ...

if (destinationAddress == address(0) || !Address.isContract(destinationAddress)) {
    revert ZetaCommonErrors.InvalidAddress();
}

By using Address.isContract, you can ensure that the provided destinationAddress is a valid contract address, reducing the risk of potential attacks.
remix


## Assessed type

Invalid Validation
Reentrancy Vulnerability
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerPancakeV3.strategy.sol#L56


# Vulnerability details

•	Issue: The _locked state variable is used to implement a basic form of reentrancy protection. However, this method is not foolproof. There are more robust ways to handle reentrancy.
•	Solution: Consider using the "ReentrancyGuard" pattern from the OpenZeppelin library to provide a more secure solution for handling reentrancy. This pattern ensures that functions marked with the nonReentrant modifier cannot be called recursively.
  solidity
	// Import the ReentrancyGuard from OpenZeppelin
	import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

	// Inherit from ReentrancyGuard
	contract ZetaTokenConsumerPancakeV3 is ZetaTokenConsumer,      ZetaTokenConsumerUniV3Errors, ReentrancyGuard {
    // Existing code...

    // Replace existing modifier with nonReentrant modifier
    modifier nonReentrant() {
        _nonReentrant();
        _;
    }

    // Additional check for reentrancy
    function _nonReentrant() internal view {
        require(!_locked, "ReentrancyGuard: reentrant call");
    }

    // Existing code...
    }

remix,code testing, manual code execution


## Assessed type

Reentrancy
1.	Access Control Bypass
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/zevm/ZRC20.sol#L52


# Vulnerability details

The onlyFungible modifier is used to restrict certain functions to only be callable by the FUNGIBLE_MODULE_ADDRESS. However, the modifier is only checking the sender's address and does not consider potential delegate calls, allowing an attacker to bypass the access control.
o	Enhance the onlyFungible modifier to include a check for the contract's code. This ensures that only direct calls from the FUNGIBLE_MODULE_ADDRESS can execute the restricted functions.
	    solidity
modifier onlyFungible() {
    require(msg.sender == FUNGIBLE_MODULE_ADDRESS && isContract(FUNGIBLE_MODULE_ADDRESS), "Caller is not the fungible module");
    _;
}

function isContract(address account) internal view returns (bool) {
    	uint256 size;
    	assembly {
        size := extcodesize(account)
    	}
    		return size > 0;
	}
The isContract function checks if the given address corresponds to a smart contract. By combining this check with the address validation, the modifier becomes more secure against delegate call attacks.
remix,manual test


## Assessed type

Access Control
sends eth to arbitrary user
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerPancakeV3.strategy.sol#L178


# Vulnerability details

## Impact
sends eth to arbitrary user

## Proof of Concept
Dangerous Call
(bool sent, ) = destinationAddress.call{value: amountOut}("");

## Tools Used
None



## Assessed type

call/delegatecall
Reentrancy Attack Vulnerability on getZetaFromETH() function
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerTrident.strategy.sol#L74-L97


# Vulnerability details

## Impact
This line is potentially vulnerable to a reentrancy attack because it makes an external call to another contract before the reentrancy lock is set. If the tridentRouter contract is malicious and it calls back into the ZetaTokenConsumerTrident contract during the execution of exactInputSingle, it could cause unexpected behavior.

## Tools Used
Manual Review

## Recommended Mitigation Steps
To fix this vulnerability, the reentrancy lock should be set before the external call. The corrected code would look like this:
```solidity
uint256 amountOut;
_locked = true;
amountOut = tridentRouter.exactInputSingle{value: msg.value}(params);
_locked = false;
```


## Assessed type

Reentrancy
`onRevert()` would send tokens to the wrong address and could end up being lost
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/ZetaConnector.eth.sol#L72-L111


# Vulnerability details


## Proof of Concept

When the Connector calls `onZetaRevert`, this struct is used as the argument, as explained [here](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/zevm/ZetaConnectorZEVM.sol#L38-L47)

```solidity
     */
    struct ZetaRevert {
        address zetaTxSenderAddress;
        uint256 sourceChainId;
        bytes destinationAddress;
        uint256 destinationChainId;
        /// @dev Equals to: zetaValueAndGas - ZetaChain gas fees - destination chain gas fees - source chain revert tx gas fees
        uint256 remainingZetaValue;
        bytes message;
    }
```

Now take a look at [ZetaConnector.eth.sol#L72-L111](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/ZetaConnector.eth.sol#L72-L111)

```solidity
    /**
     * @dev Handler to receive errors from other chain.
     * This method can be called only by TSS.
        //@audit
     * Transfers the Zeta tokens to destination and calls onZetaRevert if it's needed.
     */
    function onRevert(
        address zetaTxSenderAddress,
        uint256 sourceChainId,
        bytes calldata destinationAddress,
        uint256 destinationChainId,
        uint256 remainingZetaValue,
        bytes calldata message,
        bytes32 internalSendHash
    ) external override whenNotPaused onlyTssAddress {
        //@audit
        bool success = IERC20(zetaToken).transfer(zetaTxSenderAddress, remainingZetaValue);
        if (!success) revert ZetaTransferError();

//... ommited for brevity
```

Additionally note the implementation of `onReceive()` from [ZetaConnector.eth.sol#L47-L70](https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/ZetaConnector.eth.sol#L47-L70)

```solidity
    /**
     * @dev Handler to receive data from other chain.
     * This method can be called only by TSS.
     //@audit
     * Transfers the Zeta tokens to destination and calls onZetaMessage if it's needed.
     */
    function onReceive(
        bytes calldata zetaTxSenderAddress,
        uint256 sourceChainId,
        address destinationAddress,
        uint256 zetaValue,
        bytes calldata message,
        bytes32 internalSendHash
    ) external override onlyTssAddress {
        //@audit
        bool success = IERC20(zetaToken).transfer(destinationAddress, zetaValue);
        if (!success) revert ZetaTransferError();

```

As seen, asides the fact that this statement has been attached to both functions, i.e `Transfers the Zeta tokens to destination and calls onZetaRevert if it's needed.` one would understand that the intended way of this functioning is to have the de
Missing function call causes loss of user's ETH
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/b237708ed5e86f12c4bddabddfd42f001e81941a/repos/protocol-contracts/contracts/evm/tools/ZetaTokenConsumerUniV3.strategy.sol#L92


# Vulnerability details

## Impact
There is a bug in `getEthFromZeta()` function of `ZetaTokenConsumerUniV3` contract that creates risk for users to lost their ETH's.

## Proof of Concept
We have to take into consideration how `ExactInputSingle()` works. 

Here's some knowledge for understanding the mechanics of the bug:

* Uniswap V3 works with 2 types of swaps : `exactInput` and `exactOutput`. With `exactInput`, one specifies the exact amount of input token (s)he'd like to swap with.
* To interact with Uniswap and execute the swaps, `V3 router` is used.
* If input token specified is native ETH, it is sent directly to the router contract in swap tx.
* In some cases not all of the ETH sent is used/required.
* Router has `refundETH()` for this case, which transfers back all ETH in contract to `msg.sender`.

```solidity
function refundETH() external payable override {
    if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
}
```

It is clear how important to call `refundETH()` at the end of each tx made with the router contract. If not, you might end up leaving users' `unused` ETH in contract. Thanks to the way refundETH function is written, who calls the refundETH after you, will receive all remaining ETH.

Even though this case is more likely with `exactOutput` using ETH as input token since you don't know exact number of input amount required, it is possible to have ETH left over within with `exactInput` swap as well.
This happens/ed when enough liquidity not found in pool, relative to size of the swap. In this situation router does not include a call to `refundETH()` and left ETH simply waits for someone else to claim.
Though `amountOutMinimum` in `exactInputSingleParam` lets user specify the minimum amount to receive, it does not strictly prevent this from happening as there are many users have been impacted by that :

Not only but [here's](https://arbiscan.io/tx/0xd48a7427a059c053ea4cac9df2bb6ba3a1daa8d2dc5fc542c4c8b5bbc4272b29) a tx swapping 13.85 ETH for RDNT token with `exactInput` swap. Because there is not enough liquidity in pool only ~2.537 ETH were able to be swapped. Since `exactInput` swap does not call refundETH, user's ETH was left in the router contract.

## Tools Used
Manual review

## Recommended Mitigation Steps
Calling `uniswapRouter.refundETH()` just after `exactInputSingle()` and sending left ETH(if there is) to the user since this function will transfer to it's own `msg.sender` which is router contract.



## Assessed type

ETH-Transfer
Non-deterministic iteration in  func InitGenesis Objects in the key-value store
Audit: 2023-11-zetachain-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/genesis.go#L11


# Vulnerability details

## Impact
Due to the non-deterministic behavior of iteration over maps in Go, there could be two different types of errors in `ballotListForHeight`. Some examples could be Inconsistent State Initialization and Inconsistent Behavior. In Go, iteration over a map does not guarantee any specific order. This means that the order in which the `BallotListForHeight` objects are stored in the key-value store can vary between different runs of the program.

The `BallotListForHeight` is a struct that represents a list of ballots for a specific height. It has two properties: `Height` and `BallotsIndexList`.

The Height property is an `int64` that represents the height for which the ballot list is applicable.

The `BallotsIndexList` property is a slice of strings. Each string in this slice is an index of a `ballot`. The `GetBallotList` function is used to retrieve a `BallotListForHeight` from the store using a height.

The `AddBallotToList` function retrieves a `BallotListForHeight` for a specific height, adds a ballot to it, and then stores it back in the store.

The `GetMaturedBallotList` function retrieves a `BallotListForHeight` for a specific height and returns the `BallotsIndexList`. The specific height is calculated as the current block height minus the `BallotMaturityBlocks` parameter.

Once the order in which the `BallotListForHeight` objects are stored in the key-value store. This order is not guaranteed to be consistent because Go's map does not guarantee a specific iteration order!!


## Tools Used
Manual Review;
Unit Test;
Integration Test;
VSCODE
## Recommended Mitigation Steps
Modifying the data structure that holds the set of dependencies so that iteration becomes deterministic. For example, an array could be used which could be iterated over in the deduplication step in line 76(Line)[https://github.com/code-423n4/2023-11-zetachain/blob/main/repos/node/x/observer/genesis.go#L76]. Alternatively, insert the code to sort the keys of the ballotListForHeight map right before the loop it iterate over this map to set the ballot list. This would be between lines 76 and 77.
OP1:
```GO
// Existing code up to line 76...
//..........................
// Replace the map with a slice of struct that holds height and ballotList
type heightBallotList struct {
    Height     int64
    BallotList []string
}
var ballotListForHeight []heightBallotList

// When adding to ballotListForHeight, search for the existing height and append to its BallotList
// If the height does not exist, append a new heightBallotList to ballotListForHeight

// Sort the slice before iterating over it
sort.Slice(ballotListForHeight, func(i, j int) bool {
    return ballotListForHeight[i].Height < ballotListForHeight[j].Height
})

// Iterate over the sorted slice
for _, hbl := range ballotListForHeight {
    k.SetBallotList(ctx, &types.BallotListForHeigh