xanoutas/solidity-security-findings huggingface dataset:
Race Condition in AddToBlockGasUsed Function Leading to Potential Inconsistent Gas Tracking
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://vscode.dev/github/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/msg_server.go#L31-L32


# Vulnerability details

## description
The function EthereumTx of msg_server.go is susceptible to a race condition when multiple transactions are processed concurrently. Since this function updates the cumulative gas used in a block, concurrent transactions may read and modify the same value simultaneously, leading to inconsistent or incorrect gas usage tracking. This could result in invalid gas calculations and potentially allow blocks to exceed the gas limit or cause transaction failures.
## impact
The race condition may cause the cumulative gas used to be calculated incorrectly, leading to block gas limits being violated.
## Proof of Concept
1. Simulate multiple transactions being processed concurrently by creating parallel goroutines that invoke the EthereumTx method.
2. Within each goroutine, execute a transaction that modifies the block's gas usage via the AddToBlockGasUsed function.
3. Observe the block gas usage via logging or debugging:
Inconsistent results in cumulative gas used indicate the presence of a race condition.
Use the Go race detector (go test -race) to confirm the issue.
## Recommended mitigation steps
Protect the gas usage update logic with a mutex to ensure only one goroutine updates the value at a time.
Nonce can be manipulated by inserting a contract creation EthereumTx message first in an SDK TX with multiple EthereumTX messages
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#L330


# Vulnerability details

## Finding description and impact
The Ante handler for `MsgEthereumTx` transactions is responsible for ensuring messages are coming with correct nonces. After doing so, it'll increment the account's sequences for each message that the account has signed and broadcasted.

The problem is that when the `EthereumTx()` message server method calls `ApplyEvmMsg()` it'll override the account nonce when the currently processed EVM transaction message is a contract creation and will set it to the nonce of the message. When a non-contract creation EVM transaction message is processed, however, the `ApplyEvmMsg()` method does **not** touch the account's nonce.

This opens up an exploit window where a malicious user can replay a TX multiple times and reuse their nonces. Users can manipulate their Sequence (nonce) by submitting a contract creation EVM transaction message and multiple call/transfer EVM transaction messages in a single SDK transaction.

The code relies on `evmObj.Call()` and later `stateDB.commitCtx()` to persist a correct nonce in state but the `Call()` method on `evmObj` does **not** handle account nonces, it just executes the transaction. As we can see the method in `geth` that's normally used to transition the state increments the sender's nonce by 1 in either case:

https://github.com/NibiruChain/go-ethereum/blob/nibiru/geth/core/state_transition.go#L331-L337
```go
	if contractCreation {
		ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value)
	} else {
		// Increment the nonce for the next transaction
		st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
		ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value)
	}
```
https://github.com/NibiruChain/go-ethereum/blob/nibiru/geth/core/vm/evm.go#L498-L501
```go
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
	contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
	return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
}
```
https://github.com/NibiruChain/go-ethereum/blob/7fb652f186b09b81cce9977408e1aff744f4e3ef/core/vm/evm.go#L405-L418
```go
func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) {
	// Depth check execution. Fail if we're trying to execute above the
	// limit.
	if evm.depth > int(params.CallCreateDepth) {
		return nil, common.Address{}, gas, ErrDepth
	}
	if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
		return nil, common.Address{}, gas, ErrInsufficientBalance
	}
	nonce := evm.StateDB.GetNonce(caller.Address())
	if nonce+1 < nonce {
		return nil, common.Address{}, gas, ErrNonceUint
Multiple FunTokens can be created for a single ERC20 address
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#L108-L154


# Vulnerability details

## Finding description and impact
When a new FunToken is to be created for an existing ERC20 token, it's first checked if there's an existing FunToken mapping for the provided ERC20 address:

```go
func (k *Keeper) createFunTokenFromERC20(
	ctx sdk.Context, erc20 gethcommon.Address,
) (funtoken *evm.FunToken, err error) {
	// 1 | ERC20 already registered with FunToken?
	if funtokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.ERC20Addr.ExactMatch(ctx, erc20)); len(funtokens) > 0 {
		return funtoken, fmt.Errorf("funtoken mapping already created for ERC20 \"%s\"", erc20)
	}

        // ...
```

If there isn't one, the code proceeds to query the ERC20 token contract on the EVM for its name, symbol and decimals and generates a metadata struct for the to-be-created bank coin. After that the bank coin's metadata is saved against the provided ERC20 address which is used as a store key as is.

```go
        // ...
  
	return funtoken, k.FunTokens.SafeInsert(
		ctx, erc20, bankDenom, false,
	)
}
```

The little detail here is that `erc20` can be the valid address of a single ERC20 token for which a FunToken mapping does not exist yet in the application in quite a few cases:
- `0x000AAA111BBB...`
- `0x000aaa111bbb...`
- `0x000aAa111bbb...`
- `0x000Aaa111bbb...`
and so on, rotating which letters we capitalize... Quite a few combinations possible for a 20 byte address.

Which breaks the 5th main invariant listed in the contest readme.
> **Main invariants**
> 5. Similar to (3), any ERC20 on Nibiru can be used to create a <u>canonical</u> bank coin representation. The owner of the ERC20 is unbounded, while only the EVM Module account can mint the bank coin representation produced.
## Proof of Concept
1. Deploy a single ERC20 token on Nibiru's EVM.
2. Take the address it was deployed at and call `CreateFunToken()` with it as many variations of the hex representation as possible.
3. Now we have >1 FunToken mapping for a single ERC20 token contract.
## Recommended mitigation steps
When checking if a FunToken exists for the provided `erc20` address, lowercase it and then use it for checking of FunToken existence or using it as a key in the store.
Nibiru's bank coin to EVM balance tracking logic is completely broken for rebasing tokens and would lead to leakage/loss of funds when converting
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#L567-L624
https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/README.md#L129-L137


# Vulnerability details


## Summary

The Nibiru EVM module's token conversion mechanism contains a critical vulnerability when handling rebasing tokens. The issue stems from an incorrect assumption about the 1:1 relationship between escrowed ERC20 tokens and their bank coin representations, which can be violated when token balances change outside of transfers (e.g., through rebasing) and these type of tokens are supported by Nibiru

## Vulnerability Details

### Context

The Nibiru EVM module supports converting ERC20 tokens to bank coins and vice versa. When converting from ERC20 to bank coins, the tokens are escrowed in the EVM module, and when converting back, these escrowed tokens are used to fulfill the conversion.

This logic can be seen in the `ConvertCoinToEvm`, `convertCoinToEvmBornERC20` and `convertEvmToCoin` functions in the `msg_server.go` file, see [x/evm/keeper/msg_server.go#L486-L561](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/msg_server.go#L486-L624).

### Problem

The conversion mechanism assumes a static 1:1 relationship between escrowed ERC20 tokens and bank coins, as evidenced in the `convertCoinToEvmBornERC20` function that is used when [converting the bank coins back to its ERC20 representation](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/msg_server.go#L510-L513):

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

```go
func (k Keeper) convertCoinToEvmBornERC20(
	ctx sdk.Context,
	sender sdk.AccAddress,
	recipient gethcommon.Address,
	coin sdk.Coin,
	funTokenMapping evm.FunToken,
) (*evm.MsgConvertCoinToEvmResponse, error) {
	erc20Addr := funTokenMapping.Erc20Addr.Address
	// 1 | Caller transfers Bank Coins to be converted to ERC20 tokens.
	if err := k.Bank.SendCoinsFromAccountToModule(
		ctx,
		sender,
		evm.ModuleName,
		sdk.NewCoins(coin),
	); err != nil {
		return nil, errors.Wrap(err, "error sending Bank Coins to the EVM")
	}

	// 2 | EVM sends ERC20 tokens to the "to" account.
	// This should never fail due to the EVM account lacking ERc20 fund because
	// the an account must have sent the EVM module ERC20 tokens in the mapping
	// in order to create the coins originally.
	//
	// Said another way, if an asset is created as an ERC20 and some amount is
	// converted to its Bank Coin representation, a balance of the ERC20 is left
	// inside the EVM module account in order to convert the coins back to
	// ERC20s.
	actualSentAmount, _, err := k.ERC20().Transfer(
		erc20Addr,
		evm.EVM_MODULE_ADDRESS,
		recipient,
		coin.Amount.BigInt(),
		ctx,
	)
	if err != nil {
		ret
Tokens that are upgradeable would be linked to multiple bank denoms
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/funtoken_from_erc20.go#L108-L154


# Vulnerability details


## Proof of Concept

First from the readMe we can see that Nibiru is compatible with multiple ERC20 tokens and even upgradeable tokens, see:
https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/README.md#L130-L137

```markdown
### ERC20 token behaviors in scope

| Question                                                                                   | Answer   |
| ------------------------------------------------------------------------------------------ | -------- |
| [Upgradeability](https://github.com/d-xo/weird-erc20?tab=readme-ov-file#upgradable-tokens) | In scope |
```

Issue however is that there are some subtle invariants that an ERC20 tokens should ensure is being followed when integrating with Nibiru, i.e:

- The ERC20 token must not be already registered as a FunToken.
- The corresponding bank coin denom is not already registered.

After which the bank coin denom metadata is set in the state, i.e created and inserted into the new FunToken mapping, all these are implemented as checks in the `createFunTokenFromERC20` function.

Take a look at https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/funtoken_from_erc20.go#L108-L154

```go
func (k *Keeper) createFunTokenFromERC20(
	ctx sdk.Context, erc20 gethcommon.Address,
) (funtoken *evm.FunToken, err error) {
	// 1 | ERC20 already registered with FunToken?
	if funtokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.ERC20Addr.ExactMatch(ctx, erc20)); len(funtokens) > 0 {
		return funtoken, fmt.Errorf("funtoken mapping already created for ERC20 \"%s\"", erc20)
	}

	// 2 | Get existing ERC20 metadata
	erc20Info, err := k.FindERC20Metadata(ctx, erc20)
	if err != nil {
		return funtoken, err
	}

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

	// 3 | Coin already registered with FunToken?
	_, isFound := k.Bank.GetDenomMetaData(ctx, bankDenom)
	if isFound {
		return funtoken, fmt.Errorf("bank coin denom already registered with denom \"%s\"", bankDenom)
	}
	if funtokens := k.FunTokens.Collect(ctx, k.FunTokens.Indexes.BankDenom.ExactMatch(ctx, bankDenom)); len(funtokens) > 0 {
		return funtoken, fmt.Errorf("funtoken mapping already created for bank denom \"%s\"", bankDenom)
	}

	// 4 | Set bank coin denom metadata in state
	bankMetadata := erc20Info.ToBankMetadata(bankDenom, erc20)

	err = bankMetadata.Validate()
	if err != nil {
		return funtoken, fmt.Errorf("failed to validate bank metadata: %w", err)
	}
	k.Bank.SetDenomMetaData(ctx, bankMetadata)

	// 5 | Officially create the funtoken mapping
	funtoken = &evm.FunToken{
		Erc20Addr: eth.EIP55Addr{
			Address: erc20,
		},
		BankDenom:      bankDenom,
		IsMadeFromCoin: false,
	}

	return funtoken, k.FunTokens.SafeInsert(
		ctx, erc20, bankDenom, false,
	)
}
```

Issue
Internal Transactions Bypass EIP-3529 Gas Refund Cap
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#L208-L399


# Vulnerability details


## Proof of Concept

Take a look at [x/evm/keeper/msg_server.go#L208-L399](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/msg_server.go#L208-L399):

```go
// If msg comes from user, we apply refundQuotient capping the refund to 20% of used gas
// If msg is internal (funtoken), we refund 100%
refundQuotient := params.RefundQuotientEIP3529
if fullRefundLeftoverGas {
    refundQuotient = 1 // 100% refund
}
```

The EVM module allows internal transactions (like funtokens) to bypass EIP-3529's gas refund cap of 20%, permitting 100% gas refunds for these transactions. This violates EIP-3529 which explicitly requires all gas refunds to be capped at 1/5 (20%) of gas used, with no exceptions for internal transactions or special cases.

See [EIP-3529](https://eips.ethereum.org/EIPS/eip-3529):

> ...
> The capping of refunds to 1/5 of gas expended means that this refund can only be used to increase the amount of storage write operations needed to process a block by at most 25%, limiting the ability to use this mechanic for storage-write-focused denial-of-service attacks.

## Impact

From the readMe and code snippets protocol would like to be in compliance with EIP-3529 and also other EVM like chains, however this is not achieved and broken, which then means more refunds (_leakage of funds_) considering we now refund 100% instead of 20%.

1. **Gas Token Exploitation**: Internal transactions could be used to create new forms of gas tokens, which EIP-3529 was specifically designed to prevent.

2. **Block Size Variance**: The 100% refund allowance for internal transactions means blocks could experience high variance in gas usage that EIP-3529 aimed to fix.

3. **DoS Vector**: Could enable storage-write-focused denial-of-service attacks through internal transactions.

4. **Protocol Deviation**: Implementation deviates from Ethereum's specification, potentially leading to unexpected behaviors.

## Recommended Mitigation Steps

1. Remove the special case for internal transactions and always apply the EIP-3529 refund quotient:

```go
// Always use EIP-3529 refund quotient
refundQuotient := params.RefundQuotientEIP3529
// Remove the fullRefundLeftoverGas special case
```

2. If internal transactions require special handling, consider alternative mechanisms that don't bypass core EIP-3529 security measures.

3. Add explicit documentation if any deviations from EIP specifications are intentional, explaining the rationale and potential risks.

## QA-14 Missing London Fork Validation Could Lead to Incorrect Fee Processing

### Proof of Concept

In Nibiru's `app/evmante/evmante_validate_basic.go`, the transaction validation lacks proper London hardfork checks:

```go
// Nibiru's implementation
baseFeeMicronibi := vbd.evmKeeper.BaseFeeMicronibiPe
Nibiru-specific precompiled contracts are not added to access list
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/statedb/statedb.go#L420


# Vulnerability details

## Finding description and impact

EIP-2929 is not fully implemented for Nibiru-specific precompiled contracts.

## Proof of Concept

According to EIP-2929, precompiles should be included in the access list when a transaction starts executing. This ensures that the precompile addresses are "warmed," which results in lower gas costs when querying data (e.g., calling them, checking their balance, retrieving their code hash, etc.).  
For more details, refer to the specification: [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929#specification).

On Nibiru, while precompiles are added to the access list, the Nibiru-specific precompiled contracts (addresses 800 to 802) are not.  
You can find more information here:  
- [Code reference](https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/statedb/statedb.go#L420)  
- [Nibiru precompiles documentation](https://nibiru.fi/docs/evm/precompiles/nibiru.html)

```go
func (s *StateDB) PrepareAccessList(
	sender common.Address,
	dst *common.Address,
	precompiles []common.Address,
	list gethcore.AccessList,
) {
	s.AddAddressToAccessList(sender)
	if dst != nil {
		s.AddAddressToAccessList(*dst)
		// If it's a create-tx, the destination will be added inside evm.create
	}
	for _, addr := range precompiles {
		s.AddAddressToAccessList(addr)
	}
	for _, el := range list {
		s.AddAddressToAccessList(el.Address)
		for _, key := range el.StorageKeys {
			s.AddSlotToAccessList(el.Address, key)
		}
	}
}
```

Running a simple test reveals that retrieving the code hash of the coinbase address consumes only 115 gas, while retrieving the code hash of the precompile address (0x801) consumes 2615 gas. This indicates that Nibiru-specific precompiles are not included in the access list.


```javascript
import { describe, expect, it } from '@jest/globals';
import { parseEther, toBigInt } from 'ethers';

const hre = require("hardhat");

describe('My tests', () => {

  it('testAccessList', async () => {
    let Contract = await hre.ethers.getContractFactory("TestAccessList");
    let contract = await Contract.deploy();
    await contract.waitForDeployment();


    let tx1 = await contract.test_Coinbase();
    await tx1.wait();
    console.log("consumedGas for coinbase: ", await contract.consumedGas());

    let tx2 = await contract.test_Precompile();
    await tx2.wait();
    console.log("consumedGas for precompile: ", await contract.consumedGas());
  })
})
```

```solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;


contract TestAccessList {

  uint256 public consumedGas;

  function test_Coinbase() public returns (bytes32 h) {
    uint256 consumedGasTemp;
    address addr = block.coinbase;
    consumedGasTemp = gasleft();
    assembly {
      h := extcodehash(addr)
    }
    consumedGas = consumedGasTemp - gasleft();
  }

  function test_Precompile() public returns (bytes32 h) {
The `bankBalance` function failed to handle errors correctly.
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#L378


# Vulnerability details

## Finding description and impact


The `bankBalance` function does not handle errors after decoding the call parameters.
As a result, `p.evmKeeper.Bank.GetBalance` may throw a panic, and this erroneous panic cannot be recovered by `HandleOutOfGasPanic`, leading to the erroneous panic being propagated further up the program.

## Proof of Concept
github:[https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L378](https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/precompile/funtoken.go#L378)
```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(),
		},
	}...)
}
```
It can be observed that even if `parseArgsBankBalance` returns an error during decoding, the program will still proceed to call `p.evmKeeper.Bank.GetBalance` using incorrect data.
```go
// GetBalance returns the balance of a specific denomination for a given account
// by address.
func (k BaseViewKeeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin {
	accountStore := k.getAccountStore(ctx, addr)
	bz := accountStore.Get([]byte(denom))
	balance, err := UnmarshalBalanceCompat(k.cdc, bz, denom)
	if err != nil {
		panic(err)
	}

	return balance
}
```
This is likely to cause `GetBalance` to throw a panic. This panic is unexpected and, therefore, cannot be caught by `HandleOutOfGasPanic`, resulting in the program further throwing an exception.
```go
func HandleOutOfGasPanic(err *error) func() {
	return func() {
		if r := recover(); r != nil {
			switch r.(type) {
			case sdk.ErrorOutOfGas:
				*err = vm.ErrOutOfGas
			default:
				panic(r)
			}
		}
	}
}
```
## Recommended mitigation steps

```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 {
+		err = ErrInvalidArg