xanoutas/solidity-security-findings huggingface dataset:
fullRefundLeftoverGas is always set to false as it's not checked if the message is the inner one
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Finding description and impact

At the moment `fullRefundLeftover` gas is created to handle the special behavior applied only to the FunToken related messages. This behavior differs from EIP3529 where refund quotient is capped at the value of 5. However, the `EthereumTx()` does not have any check on whether the message is related to the FunToken operation or not and always sets `fullRefundLeftover` value to 0. This creates a situation where the refunds are not handled properly for the inner operations.

## Proof of Concept

`EthereumTx()` function makes initial validation and then applies the message:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L65
```
evmResp, _, err = k.ApplyEvmMsg(tmpCtx, evmMsg, nil, true, evmConfig, txConfig, false)

```

The last argument here is exactly the `fullRefundLeftoverGas` and it's set to `false`. Inside of the `ApplyEvmMsg()`, the refunds are handled the following way:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L369-372
```
refundQuotient := params.RefundQuotientEIP3529
	if fullRefundLeftoverGas {
		refundQuotient = 1 // 100% refund
	}

```

As you can see here, the refund quotient is assigned based on the `fullRefundLeftoverGas` value.

As `EthereumTx()` is the only point where the state changes are committed (`grpc_query` only for simulation), it's supposed to have a validation on what type of message is passed through and apply the corresponding refund quotient value for this.

## Recommended mitigation steps

Change the logic so that it has some validation whether the messages that passed to the function is FunToken related message.
effectiveGasPriceDec does not depend on the user and uses hardcoded parameter leading to a potential tx revert
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Finding description and impact

Currently `evmante_mempool_fees` file checks for the `effectiveGasPriceDec` to be no lower than `minGasPrice`. The problem is that the value does not depend on the user input at all and instead uses hardcoded `baseFeeMicronibiDec`. This means that if the hardcoded value is lower than `minGasPrice`, the `AnteHandle()` will always return an error.

## Proof of Concept

Take a look at how `baseFeeMicronibiDec` value is assigned:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_mempool_fees.go#L42
```
baseFeeMicronibi := d.evmKeeper.BaseFeeMicronibiPerGas(ctx)
```

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_mempool_fees.go#L62-65
```
	baseFeeWei := evm.NativeToWei(baseFeeMicronibi)
		effectiveGasPriceDec := math.LegacyNewDecFromBigInt(
			evm.WeiToNative(ethTx.EffectiveGasPriceWeiPerGas(baseFeeWei)),
		)
```

As you can see here, `effectiveGasPriceDec` is derived from the `baseFeeWei`. This is incorrect as, according to the EIP-1559:

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md
```
effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas
```

But currently `effectiveGasPriceDec` only includes `baseFeeWei` that is hardcoded in the `keeper`:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/keeper.go#L118-124
```
// BaseFeeMicronibiPerGas returns the gas base fee in units of the EVM denom. Note
// that this function is currently constant/stateless.
func (k Keeper) BaseFeeMicronibiPerGas(_ sdk.Context) *big.Int {
	// TODO: (someday maybe):  Consider making base fee dynamic based on
	// congestion in the previous block.
	return evm.BASE_FEE_MICRONIBI
}
```

So if this value is less than `minGasPrice`, the function will return an error here:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_mempool_fees.go#L66-75
```
if effectiveGasPriceDec.LT(minGasPrice) {
			// if sdk.NewDecFromBigInt(effectiveGasPrice).LT(minGasPrice) {
			return ctx, errors.Wrapf(
				sdkerrors.ErrInsufficientFee,
				"provided gas price < minimum local gas price (%s < %s). "+
					"Please increase the priority tip (for EIP-1559 txs) or the gas prices "+
					"(for access list or legacy txs)",
				effectiveGasPriceDec, minGasPrice,
			)
		}

```

## Recommended mitigation steps

Change the logic so that `effectiveGasPrice` was formed from both priority tip + base fee and checked against `minGasPrice` and not only from hardcoded base fee value.
MaxPriorityFeePerGas and MaxFeePerGas are specified for a user which is not correct
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/eth/rpc/backend/call_tx.go#L104


# Vulnerability details

## Finding description and impact

The current implementation of the `SetTxDefaults()` sets a priority gas fee instead of a user which is not correct as the value is optional and can be bigger than the amount that a user wants to pay (the value specified can be 0 as well but in this scenario the tx will be likely to not be processed):

## Proof of concept

According to the EIP1559 spec:

```
# priority fee is capped because the base fee is filled first
			priority_fee_per_gas = min(transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas - block.base_fee_per_gas)
```

So the value is either `transaction.max_priority_fee_per_gas` or `transaction.max_fee_per_gas - block.base_fee_per_gas` but the current implementation of the NibiruVM just sets an arbitrary value that's suggested as a priority tip:


https://github.com/code-423n4/2024-11-nibiru/blob/main/eth/rpc/backend/call_tx.go#L100-109
```
if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil {
		// In this clause, user left some fields unspecified.
		if head.BaseFee != nil && args.GasPrice == nil {
			if args.MaxPriorityFeePerGas == nil {
				tip, err := b.SuggestGasTipCap(head.BaseFee)
				if err != nil {
					return args, err
				}
				args.MaxPriorityFeePerGas = (*hexutil.Big)(tip)
			}
```

The same thing is with `MaxFeePerGas` value that's set to the following:

https://github.com/code-423n4/2024-11-nibiru/blob/main/eth/rpc/backend/call_tx.go#L111-117
```
if args.MaxFeePerGas == nil {
				gasFeeCap := new(big.Int).Add(
					(*big.Int)(args.MaxPriorityFeePerGas),
					new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
				)
				args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap)
			}

```

It adds the `BaseFee` multiplied by 2 and adds `MaxPriorityFeePerGas` to the `gasFeeCap` making user to pay excessive fees as the only thing that's required to be covered by EIP1559 is the `BaseFee` and not `BaseFee` multiplied by 2. Moreover, `MaxFeePerGas` is a maximum base fee a user wants to pay but the function also mistakenly includes `MaxPriorityFeePerGas` here.

## Recommended mitigation steps

Change the logic so the fees are set according to the EIP1559 standard.
Only hardcoded base fee is deducted from the user's account which is not correct
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Finding description and impact

Currently only base fee is deducted as a fee from the user's account when executing a message on Ethereum. This leads to an incorrect implementation and deviation from the standard geth client.

## Proof of Concept

`evmante_gas_consume` accounts for the gas that's being consumed during the tx execution:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_gas_consume.go#L100-105
```
	fees, err := keeper.VerifyFee(
			txData,
			evm.EVMBankDenom,
			baseFeeMicronibiPerGas,
			ctx.IsCheckTx(),
		)
```

This is the implementation of the `VerifyFee()` in the `gas_fees` module inside of the `keeper`:

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/gas_fees.go#L193-200
```
	baseFeeWei := evm.NativeToWei(baseFeeMicronibi)
	feeAmtMicronibi := evm.WeiToNative(txData.EffectiveFeeWei(baseFeeWei))
	if feeAmtMicronibi.Sign() == 0 {
		// zero fee, no need to deduct
		return sdk.Coins{{Denom: denom, Amount: sdkmath.ZeroInt()}}, nil
	}

	return sdk.Coins{{Denom: denom, Amount: sdkmath.NewIntFromBigInt(feeAmtMicronibi)}}, nil


```

So it basically just derives the `baseFeeMicronibi` that's currently hardcoded and returns this amount in native denom (unibi). Then it's deducted from the sender's address:

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_gas_consume.go#L110-112
```
if err = anteDec.deductFee(ctx, fees, from); err != nil {
			return ctx, err
		}

```

https://github.com/code-423n4/2024-11-nibiru/blob/main/app/evmante/evmante_gas_consume.go#L162-175
```

func (anteDec AnteDecEthGasConsume) deductFee(
	ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress,
) error {
	if fees.IsZero() {
		return nil
	}

	if err := anteDec.evmKeeper.DeductTxCostsFromUserBalance(
		ctx, fees, gethcommon.BytesToAddress(feePayer),
	); err != nil {
		return errors.Wrapf(err, "failed to deduct transaction costs from user balance")
	}
	return nil
}

```

But that's not correct for EIP1559 transactions, for example, that also contain priority fee that's paid to the coinbase address. This creates a deviation from the standard EVM execution client implementation. Moreover, base fee has to derived from the block header and not to be hardcoded.

## Recommended mitigation steps

Deduct all the fees that are needed to be taken for EIP1559 transactions.
Fee-on-Transfer Tokens Incur Unexpected Double Fees During Round-Trip Conversions, Leading to Significant User Fund Loss
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Summary
When using fee-on-transfer tokens with Nibiru's FunToken bridge, users suffer a systematic loss of funds through unexpected double fees during round-trip conversions. The bridge incorrectly applies the fee-on-transfer mechanism during the conversion back to ERC20, when it should be a mint operation, leading to an additional 5% loss of user funds beyond the expected fee.

## Proof of Concept
The following test demonstrates the systematic loss of user funds when using fee-on-transfer tokens:


x/evm/embeds/contracts/TestERC20FeeOnTransfer.sol
```solidity
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TestERC20FeeOnTransfer is ERC20, ERC20Burnable, Ownable {
    uint8 private _decimals;
    uint256 private _feeBps; // Fee in basis points (500 = 5%)

    constructor(
        string memory name, 
        string memory symbol, 
        uint8 decimals_,
        uint256 feeBps_
    ) ERC20(name, symbol) {
        _setupDecimals(decimals_);
        _feeBps = feeBps_;
    }

    function _setupDecimals(uint8 decimals_) private {
        _decimals = decimals_;
    }

    function decimals() public view virtual override returns (uint8) {
        return _decimals;
    }

    function mint(address to, uint256 amount) public virtual onlyOwner {
        _mint(to, amount);
    }

    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");

        uint256 fee = (amount * _feeBps) / 10000;
        uint256 actualAmount = amount - fee;

        super._transfer(from, to, actualAmount);
        if (fee > 0) {
            // Burn the fee
            _burn(from, fee);
        }
    }

    function getFee(uint256 amount) public view returns (uint256) {
        return (amount * _feeBps) / 10000;
    }
}
```

Also, add this contract to embeds.go like other embedded contracts
```go
	// SmartContract_TestERC20FeeOnTransfer is a test contract that implements
	// a fee-on-transfer token to test FunToken behavior with tokens that modify
	// transfer amounts
	SmartContract_TestERC20FeeOnTransfer = CompiledEvmContract{
		Name:      "TestERC20FeeOnTransfer.sol",
		EmbedJSON: testErc20FeeOnTransferJson,
	}
    ...
    SmartContract_TestERC20FeeOnTransfer.MustLoad()
```

Then compile embedded contracts `npx hardhat compile`

Test for ERC20 Fee on Transfer Token
```go
package keeper_test

import (
	"math/big"
	"testing"

	"github.com/NibiruChain/nibir
Vesting Account Preemption Attack Preventing Future Contract Deployment
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

https://github.com/NibiruChain/nibiru/blob/f3cbcaec58f23c54f6b75204b0c4009856b47250/x/evm/keeper/statedb.go#L130-L134


# Vulnerability details

## Finding description and impact

This vulnerability allows an attacker to preemptively set a target address as a vesting account, permanently blocking contract deployments by Factory contracts or other users to that address. Once the address is marked as a vesting account, any deployment attempt stores the contract bytecode in the state without creating a codeHash, rendering the contract permanently inaccessible.

For example, an attacker could target critical ecosystem addresses, such as those planned for LayerZero or Uniswap, and preemptively mark them as vesting accounts. This would effectively “orphan” the contract bytecode at these addresses, with no way to interact with or access it. The severity is compounded if funds are deployed with the contract, as these would also be irretrievable.

If exploited, this vulnerability allows an attacker to lock up critical addresses by setting them as vesting accounts, resulting in “lost” contracts with unreachable bytecode and permanently inaccessible funds. For ecosystem-critical contracts or high-value deployments, this could disrupt functionality and lead to substantial, irreversible losses.

## Proof of Concept

The following commit demonstrates the vulnerability through a step-by-step exploit:

- Commit link: https://github.com/zsystm/nibiru-fork-for-audit/commit/9ea38e3e3a6bbc64bcbbd4271ca0f825d3a0259a (private repo)
- Description: Key aspects of this vulnerability are demonstrated in `TestVestingAccountPreemptionAttack`, which provides a proof of concept for the vulnerability in the form of a test scenario.

Please provide your GitHub handles, and I will grant access to the private repository.

For quick reference before access permissions are granted, I’ve included a core snippet of the reproduction code below.
```go
func (s *Suite) TestVestingAccountPreemptionAttack() {
	deps := evmtest.NewTestDeps()
	// Step-1: Set up the deterministic victim account
	privKeyE, _ := crypto.HexToECDSA("46e86cbf25a9aeb0630feebbb4ec22d6ee7acbdbde8b54d0382112c9b0cfe37c")
	privKey := &ethsecp256k1.PrivKey{
		Key: crypto.FromECDSA(privKeyE),
	}
	ethAddr := crypto.PubkeyToAddress(privKeyE.PublicKey)
	deps.Sender = evmtest.EthPrivKeyAcc{
		EthAddr:       ethAddr,
		NibiruAddr:    eth.EthAddrToNibiruAddr(ethAddr),
		PrivKey:       privKey,
		KeyringSigner: evmtest.NewSigner(privKey),
	}
	victim := deps.Sender
	fundedAmount := evm.NativeToWei(big.NewInt(100))
	fundedCoin := sdk.NewCoins(sdk.NewCoin("unibi", sdk.NewIntFromBigInt(fundedAmount)))
	s.Require().NoError(testapp.FundModuleAccount(deps.App.BankKeeper, deps.Ctx, authtypes.FeeCollectorName, fundedCoin))
	s.Require().NoError(testapp.FundAccount(deps.App.BankKeeper, deps.Ctx, victim.NibiruAddr, fundedCoin))
	// Step-2: Victim account deploys a Factory contract
	gasLimit := big.NewInt(3_000_000)
	initialF
The Iteration Over the Map in x/evm/keeper/precompiles.go is Non-Deterministic
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

In x/evm/keeper/precompiles.go, the function AddPrecompiles iterates over a map in a non-deterministic order.

```go
func (k *Keeper) AddPrecompiles(
	precompileMap map[gethcommon.Address]vm.PrecompiledContract,
) {
	if k.precompiles.Len() == 0 {
		k.precompiles = omap.SortedMap_EthAddress(
			precompileMap,
		)
	} else {
		for addr, precompile := range precompileMap {
			k.precompiles.Set(addr, precompile)
		}
	}
}
```

If k.precompiles already contains elements, the function iterates over each item in precompileMap (i.e., each address and its corresponding precompiled contract) and adds them to k.precompiles.

Since k.precompiles is a map, the iteration order over precompileMap is non-deterministic. This can lead to the following issues:

- **Inconsistent State**: If the storage of k.precompiles relies on the order of contract addresses, and the iteration order of precompileMap is non-deterministic, the order of contract addresses could vary each time AddPrecompiles is called. This could lead to inconsistent contract states or other critical data being stored.
- **Unpredictable Behavior**: If other logic depends on the order of contracts in k.precompiles (such as the priority or processing order of certain contract addresses), different nodes could produce different results due to the varying order. This could lead to consensus issues.
- **Future Risk**: Even small unrelated changes to the keeper may trigger this issue, as the order of processing could vary.

### Suggested Fix: Use Sorted Keys

To resolve this issue, a common approach is to avoid directly iterating over the map. Instead, the keys of the map should be extracted and sorted first, and then the iteration should occur in the sorted order. This ensures that the iteration order is consistent and prevents non-deterministic issues caused by varying iteration sequences.

The fix is as follows:

```go
func (k *Keeper) AddPrecompiles(
	precompileMap map[gethcommon.Address]vm.PrecompiledContract,
) {
	if k.precompiles.Len() == 0 {
		k.precompiles = omap.SortedMap_EthAddress(
			precompileMap,
		)
	} else {
		keys := make([]gethcommon.Address, 0, len(precompileMap))
		for addr := range precompileMap {
			keys = append(keys, addr)
		}
		sort.Slice(keys, func(i, j int) bool {
			return keys[i].String() < keys[j].String()
		})
		for _, addr := range keys {
			k.precompiles.Set(addr, precompileMap[addr])
		}
	}
}
```

### Additional Context

In a previous Zellic audit report, a similar issue was discovered and classified as a Critical vulnerability. You can view the report here: [Zellic Report on xoraclekeeper Iterating Over Maps](https://reports.zellic.io/publications/nibiru/findings/high-xoraclekeeper-iterating-over-maps-is-nondeterministic).

Given the similarity of this issue, I believe it should also be classified as a Critical vulnerability.
DOS or OOM Due to Unrestricted String Length in x/evm/precompile/wasm.go
Audit: 2024-11-nibiru-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/precompile/wasm.go#L68


# Vulnerability details

The `Run` method handles calls to various types of Wasm contract methods, selecting the correct execution logic based on the incoming contract method name. This ensures proper contract invocation through method name matching.

```go
// Run runs the precompiled contract
func (p precompileWasm) Run(
	evm *vm.EVM, contract *vm.Contract, readonly bool,
) (bz []byte, err error) {
	defer func() {
		err = ErrPrecompileRun(err, p)
	}()
	startResult, err := OnRunStart(evm, contract.Input, p.ABI(), contract.Gas)
	if err != nil {
		return nil, err
	}

	// Gracefully handles "out of gas"
	defer HandleOutOfGasPanic(&err)()

	// NOTE: The NibiruBankKeeper needs to reference the current [vm.StateDB] before
	// any operation that has the potential to use Bank send methods. This will
	// guarantee that [evmkeeper.Keeper.SetAccBalance] journal changes are
	// recorded if wei (NIBI) is transferred.
	p.Bank.StateDB = startResult.StateDB
	switch PrecompileMethod(startResult.Method.Name) {
	case WasmMethod_execute:
		bz, err = p.execute(startResult, contract.CallerAddress, readonly)
	case WasmMethod_query:
		bz, err = p.query(startResult, contract)
	case WasmMethod_instantiate:
		bz, err = p.instantiate(startResult, contract.CallerAddress, readonly)
	case WasmMethod_executeMulti:
		bz, err = p.executeMulti(startResult, contract.CallerAddress, readonly)
	case WasmMethod_queryRaw:
		bz, err = p.queryRaw(startResult, contract)
	default:
		// Note that this code path should be impossible to reach since
		// "[decomposeInput]" parses methods directly from the ABI.
		err = fmt.Errorf("invalid method called with name \"%s\"", startResult.Method.Name)
		return
	}
	if err != nil {
		return nil, err
	}

	// Gas consumed by a local gas meter
	// The reason it's unnecessary to check for a success value is because
	// GasConsumed is guaranteed to be less than the contract.Gas because the gas
	// meter was initialized....
	contract.UseGas(startResult.CacheCtx.GasMeter().GasConsumed())
	return bz, err
}
```

The issue occurs when `startResult.Method.Name` doesn’t match any predefined methods, triggering `fmt.Errorf` to print `startResult.Method.Name`. However, the `Run` function does not validate this parameter, such as checking its length. An attacker could set `method.Name` to an extremely large string, potentially causing a DOS (Denial of Service) or OOM (Out of Memory) issue.

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/precompile/wasm.go#L68

```go
err = fmt.Errorf("invalid method called with name \"%s\"",
startResult.Method.Name) // <-------
```

Similarly, without restricting the length of `method.Name`, the following branches could also trigger a DOS or OOM issue when they ultimately call `assertNotReadonlyTx`:

```go
	case WasmMethod_execute:
		bz, err = p.execute(startResult, contract.CallerAddress, readonly)
	case WasmMethod_inst