xanoutas/solidity-security-findings huggingface dataset:
Nibiru is incompatible with rebasing ERC20 tokens
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#L109


# Vulnerability details

## Finding description and impact
The Nibiru chain is designed to handle FOT tokens properly but is incompatible with ERC20 tokens where balances can change outside of transfers (also known as rebasing tokens). The problem arises from the fact that Nibiru doesn't keep any internal accounting for the ERC20 tokens for which FunTokens are created.

This allows for a situation where users deposit a rebasing ERC20 token in exchange for `x/bank` coins (through a FunToken mapping) and later only being able to withdraw their initial deposit in case the underlying ERC20 token rebases up or incur losses for other depositors in case the token rebases down.
## Proof of Concept
Suppose user A and B convert each $1\ 000$ `token`s to a bank coin via `sendToBank()` on the `FunToken` precompile contract.
1. Each one of them transfer $1\ 000$ `token` to the `x/evm` ETH account.
2. `x/evm` ETH account now holds $2\ 000$ `token` ERC20 tokens.
2. Each user gets minted $1\ 000$ `erc20/token` bank coins. Summed up, they have $2\ 000$ `erc20/token` bank tokens.
3. Some time passes and the `token` ERC20 token shrinks by 10%.
4. The `x/evm` module ETH account now owns only $1\ 900$ `token` ERC20 tokens.
5. User A calls `ConvertCoinToEvm()` to convert back their $1\ 000$ `erc20/token` bank tokens into the `token` ERC20 token.
6. User B calls `ConvertCoinToEvm()` to also convert back their $1\ 000$ `erc20/token` bank tokens into the `token` ERC20 token, but their request fails, as the `x/evm` module ETH account only has a balance of $900$ `token` ERC20 tokens.
7. User B carries the entire $100$ `token`s loss instead of it being split 50/50 between the 2 users.
## Recommended mitigation steps
Implementing an internal accounting to support rebasing tokens properly.
erc20Calls.Transfer is not compatible with missing return value tokens
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Finding description and impact

Certain ERC20 tokens, such as [USDT](https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code), deviate from the ERC20 standard by not returning a value when the `transfer` function is called. This behavior is problematic for `erc20Calls.Transfer`, as the current implementation does not handle tokens with missing return values.

The following code snippet highlights the issue:

```go
resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)
if err != nil {
    return balanceIncrease, nil, err
}

var erc20Bool ERC20Bool
err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
if err != nil {
    return balanceIncrease, nil, err
}
```

### Key Issue:
- **Call Failure**: When a token that does not return a value is used (e.g., USDT), `CallContract` may not return an error (`err` is `nil`), but the `resp.Ret` field will contain an empty byte slice (`[]byte{}`).
- **Unpacking Error**: Attempting to unpack an empty byte slice using `UnpackIntoInterface` results in an error:
    * Error Message: `"abi: attempting to unmarshall an empty string while arguments are expected"`.
- **Impact**: This makes it impossible to use tokens with missing return values as FunTokens, potentially blocking their functionality in the system.


## Proof of Concept
The following test case demonstrates the issue and can be added to [erc20_test.go](https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/keeper/erc20_test.go#L69)

```go
s.T().Log("Error on empty unpack")
{
    erc20 := deps.EvmKeeper.ERC20()
    var erc20Bool struct{ Value bool }
    err := erc20.ABI.UnpackIntoInterface(&erc20Bool, "transfer", []byte{})
    if err != nil {
        s.T().Log("Error", err)
    }
    s.T().Log("Return", erc20Bool.Value)
}
```

### Result
Running the test yields the following output:

```sh
erc20_test.go:71: Error on empty unpack
erc20_test.go:77: Error abi: attempting to unmarshall an empty string while arguments are expected
erc20_test.go:79: Return false
```

This confirms the unpacking process fails when resp.Ret is an empty byte slice.


## Recommended mitigation steps

To address this issue, add a check to verify whether resp.Ret is empty before attempting to unpack. If it is empty, assume a successful transfer for tokens that do not return a value. For example:

```go
resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)
if err != nil {
    return balanceIncrease, nil, err
}

var erc20Bool ERC20Bool

// Check for empty response
if len(resp.Ret) == 0 {
    // Assume success for tokens that do not return a value
    erc20Bool.Value = true
} else {
    err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
    if err != nil {
        return balanceIncrease, nil, err
    }
}
FunToken is not compatible with tokens that change balance outside of transfers
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-L610
https://github.com/code-423n4/2024-11-nibiru/blob/main/x/evm/precompile/funtoken.go#L148-L174


# Vulnerability details

FunToken is not compatible with tokens that change balance outside of transfers

## Finding Description and Impact

Certain tokens, such as [Ampleforth (AMPL)](https://etherscan.io/token/0xd46ba6d942050d489dbd938a2c909a5d5039a161), utilize mechanisms that adjust token balances outside of traditional `transfer` calls. These mechanisms, often referred to as "rebasing," can increase or decrease token balances in holders' accounts dynamically.

### **Impact**
1. **Accounting Errors**: FunToken assumes token balances remain constant unless explicitly altered through transfers. Rebasing tokens violate this assumption, causing discrepancies between FunToken's internal accounting and the actual token balances.
2. **Potential Fund Loss**: Users might experience scenarios where:
   - Some users redeem more than their fair share of tokens, leading to **overcompensation**.
   - Other users lose funds due to **insufficient remaining balances** in the FunToken contract.
3. **Ecosystem Incompatibility**: FunToken cannot reliably interact with rebasing tokens, reducing its utility for such tokens and affecting user trust.


## Proof of Concept

The following scenario demonstrates the issue with rebasing tokens like AMPL:

1. Alice and Bob each convert **50 AMPL** into FunToken, resulting in the FunToken contract holding a total of **100 AMPL**.
2. A rebasing event occurs, reducing the total supply of AMPL. The FunToken contract's balance now reflects **80 AMPL** instead of 100.
3. Alice decides to redeem her FunToken for **50 AMPL**. After this redemption:
   - The FunToken contract balance becomes **30 AMPL** (80 - 50).
4. Bob attempts to redeem his **50 FunToken**, but only **30 AMPL** remains in the FunToken contract. Bob cannot fully redeem his FunToken, leading to a loss of **20 AMPL**.

This discrepancy arises because FunToken does not account for balance changes resulting from rebasing events.


## Recommended Mitigation Steps

To ensure compatibility with rebasing tokens, you can calculate the redemption amount as a proportion of the EVM module current token balance.
USDT and similar tokens that do not return boolean values would not work
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#L147-L149
https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/erc20.go#L66-L75


# Vulnerability details


## Proof of Concept

First note that from the readMe, Nibiru intends to be compatible with any ERC20, see:
https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/README.md#L119-L124

```markdown
### General questions

| Question                   | Answer                    |
| -------------------------- | ------------------------- |
| ERC20 used by the protocol | Any (all possible ERC20s) |
```

Also see:

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/README.md#L130-L134

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

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

Issue however is that there is an enforcal during actions like transfers that require the `success` value to be `true` to be executed, see:

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/erc20.go#L76-L122

```go
func (e erc20Calls) Transfer(
	contract, from, to gethcommon.Address, amount *big.Int,
	ctx sdk.Context,
) (balanceIncrease *big.Int, resp *evm.MsgEthereumTxResponse, err error) {
	recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx)
	if err != nil {
		return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance")
	}

	resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)
	if err != nil {
		return balanceIncrease, nil, err
	}

	var erc20Bool ERC20Bool
	err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
	if err != nil {
		return balanceIncrease, nil, err
	}

	// Handle the case of success=false: https://github.com/NibiruChain/nibiru/issues/2080
	//@audit trying to use the success value of transfers here would fail for tokens like `USDT`.
	success := erc20Bool.Value
	if !success {
		return balanceIncrease, nil, fmt.Errorf("transfer executed but returned success=false")
	}

	recipientBalanceAfter, err := e.BalanceOf(contract, to, ctx)
	if err != nil {
		return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance")
	}

	balanceIncrease = new(big.Int).Sub(recipientBalanceAfter, recipientBalanceBefore)

	// For flexibility with fee on transfer tokens and other types of deductions,
	// we cannot assume that the amount received by the recipient is equal to
	// the call "amount". Instead, verify that the recipient got tokens and
	//
USDT Transfer Issue (and similar tokens)
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/8ed91a036f664b421182e183f19f6cef1a4e28ea/x/evm/keeper/erc20.go#L90-L94


# Vulnerability details

## Finding description and impact

The `Transfer` function in `erc20.go` encounters a significant point when handling USDT and similar tokens that do not return a boolean value upon transfer. This non-standard behavior breaks the ABI unpacking process, leading to  error in the function. As a result, even though the transfer is successfully executed on the EVM side, the function returns an error, causing the Cosmos SDK to perceive the transaction as failed.


## Proof of Concept

Here is the transfer function:
```go
Contract: erc20.go

 76: func (e erc20Calls) Transfer(
 77: 	contract, from, to gethcommon.Address, amount *big.Int, 
 78: 	ctx sdk.Context,
 79: ) (balanceIncrease *big.Int, resp *evm.MsgEthereumTxResponse, err error) {
 80: 	recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx)
 81: 	if err != nil {
 82: 		return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance")
 83: 	}
 84: 
 85: 	resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)
 86: 	if err != nil {
 87: 		return balanceIncrease, nil, err
 88: 	}
 89: 
 90: 	var erc20Bool ERC20Bool
 91: 	err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
 92: 	if err != nil {
 93: 		return balanceIncrease, nil, err
 94: 	}
 95: 
 96: 	// Handle the case of success=false: https://github.com/NibiruChain/nibiru/issues/2080
 97: 	success := erc20Bool.Value 
 98: 	if !success {
 99: 		return balanceIncrease, nil, fmt.Errorf("transfer executed but returned success=false")
100: 	}
101: 
102: 	recipientBalanceAfter, err := e.BalanceOf(contract, to, ctx)
103: 	if err != nil {
104: 		return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance")
105: 	}
106: 
107: 	balanceIncrease = new(big.Int).Sub(recipientBalanceAfter, recipientBalanceBefore)
108: 
109: 	// For flexibility with fee on transfer tokens and other types of deductions,
110: 	// we cannot assume that the amount received by the recipient is equal to
111: 	// the call "amount". Instead, verify that the recipient got tokens and
112: 	// return the amount.
113: 	if balanceIncrease.Sign() <= 0 {
114: 		return balanceIncrease, nil, fmt.Errorf(
115: 			"amount of ERC20 tokens received MUST be positive: the balance of recipient %s would've changed by %v for token %s",
116: 			to.Hex(), balanceIncrease.String(), contract.Hex(),
117: 		)
118: 	}
119: 
120: 	return balanceIncrease, resp, err
121: }
```

USDT, like some other tokens, does not conform to the standard ERC20 behavior of returning a boolean value upon a successful transfer. This deviation causes the ABI unpacking process to fail, resulting in an error being returned by the `Transfer` function. Consequently, the Cosmos SDK, which relies on this function's return value to update its state, perceives th
ERC20 Transfer Fails With Non-Compliant Tokens Missing Return Values
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/84054a4f00fdfefaa8e5849c53eb66851a762319/x/evm/keeper/erc20.go#L91


# Vulnerability details

## Finding description and impact

The `erc20.go` file assumes all ERC20 tokens return a boolean value from transfer operations, but some popular tokens like USDT, BNB, and OMG do not (https://github.com/d-xo/weird-erc20). These tokens are valid ERC20s but do not conform to the current standard of returning a boolean.

When attempting to transfer such tokens, `UnpackIntoInterface` will call `Unpack` which fails when there is no return data but a return value is expected in the ABI. This causes the transfer to fail with an error, preventing users from transferring otherwise valid tokens through the system.

As noted in the contest scope, handling of "Missing return values" is explicitly in scope for this audit.

## Proof of Concept

The issue occurs in [erc20.go:91-95](https://github.com/code-423n4/2024-11-nibiru/blob/84054a4f00fdfefaa8e5849c53eb66851a762319/x/evm/keeper/erc20.go#L91):

```go
var erc20Bool ERC20Bool
err = e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret)
if err != nil {
    return balanceIncrease, nil, err
}
```

The underlying `Unpack` function in the Ethereum ABI encoding will error when attempting to unpack empty return data if the ABI specifies a return value (`go-ethereum/accounts/abi/argument.go`):

```go
func (arguments Arguments) Unpack(data []byte) ([]interface{}, error) {
    if len(data) == 0 {
        if len(arguments.NonIndexed()) != 0 {
            return nil, errors.New("abi: attempting to unmarshall an empty string while arguments are expected")
        }
        return make([]interface{}, 0), nil
    }
    return arguments.UnpackValues(data)
}
```

This prevents transfers of tokens like USDT that don't return a value, even when the transfer itself succeeds.

## Recommended mitigation steps

Modify the transfer function to consider a transfer successful if:
1. The token returns true OR
2. The token returns no value and the transfer didn't revert

```go
func (e erc20Calls) Transfer(
    contract, from, to gethcommon.Address, amount *big.Int,
    ctx sdk.Context,
) (balanceIncrease *big.Int, resp *evm.MsgEthereumTxResponse, err error) {
    recipientBalanceBefore, err := e.BalanceOf(contract, to, ctx)
    if err != nil {
        return balanceIncrease, nil, errors.Wrap(err, "failed to retrieve recipient balance")
    }

    resp, err = e.CallContract(ctx, e.ABI, from, &contract, true, Erc20GasLimitExecute, "transfer", to, amount)
    if err != nil {
        return balanceIncrease, nil, err
    }

    // If there's return data, try to unpack it
    if len(resp.Ret) > 0 {
        var erc20Bool ERC20Bool
        if err := e.ABI.UnpackIntoInterface(&erc20Bool, "transfer", resp.Ret); err != nil {
            return balanceIncrease, nil, err
        }
        if !erc20Bool.Value {
            return balanceIncrease, nil, fmt.Errorf("transfer executed but
RPC implementation Deviates from Ethereum JSON-RPC Spec
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Finding description and impact

The implementation of `eth_getBalance` and other endpoints that accept blocks in Nibiru's RPC interface deviates from the official Ethereum JSON-RPC specification, which could cause compatibility issues with common Ethereum tooling and libraries.

For instance, consider the `GetBalance` method in `account_info.go`, which only accepts `BlockNumberOrHash` as the block parameter:

```go
func (b *Backend) GetBalance(
    address gethcommon.Address,
    blockNrOrHash rpc.BlockNumberOrHash, // Only accepts BlockNumberOrHash
) (*hexutil.Big, error)
```

However, according to the [Ethereum JSON-RPC specification](https://ethereum.github.io/execution-apis/api-documentation/), the block parameter should accept:
- Block numbers
- Block hashes
- Block tags ("earliest", "finalized", "safe", "latest", "pending")

This deviation from the spec could cause issues when using popular Ethereum development tools and libraries, which expect full compliance with the Ethereum JSON-RPC specification. These tools often use block tags like "latest" or "pending" to query balances, and the current implementation may reject these valid queries.

The impact is particularly concerning because:
1. It breaks compatibility with standard Ethereum tooling
2. It may cause integration issues for dApps trying to port to Nibiru
3. It creates inconsistent behavior compared to other EVM chains, harming developer experience

*Note on severity: Compatibility with common tooling was explicitly mentioned as an area of concern for the sponsor, because of this I consider this Medium.*

## Proof of Concept

The issue can be found in [`account_info.go`](https://github.com/code-423n4/2024-11-nibiru/blob/main/eth/rpc/backend/account_info.go#L170):

```go
func (b *Backend) GetBalance(
    address gethcommon.Address,
    blockNrOrHash rpc.BlockNumberOrHash,
) (*hexutil.Big, error)
```

Comparing this with the official Ethereum JSON-RPC spec (https://github.com/ethereum/execution-apis/blob/4140e528360fea53c34a766d86a000c6c039100e/src/eth/state.yaml#L11):

```yaml
- name: eth_getBalance
  params:
    - name: Address
      required: true
      schema:
        $ref: '#/components/schemas/address'
    - name: Block
      required: true
      schema:
        $ref: '#/components/schemas/BlockNumberOrTagOrHash'
```

And from `block.yaml`, we can see that `BlockNumberOrTagOrHash` should include block tags:

```yaml
BlockTag:
  title: Block tag
  type: string
  enum:
    - earliest
    - finalized
    - safe
    - latest
    - pending
```

## Recommended mitigation steps

1. Update the `GetBalance` method signature to accept `BlockNumberOrTagOrHash` instead of just `BlockNumberOrHash`:

```go
func (b *Backend) GetBalance(
    address gethcommon.Address,
    blockParam rpc.BlockNumberOrTagOrHash,
) (*hexutil.Big, error)
```

2. Implement prope
Double fee application breaks supply invariant for fee-on-transfer ERC20s
Audit: 2024-11-nibiru-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-11-nibiru/blob/84054a4f00fdfefaa8e5849c53eb66851a762319/x/evm/precompile/funtoken.go#L162-L170


# Vulnerability details

## Finding description and impact

The EVM module incorrectly handles fee-on-transfer tokens when converting bank coins back to ERC20s, resulting in unbacked bank coins remaining in circulation. This breaks the intended 1:1 supply tracking invariant between ERC20 tokens and their bank coin representations.

When converting from ERC20 to bank coins via `sendToBank`, the code correctly accounts for transfer fees by only minting bank coins equal to the amount actually received. However, when converting these bank coins back to ERC20s via `convertCoinToEvmBornERC20`, the code:
1. Takes in X bank coins from the user
2. Tries to transfer X ERC20 tokens
3. Due to fees, only Y tokens are received (Y < X)
4. Only burns Y bank coins

This creates a discrepancy since the original conversion already accounted for fees. The transfer fees are effectively applied twice:
1. First fee: 100 ERC20 -> 95 bank coins (correct)
2. Second fee: 95 bank coins -> ~90.25 ERC20 (and only burn 90.25 bank coins)
   
This leaves 4.75 unbacked bank coins in circulation (95 - 90.25), as the code only burns what was actually transferred in the second conversion.

The impact is monetary - it creates unbacked bank coins that can be used in the rest of the system but don't have corresponding ERC20 tokens backing them in the EVM module's account. Over time, this could lead to significant supply inflation of the bank coin representation.

## Proof of Concept

The first conversion correctly handles fees in `funtoken.go`:

```go
// First conversion correctly uses actual received amount
gotAmount, transferResp, err := p.evmKeeper.ERC20().Transfer(erc20, caller, transferTo, amount, ctx)
if err != nil {
    return nil, fmt.Errorf("error in ERC20.transfer from caller to EVM account: %w", err)
}
coinToSend := sdk.NewCoin(funtoken.BankDenom, math.NewIntFromBigInt(gotAmount))
```
[Link to code](https://github.com/code-423n4/2024-11-nibiru/blob/84054a4f00fdfefaa8e5849c53eb66851a762319/x/evm/precompile/funtoken.go#L162-L170)

But when converting back in `msg_server.go`, it incorrectly applies fees again:

```go
actualSentAmount, _, err := k.ERC20().Transfer(
    erc20Addr,
    evm.EVM_MODULE_ADDRESS,
    recipient,
    coin.Amount.BigInt(),
    ctx,
)
if err != nil {
    return nil, errors.Wrap(err, "failed to transfer ERC-20 tokens")
}

// Only burns the amount after fees, even though fees were already accounted for
burnCoin := sdk.NewCoin(coin.Denom, sdk.NewIntFromBigInt(actualSentAmount))
err = k.Bank.BurnCoins(ctx, evm.ModuleName, sdk.NewCoins(burnCoin))
```
[Link to code](https://github.com/code-423n4/2024-11-nibiru/blob/84054a4f00fdfefaa8e5849c53eb66851a762319/x/evm/keeper/msg_server.go#L597-L613)

The code explicitly aims to maintain a supply invariant:
```go
// to preserve an invariant on the sum of the FunToken's ban