xanoutas/solidity-security-findings huggingface dataset:
Hardcoded gas used in ERC20 queries allows for block production halt from infinite recursion
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L149
https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L285


# Vulnerability details

The `funtoken` precompile allows an EVM caller to access information about tokens that coexist in the Cosmos ("coin") and EVM ("ERC20") spaces.

Some operations performed by this precompile consist of EVM calls; for example, if we look at the `balance` method:
```go
File: funtoken.go
265: func (p precompileFunToken) balance(
266: 	start OnRunStartResult,
267: 	contract *vm.Contract,
268: ) (bz []byte, err error) {
---
285: 	erc20Bal, err := p.evmKeeper.ERC20().BalanceOf(funtoken.Erc20Addr.Address, addrEth, ctx)
286: 	if err != nil {
287: 		return
288: 	}
```
we see that for fetching the EVM info, it calls the `evmKeeper.ERC20().BalanceOf` function:
```go
File: erc20.go
125: func (e erc20Calls) BalanceOf(
126: 	contract, account gethcommon.Address,
127: 	ctx sdk.Context,
128: ) (out *big.Int, err error) {
129: 	return e.LoadERC20BigInt(ctx, e.ABI, contract, "balanceOf", account)
130: }
```
which in turn calls `LoadERC20BigInt`:
```go
File: erc20.go
222: func (k Keeper) LoadERC20BigInt(
223: 	ctx sdk.Context,
224: 	abi *gethabi.ABI,
225: 	contract gethcommon.Address,
226: 	methodName string,
227: 	args ...any,
228: ) (out *big.Int, err error) {
229: 	res, err := k.CallContract(
230: 		ctx,
231: 		abi,
232: 		evm.EVM_MODULE_ADDRESS, // @audit from
233: 		&contract,
234: 		false, // @audit commit = false
235: 		Erc20GasLimitQuery, // @audit 100_000
236: 		methodName,
237: 		args...,
238: 	)
239: 	if err != nil {
240: 		return nil, err
241: 	}
```

If we look closely to how this callback to the EVM is done, we see that the gas allowed for this call is hardcoded to `100_000` and is charged only after the call returned.

This is problematic because `100_000` is allocated regardless of the gas limit used to call the `funtoken` precompile, and this breaks the [core invariant of the 63/64 gas allocation](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) that ultimately secures EVM implementation from infinite recursions, which can halt block production and cause the validator to be slashed.

While the `balance` example was described in detail, the same applies to the `Transfer` call in `sendToBank`:

```go
File: funtoken.go
109: func (p precompileFunToken) sendToBank(
110: 	startResult OnRunStartResult,
111: 	caller gethcommon.Address,
112: 	readOnly bool,
113: ) (bz []byte, err error) {
---
149: 	gotAmount, transferResp, err := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
150: 	if err != nil {
151: 		return nil, fmt.Errorf("error in ERC20.transfer from caller to EVM account: %w", err)
152: 	}
```

The `Burn` call in `sendToBank` is instead secure because it only applies to ERC20 tokens deployed from Coins whose EVM contract is safe
`bankBalance` precompile unintentionally skips error validation for parsed arguments
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L364-L391


# Vulnerability details

## Proof of Concept

Take a look at [`bankBalance`](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L378-L379) method; the function doesn't check if the error returned from `parseArgsBankBalance` is non-nil before proceeding:

```go
func (p precompileFunToken) bankBalance(
	start OnRunStartResult,
	contract *vm.Contract,
) (bz []byte, err error) {
	method, args, ctx := start.Method, start.Args, start.CacheCtx
	defer func() {
		if err != nil {
			err = ErrMethodCalled(method, err)
		}
	}()
	if err := assertContractQuery(contract); err != nil {
		return bz, err
	}

@> addrEth, addrBech32, bankDenom, err := p.parseArgsBankBalance(args)
@> bankBal := p.evmKeeper.Bank.GetBalance(ctx, addrBech32, bankDenom).Amount.BigInt()

	return method.Outputs.Pack([]any{
		bankBal,
		struct {
			EthAddr    gethcommon.Address `json:"ethAddr"`
			Bech32Addr string             `json:"bech32Addr"`
		}{
			EthAddr:    addrEth,
			Bech32Addr: addrBech32.String(),
		},
	}...)
}
```

If `parseArgsBankBalance` returns an error, it continues to execute and tries to get the balance using invalid addresses or denomination. 


All other methods ([`sendToBank`](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L119-L122), [`balance`](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L279-L283), [`whoAmI`](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L456-L459)) properly check for errors from their respective parsing functions before proceeding with their logic.

## Impact
By not checking the error, the function may attempt to use invalid addresses or denominations which could lead to panics. 


## Recommendation
Consider checking the error immediately after `parseArgsBankBalance` and return if there's an error, similar to how other methods handle errors.

```diff
func (p precompileFunToken) bankBalance(
	start OnRunStartResult,
	contract *vm.Contract,
) (bz []byte, err error) {
	method, args, ctx := start.Method, start.Args, start.CacheCtx
	defer func() {
		if err != nil {
			err = ErrMethodCalled(method, err)
		}
	}()
	if err := assertContractQuery(contract); err != nil {
		return bz, err
	}

 addrEth, addrBech32, bankDenom, err := p.parseArgsBankBalance(args)
+ if err != nil {
+     return nil, err
+ }
 bankBal := p.evmKeeper.Bank.GetBalance(ctx, addrBech32, bankDenom).Amount.BigInt()

	return method.Outputs.Pack([]any{
		bankBal,
		struct {
			EthAddr    gethcommon.Address `json:"ethAddr"`
			Bech32Addr string             `json:"bech32Addr"`
		}{
			EthAddr:    addrEth,
			Bech32Addr: addrBech32.String(),
		},
	}...)
}
```
Refund logic is not correct when applying EVM message
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L87


# Vulnerability details

## Finding description and impact

Currently, refunds are incorrectly calculated after EVM message is applied. This is because the function `EthereumTx()` uses different formula rather than using refund that's calculated inside of the `ApplyEvmMsg()` function. This leads to a situation where NibiruVM loses compatibility with EVM as refunds are handled differently.

## Proof of Concept

The current refund functionality is implemented the following way:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L85-92
```
	refundGas := uint64(0)
	if evmMsg.Gas() > blockGasUsed {
		refundGas = evmMsg.Gas() - blockGasUsed
	}
	weiPerGas := txMsg.EffectiveGasPriceWeiPerGas(evmConfig.BaseFeeWei)
	if err = k.RefundGas(ctx, evmMsg.From(), refundGas, weiPerGas); err != nil {
		return nil, errors.Wrapf(err, "EthereumTx: error refunding leftover gas to sender %s", evmMsg.From())
	}

```

As you can see here, the `refundGas` is first set to 0 and then it's calculated by using the difference between gas limit of the message and total block gas used. Later, it just calls `RefundGas()` function from the `gas_fees` where the funds are finally transferred to the `from` account. However, that's not correct, as the refunds are calculated in the geth client the following way:

https://github.com/NibiruChain/go-ethereum/blob/nibiru/geth/core/state_transition.go#L368-383
```
func (st *StateTransition) refundGas(refundQuotient uint64) {
	// Apply refund counter, capped to a refund quotient
	refund := st.gasUsed() / refundQuotient
	if refund > st.state.GetRefund() {
		refund = st.state.GetRefund()
	}
	st.gas += refund

	// Return ETH for remaining gas, exchanged at the original rate.
	remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
	st.state.AddBalance(st.msg.From(), remaining)

	// Also return remaining gas to the block gas counter so it is
	// available for the next transaction.
	st.gp.AddGas(st.gas)
}

```

So it first calculates the `refund` value by using `gasUsed` and `refundQuotient` parameters. This logic is implemented inside of the `ApplyEvmMsg()` in the Nibiru client:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L369-381
```
refundQuotient := params.RefundQuotientEIP3529
	if fullRefundLeftoverGas {
		refundQuotient = 1 // 100% refund
	}
	temporaryGasUsed := msg.Gas() - leftoverGas
	refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient)

	// update leftoverGas and temporaryGasUsed with refund amount
	leftoverGas += refund
	temporaryGasUsed -= refund
	if msg.Gas() < leftoverGas {
		return nil, evmObj, errors.Wrapf(core.ErrGasUintOverflow, "ApplyEvmMsg: message gas limit (%d) < leftover gas (%d)", msg.Gas(), leftoverGas)
	}

```

It uses the formula from the `gas_fees` file:

https://github.com/code-423n4/2024-11-nibiru/blob/m
The protocol is not compatible with tokens that don't return any values
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L594


# Vulnerability details

## Finding description and impact

Currently, Nibiru uses `transfer()` method to handle the ERC20 tokens' transfer operations. However, there are such tokens that do not return any values and the protocol would not be able to handle such scenarios.

## Proof of Concept

There are tokens that don't return boolean for ERC20 operations. The full list can be checked here:

https://github.com/d-xo/weird-erc20?tab=readme-ov-file#no-revert-on-failure
```
Some tokens do not return a bool (e.g. USDT, BNB, OMG) on ERC20 methods. see here for a comprehensive (if somewhat outdated) list.
```

Currently, the protocol uses `transfer()` method for ERC20 operations instead of OZ `safeTransfer()` that could handle such scenarios:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/erc20.go#L85
```
resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)

```


## Recommended mitigation steps

Use `safeTransfer()` that is considered to be the most secure version of the ERC20 token transfer method.
Premature loop termination in pending tx nonce could cause incorrect nonce values to be returned to users
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/eth/rpc/backend/utils.go#L91


# Vulnerability details

## Proof of Concept
Take a look at [`getAccountNonce`](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/eth/rpc/backend/utils.go#L91) function:

```go
  // add the uncommitted txs to the nonce counter
  // only supports `MsgEthereumTx` style tx
  for _, tx := range pendingTxs {
      for _, msg := range (*tx).GetMsgs() {
          ethMsg, ok := msg.(*evm.MsgEthereumTx)
          if !ok {
              // not ethereum tx
@>            break  
          }
  
          sender, err := ethMsg.GetSender(b.chainID)
          if err != nil {
            continue
          }
          if sender == accAddr {
              nonce++
          }
      }
  }
```

Problem's in the `break` statement when encountering a non-Ethereum tx. Using `break` here will exit the inner `for` loop that iterates over messages potentially missing other valid Ethereum txs in the same tx that might come after a non Ethereum message. This can also result in undercounting of the nonce for accounts with pending txs.

## Recommendation

Consider using `continue` instead of `break` to skip only the current message and continue checking the remaining messages in the transaction.


```diff
for _, tx := range pendingTxs {
    for _, msg := range (*tx).GetMsgs() {
        ethMsg, ok := msg.(*evm.MsgEthereumTx)
        if !ok {
            // not ethereum tx
-             break
+            continue 
        }

        sender, err := ethMsg.GetSender(b.chainID)
        if err != nil {
            continue
        }
        if sender == accAddr {
            nonce++
        }
    }
}
```

The current implementation might lead to undercounting the nonce if there are valid Ethereum transactions that appear after a non-Ethereum message in the same transaction, which could cause transaction ordering issues.
bankDenom is set to erc20 address converted to string instead of the actual denom
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/funtoken_from_erc20.go#L122


# Vulnerability details

## Finding description and impact

Currently `bankDenom` is converted to the string of the ERC20 address. The problem is that it's not supposed to be a string of an address but rather a base denomination with exponent of 0 leading to an incorrect FunToken metadata construction.

## Proof of Concept

Take a look at how `bankDenom` is assigned:

```
bankDenom := fmt.Sprintf("erc20/%s", erc20.String())

```

So it takes input parameter and converts it to string:

```
func (k *Keeper) createFunTokenFromERC20(
	ctx sdk.Context, erc20 gethcommon.Address,
)

```

As you can see here, `erc20` here is equal to the ETH address so it will be just an address converted to a string. Then it creates FunToken mapping the following way:

```
funtoken = &evm.FunToken{
		Erc20Addr: eth.EIP55Addr{
			Address: erc20,
		},
		BankDenom:      bankDenom,
		IsMadeFromCoin: false,
	}
```

As you can see, `BankDenom` is equal to the `bankDenom` assigned to a string instead of being an actual bankDenom. From the docs:

https://nibiru.fi/docs/evm/funtoken.html
```
Base denomination. The DenomUnit.Denom with an exponent of 0.

```

For example, for wei it has to be "Wei" as `bankDenom` instead of the string of the ERC20 address.

## Recommended mitigation steps

Change the `bankDenom` so it reflects the actual value accordingly.
evmante_increment_sender_seq does not check if the operation is the contract creation
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_increment_sender_seq.go#L70


# Vulnerability details

## Finding description and impact

The current functionality of `evmante_increment_sender_seq` is designed to increment a nonce parameter of an account (sender). The problem is that there is no check whether the operation is a contract creation leading to a situation where a nonce will be incremented NOT during the contract creation process.

## Proof of Concept

At the moment `AnteHandle()` just increments a nonce for a specific sender:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_increment_sender_seq.go#L52-68
```
acc := issd.accountKeeper.GetAccount(ctx, msgEthTx.GetFrom())
		if acc == nil {
			return ctx, errors.Wrapf(
				sdkerrors.ErrUnknownAddress,
				"account %s is nil", gethcommon.BytesToAddress(msgEthTx.GetFrom().Bytes()),
			)
		}
		nonce := acc.GetSequence()

		// we merged the nonce verification to nonce increment, so when tx includes multiple messages
		// with same sender, they'll be accepted.
		if txData.GetNonce() != nonce {
			return ctx, errors.Wrapf(
				sdkerrors.ErrInvalidSequence,
				"invalid nonce; got %d, expected %d", txData.GetNonce(), nonce,
			)
		}

```

However, there is a following requirement:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_increment_sender_seq.go#L28-30
```
// AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a
// contract creation, the nonce will be incremented during the transaction execution and not within
// this AnteHandler decorator.
```

There is currently no check for whether the operation is a contract creation (`to` == `nil`) or not.

## Recommended mitigation steps

Add a check for `to` == `nil` situation to indicate that operation is a contract creation and the nonce behavior has to be handled differently.
Coinbase fees are not transferred to the coinbase address deviating from the standard execution client spec
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L249


# Vulnerability details

## Finding description and impact

At the moment `ApplyEvmMsg()` does not account for coinbase fees breaking compatibility with the standard EVM execution client. There is always a priority fee parameter that reflects how much fees has to be paid to the validator of the block but it's currently missing.

## Proof of Concept

Take a look at the geth client implementation:

https://github.com/NibiruChain/go-ethereum/blob/nibiru/geth/core/state_transition.go#L351-359
```
if st.evm.Config.NoBaseFee && st.gasFeeCap.Sign() == 0 && st.gasTipCap.Sign() == 0 {
		// Skip fee payment when NoBaseFee is set and the fee fields
		// are 0. This avoids a negative effectiveTip being applied to
		// the coinbase when simulating calls.
	} else {
		fee := new(big.Int).SetUint64(st.gasUsed())
		fee.Mul(fee, effectiveTip)
		st.state.AddBalance(st.evm.Context.Coinbase, fee)
	}

```

When sending a transaction, the Nibiru `backend` module includes `MaxPriorityFeePerGas` parameter even if it's not set by a user:

https://github.com/code-423n4/2024-11-nibiru/blob/main/eth/rpc/backend/call_tx.go#L103-109
```
if args.MaxPriorityFeePerGas == nil {
				tip, err := b.SuggestGasTipCap(head.BaseFee)
				if err != nil {
					return args, err
				}
				args.MaxPriorityFeePerGas = (*hexutil.Big)(tip)
			}
```


But when applying a message, there is no transfer to the coinbase address at all:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L249

The function only calculates the gas and subtracts all the gas costs from the gas limit but there is no any transfer of priority fee to the coinbase address.

## Recommended mitigation steps

Add coinbase address transfer similar to the vanilla geth client implementation.
Incompatibility in sendToBank Function with Tokens Lacking Transfer Response
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://vscode.dev/github/code-423n4/2024-11-nibiru/blob/main/x/evm/precompile/funtoken.go#L109-L181


# Vulnerability details

## Description 
The `sendToBank` function in `funtoken.go` assumes that the Transfer method of an ERC20 token will return a non-nil MsgEthereumTxResponse. However, tokens like USDT (and potentially other non-standard ERC20 implementations) do not return a response in the standard format or omit it entirely. This discrepancy can cause runtime errors, such as nil pointer dereference, or lead to incomplete transaction handling for these tokens.
## Impact
Transaction Failures: Calls to sendToBank involving non-standard tokens like USDT fail due to the missing response.


## Recommended mitigation steps
Add checks to identify tokens with non-standard behavior (e.g., USDT) and adapt the handling logic accordingly.
EVM transactions bundled in a single SDK transaction will not receive gas refunds
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/msg_server.go#L86-L88


# Vulnerability details

## Finding description and impact
When executing an **EVM** transaction message in `EthereumTx()` the sender is only refunded gas if their current **EVM** transaction's `GasLimit` is greater than all the gas used by **EVM** messages executed until now within the currently processed **SDK** transaction. This, however, is wrong because when a user bundles multiple **EVM** transactions in a single **SDK** transaction, `blockGasUsed` will be incremented by the actual gas consumed by every single transaction and the last **EVM** transactions in the bundle are at an increasing risk of **not** receiving a gas refund.

The gas in Nibiru is handled in the following manner (overly simplified):
1. User submits **EVM** transaction messages via a **SDK** transaction.
2. The ante handler deducts in advance the gas fees in `unibi` coins from the sender's bank coin balance.
3. **EVM** transactions are processed via `EthereumTx()`.
4. User is refunded the leftover gas that wasn't used at the same price they were charged by the ante handler.

The conditions under which the user is refunded their unused gas are wrong and will result in users **NOT** receiving gas refunds a lot of the times when they submit multiple **EVM** transactions in a single **SDK** transaction.

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/msg_server.go#L31-L92
```go
func (k *Keeper) EthereumTx(
	goCtx context.Context, txMsg *evm.MsgEthereumTx,
) (evmResp *evm.MsgEthereumTxResponse, err error) {
  // ...

	evmResp, _, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig, false)

  // ...

	blockGasUsed, err := k.AddToBlockGasUsed(ctx, evmResp.GasUsed)

	// refund gas in order to match the Ethereum gas consumption instead of the
	// default SDK one.
	refundGas := uint64(0)
	if evmMsg.Gas() > blockGasUsed {
		refundGas = evmMsg.Gas() - blockGasUsed
	}
	weiPerGas := txMsg.EffectiveGasPriceWeiPerGas(evmConfig.BaseFeeWei)
	if err = k.RefundGas(ctx, evmMsg.From(), refundGas, weiPerGas); err != nil {
		return nil, errors.Wrapf(err, "EthereumTx: error refunding leftover gas to sender %s", evmMsg.From())
	}
```

Here we see that the **EVM** transaction's `GasLimit` is checked if greater than `blockGasUsed`. `blockGasUsed` is set to 0 by the ante handler each time a new **SDK** transaction is executed. We can see in this code snippet that every `MsgEthereumTx` message will increment it by the gas that the currently processed **EVM** transaction actually consumed, so with each processed **EVM** transaction `blockGasUsed` will grow. By the time the second, or third **EVM** transaction message in a single **SDK** transaction is executed, this value will already be big enough so that it makes the if-clause evaluate to `false` every time.

On the other side, `evmResp.