xanoutas/solidity-security-findings huggingface dataset:
Insufficient check on s-value can cause signature malleability
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/signature.cairo#L10-L35


# Vulnerability details

## Vulnerability Details:
The protocol relies on function [verify_eth_signature_uint256()](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/signature.cairo#L10-L35) to verify that the provided signature is valid, 
with help of starkware library it does checks that the value  of r and s is in the range [1,N) 
and that the limbs of value are in the range [0,Base).

However due to insufficient checks inside func verify_eth_signature_uint256() it allows signature malleability. 

Example:
If the library generates a signature such as s-values is in the upper range, we can calculate a new 
s-value with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 
and flip v from 27 to 28 or vise versa to accept this malleable signature as well.

## Impact:

When a signature is malleable, it means that another valid signature can be created for the same message.
An attacker could maipulate and intercepted signature to create another valid signature that the function would still verify as correct leading to unauthorized actions such as performing operations or access rights reserved to original signer.

## Recommended Mitigation:
Consider applying a check on s-value of the signature to ensure is it in valid range to avoid signature malleability.

if (s_bigint > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0)
{
	 return();
}



## Assessed type

Other
Missing Constraint in default_dict_copy
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/account.cairo#L83
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/dict.cairo#L83


# Vulnerability details

### Summary
In CairoZero, the correct usage of dict objects created via `default_dict_new` must be paired with a call to `default_dict_finalize` to ensure the integrity and prevent malicious prover's manipulation of its contents. However, this constraint is missing in the handling of `transient_storage`, `storage` and `valid_jumpdests`, leading to severe vulnerabilities when executing smart contracts.

### Description of the Issue  
According to CairoZero's documentation ([link to default_dict](https://docs.cairo-lang.org/reference/common_library.html#default-dict)), a proper workflow involving `default_dict_new` includes a finalization step using `default_dict_finalize`. This ensures the correct initialization of dictionary elements and prevents malicious provers from manipulating dictionary values through hints. Specifically, `default_dict_finalize` enforces the constraint that the initial value of the first element's `prev_value` in the dictionary must equal to the `default_value`.

However, in the case of `transient_storage`, `storage` and `valid_jumpdests`, this crucial constraint is missing. 

I will illustrate this issue using `transient_storage`. First, `transient_storage` is initialized in `Account.init()` as follows:

```rust
let (transient_storage_start) = default_dict_new(0);
```

However, there is no subsequent call to `default_dict_finalize(transient_storage_start, transient_storage, 0)` to finalize the storage. Instead, the function `default_dict_copy()` is called on `transient_storage` multiple times during a transaction through the `Account.copy()` function:

```rust
let (transient_storage_start, transient_storage) = default_dict_copy(
    self.transient_storage_start, self.transient_storage
);
```

This copy operation starts by calling `dict_squash` on the original `transient_storage`:

```rust
func default_dict_copy{range_check_ptr}(start: DictAccess*, end: DictAccess*) -> (
    DictAccess*, DictAccess*
) {
    alloc_locals;
    let (squashed_start, squashed_end) = dict_squash(start, end);
    local range_check_ptr = range_check_ptr;
    let dict_len = squashed_end - squashed_start;

    local default_value;
    if (dict_len == 0) {
        assert default_value = 0;
    } else {
        assert default_value = squashed_start.prev_value;
    }

    let (local new_start) = default_dict_new(default_value);
    ...
```

`dict_squash` itself does not assert the `prev_value` of the first element in the dictionary. 
As a result, the subsequent copied transient_storage's initial value is also under the malicious prover's control.

```
let (local new_start) = default_dict_new(default_value);
```

If we look at the source code of default_dict_finalize, we will notice an extra co
Insufficient input validation in ETH transaction decoder allows for manipulation of relative jump offset
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/eth_transaction.cairo#L244-L268


# Vulnerability details

The function `decode()` in `eth_transaction.cairo` is responsible for selecting the correct decoder for the given transaction type. It uses the `tx_type` item of the RLP-decoded transaction data as an offset into a jump table.

The provided type is validated using the following assertion:

```
        let is_supported = is_nn(2 - tx_type);
        with_attr error_message("Kakarot: transaction type not supported") {
            assert is_supported = TRUE;
        }
```

While `tx_data` is supposed to be a bytes array, there is no guarantee that the felt elements in the `tx_data` array provided to `eth_send_raw_unsigned_tx` (when called directly) are in the range 0..255. As a result, many of the assumptions in the transaction decoder are broken. 

Most of the time, this is not a problem since only account contracts created by Kakarot can successfully execute this function (otherwise, the transaction will revert during `eth_send_transaction()` / `safe_get_evm_address()`. This case is an exception since execution never reaches the sender address check in `safe_get_evm_address()`.

If `tx_type` is set to a value near `FIELD_PRIME`, the calculation `2 - tx_type` will underflow into a value within the expected bounds and the `is_nn()` check will pass.

As a result, the calculated offset will point into uninitialized memory, potentially allowing a malicious prover to craft/prove a transaction that executes arbitrary Cairo code. Note that whether actual code execution is possible depends on the underlying Cairo VM.

Note that this issue was previously reported by Zellic ([audit report](https://github.com/kkrt-labs/kakarot/blob/main/audits/cairo_zero/Kakarot%20EVM%20-%20Zellic%20Audit%20Report.pdf) chapter 3.25) and fixed by KKRT labs but the fix is insufficient.


## Proof of Concept

We can demonstrate this issue by modifying a line in the `helpers.py` test utility:

```
FIELD_PRIME = 2**251 + 17 * 2**192 + 1

def rlp_encode_signed_data(tx: dict) -> bytes:
    if "type" in tx:
        typed_transaction = TypedTransaction.from_dict(tx)

        sanitized_transaction = transaction_rpc_to_rlp_structure(
            typed_transaction.transaction.dictionary
        )

        # RPC-structured transaction to rlp-structured transaction
        rlp_serializer = (
            typed_transaction.transaction.__class__._unsigned_transaction_serializer
        )
        encoded_unsigned_tx = [
            FIELD_PRIME - 1,
            # typed_transaction.transaction_type,
            *rlp.encode(rlp_serializer.from_dict(sanitized_transaction)),
```

With this modified code, we run any of the `test__eth_send_raw_unsigned_tx` tests. The invalid chain id will not be detected. Instead, we will reach the `jmp rel offset` instruction:

```
Error at pc=0:25127:

Could not complete computation jmp rel of non pu
intrinsic gas consumption lack of error handling
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L923


# Vulnerability details

# Description

EVM transaction has this concept of [intrinsic gas cost](https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a0-0-intrinsic-gas)

> Intrinsic gas is the amount of gas paid prior to execution of a transaction. That is, the gas paid by the initiator of a transaction, which will always be an externally-owned account, before any state updates are made or any code is executed.

In kakarot Interpreter, [intrinsic gas consumption](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L923) performs a unsafe subtraction and lack of error handling 

```solidity
        let intrinsic_gas = intrinsic_gas + access_list_cost;
        let evm = EVM.init(message, gas_limit - intrinsic_gas); // @audit safe subtraction

        let is_gas_limit_enough = is_le_felt(intrinsic_gas, gas_limit);
        if (is_gas_limit_enough == FALSE) {
            let evm = EVM.halt_validation_failed(evm);
            State.finalize{state=state}();
            return (evm, stack, memory, state, 0, 0);
        }
```

If the gas limit is not sufficient to pay for intrinsic_gas, code panic happens in math underflow, while the code should gracefully happen the out of gas error.

# Recommendation

We can check [geth implementation](https://github.com/ethereum/go-ethereum/blob/b6c62d5887e2bea38df0c294077d30ca0f6a3c97/core/state_transition.go#L403) for reference.

```golang
	// Check clauses 4-5, subtract intrinsic gas if everything is correct
	gas, err := IntrinsicGas(msg.Data, msg.AccessList, contractCreation, rules.IsHomestead, rules.IsIstanbul, rules.IsShanghai)
	if err != nil {
		return nil, err
	}
	if st.gasRemaining < gas {
		return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas)
	}
```

then the cairo logic should be:

```solidity
        let intrinsic_gas = intrinsic_gas + access_list_cost;
        let is_gas_limit_enough = is_le_felt(intrinsic_gas, gas_limit);
        if (is_gas_limit_enough == FALSE) {
            let evm = EVM.halt_validation_failed(evm);
            State.finalize{state=state}();
            return (evm, stack, memory, state, 0, 0);
        }
        let evm = EVM.init(message, gas_limit - intrinsic_gas); // @audit safe subtraction
```




## Assessed type

Math
Lack of error handling for token transfer
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1032


# Vulnerability details

## Proof of Concept

The code to handle token transfer is:

```solidity
    /// A `Result` indicating success or an `EVMError` if the transfer fails.
    #[inline(always)]
    fn add_transfer(ref self: State, transfer: Transfer) -> Result<(), EVMError> {
        if (transfer.amount == 0 || transfer.sender.evm == transfer.recipient.evm) {
            return Result::Ok(());
        }
        let mut sender = self.get_account(transfer.sender.evm);
        let mut recipient = self.get_account(transfer.recipient.evm);

        let (new_sender_balance, underflow) = sender.balance().overflowing_sub(transfer.amount);
        ensure(!underflow, EVMError::InsufficientBalance)?;

        let (new_recipient_balance, overflow) = recipient.balance.overflowing_add(transfer.amount);
        ensure(!overflow, EVMError::NumericOperations(BALANCE_OVERFLOW))?;

        sender.set_balance(new_sender_balance);
        recipient.set_balance(new_recipient_balance); // @audit add callback?

        self.set_account(sender);
        self.set_account(recipient);

        self.transfers.append(transfer);
        Result::Ok(())
    }
```

the comments and implementations says: A `Result` indicating success or an `EVMError` if the transfer fails.

But in some code, the return value for transfer is not sufficiently handled, result in slient transfer failure.

### Slient failure of the coinbase address payment

In the end of transaction, the sender needs to [pay for fee](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1032) and the coinbase address is the recipient.

```solidity
        // So as to not burn the base_fee_per gas, we send it to the coinbase.
        let actual_fee = total_gas_used * env.gas_price;
        let (fee_high, fee_low) = split_felt(actual_fee);
        let actual_fee_u256 = Uint256(low=fee_low, high=fee_high);
        let transfer = model.Transfer(sender.address, coinbase.address, actual_fee_u256);

        with state {
            State.add_transfer(transfer);
            State.finalize();
        }
```

With unhandled add_transfer return, even when the sender address has insufficient balance and transaction should revert, the transfer sliently fails and the coinbase payment is bypassed.

This allow users to evade coinbase payment.

## Recommended Mitigation Steps

Always Handle the return value of add_transfer



## Assessed type

Math
All ether is removed from selfdestructed contracts at the end of the transaction 
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L189-L201
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L852-L868


# Vulnerability details

## Description
Contracts that are selfdestructed after Cancun will have all their ether balance sent to a destination after using the `SELFDESTRUCT` opcode. However, Kakarot will also burn any ether that has been sent after selfdestruct has been invoked. 
For example suppose selfdestruct is called on a recently created contract: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L852-L868
Then the entire eth balance will be sent to the recipient. However, suppose there is a transfer of ether to the exact same address later. After the transaction ends and `Starknet.commit` is called, we hit the following line of code: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L189-L201
This burns the leftover ether that the account has although the account is still supposed to have ether at the beginning of the next transaction. As a result this ETH is lost permanently. This completely breaks EVM semantics and many use cases (i.e. sending ether to a deterministic contract address that can be created later with create2) 

## Recommended Mitigation Steps
Don't remove the entire balance at the end of the transaction in `Starknet.commit`. 


## Assessed type

Token-Transfer
Precompiles that go out of gas still contain returndata
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L94-L100
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L762-L779
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1039


# Vulnerability details

## Description
When a precompile is called, gas is not charged immediately and instead is only charged if the call to the precompile is successful: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L94-L100 However instead of reverting with no returndata if there is not enough gas, `EVM.stop` is invoked which updates the returndata to the value given by the precompile. Because this is an exceptional revert it will be caught and the returndata will be set to 0 if invoked from any contract however if an EOA invokes a precompile (i.e the context is root) then the returndata will still be returned: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L762-L779
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1039
This directly violates the EVM specification since precompiles that go out of gas do not have any returndata
## Recommended Mitigation Steps
It might be better to charge gas directly in the precompile like how geth does instead of at the end of execution, or just ensure that `EXEPTIONAL_REVERT`s in the root ctx have no returndata


## Assessed type

Invalid Validation
Out-of-Bounds Read in parse_storage_keys Function
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/eth_transaction.cairo#L313


# Vulnerability details

**Summary**:  
An out-of-bounds read vulnerability exists in the `parse_storage_keys` function, which processes storage keys within Ethereum transactions. This issue can be exploited by a malicious prover to manipulate transaction data, potentially leading to serious security implications.

**Description**:  
When processing transactions via `eth_send_raw_unsigned_tx`, the transaction data (`tx_data`) is decoded using the `EthTransaction.decode` function. Based on the type of transaction, the data is further decoded by one of the following methods: `decode_legacy_tx`, `decode_2930`, or `decode_1559`. The resulting decoded transaction is represented as an `RLP.Item` structure:

```cairo
struct Item {
    data_len: felt,
    data: felt*,
    is_list: felt,
}
```

In the context of this structure, it is critical to verify that the `data_len` field accurately reflects the size of the `data` buffer before accessing its contents. However, in the `parse_storage_keys` function, the `keys_list.data_len` is not adequately checked, leading to an out-of-bounds read.

The relevant code is as follows:

```cairo
func parse_storage_keys{range_check_ptr}(
    parsed_keys: felt*, keys_list_len: felt, keys_list: RLP.Item*
) {
    alloc_locals;
    if (keys_list_len == 0) {
        return ();
    }

    let key = Helpers.bytes32_to_uint256(keys_list.data);
    assert [parsed_keys] = key.low;
    assert [parsed_keys + 1] = key.high;

    parse_storage_keys(
        parsed_keys + Uint256.SIZE, keys_list_len - 1, keys_list + RLP.Item.SIZE
    );
    return ();
}
```

The function directly accesses `keys_list.data` via the line:

```cairo
let key = Helpers.bytes32_to_uint256(keys_list.data);
```

If `keys_list.data_len` is less than 32 bytes, this access will read memory out of bounds, as `bytes32_to_uint256` expects at least 32 bytes of data to operate correctly. The absence of a check on `keys_list.data_len` before dereferencing `keys_list.data` opens up the potential for an attacker to exploit this vulnerability.

**Impact**:  
A malicious prover could leverage this out-of-bounds read to manipulate or fake the storage keys being read from the transaction. This could result in forged or manipulated data within the transaction, potentially compromising the integrity of the system and leading to unauthorized actions. Depending on the context of the transaction, this could lead to severe consequences, such as:

- Inaccurate storage key interpretation.
- Forged data that bypasses security checks.
- Manipulation of state data, leading to unauthorized access or control over storage.

**Recommendation**:  
To mitigate this issue, a proper check should be implemented to ensure that `keys_list.data_len` is at least 32 bytes before attempting to access `keys_list.data`. Specifically, the function should validate the
Underconstrained Hints in load_bytecode Function Allow Bytecode Manipulation
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/b8f5f2a20bd733cc8885c010291885e1df7dc50e/cairo_zero/utils/utils.cairo#L959-L1063


# Vulnerability details

# Summary
The load_bytecode function in the kakarot project is underconstrained due to critical computations being performed within hints without sufficient accompanying constraints. This vulnerability allows a malicious prover to manipulate the hint execution to produce incorrect proofs that would still be accepted by the verifier. Specifically, the prover can provide incorrect bytecode while passing all the assertions in the code, potentially leading to execution of malicious or unintended code.
Affected Code

The vulnerability is present in the load_bytecode function defined in the Cairo contract. The critical computations are performed within a hint that is underconstrained.
Code Snippet

```
%{
    memory[ids.output] = res = (int(ids.value) % PRIME) % ids.base
    assert res < ids.bound, f'split_int(): Limb {res} is out of range.'
%}
tempvar a = [output];
%{
    from starkware.cairo.common.math_utils import assert_integer
    assert_integer(ids.a)
    assert 0 <= ids.a % PRIME < range_check_builtin.bound, f'a = {ids.a} is out of range.'
%}
assert a = [range_check_ptr];
```


# Demonstration Using Test Case

To illustrate the vulnerability, consider the following test case where the hint is modified to manipulate the extracted bytes:

```

@given(bytecode=binary(min_size=1, max_size=400))
@settings(max_examples=5)
def test_should_raise_when_read_bytecode_zellic_issue_1279(
    self, cairo_program, cairo_run, bytecode
):  
    modified_hint = (
        "value_mod_prime = int(ids.value) % PRIME\n"
        "if value_mod_prime > ids.base:\n"
        "    res = (value_mod_prime - 1) % ids.base\n"
        "else:\n"
        "    res = value_mod_prime % ids.base\n"
        "memory[ids.output] = res\n"
        "assert res < ids.bound, f'split_int(): Limb {res} is out of range.'"
    )
                        
    with (
        patch_hint(
            cairo_program,
            "memory[ids.output] = res = (int(ids.value) % PRIME) % ids.base\n"
            "assert res < ids.bound, f'split_int(): Limb {res} is out of range.'",
            modified_hint,
        ),
        patch.object(
            SyscallHandler, "mock_storage", side_effect=self.storage(bytecode)
        ),
    ):
        with cairo_error(message="Value is not empty"):
            output_len, output = cairo_run("test__bytecode")
```


# Explanation of the Proof of Concept

* Test Execution:
        The test runs the load_bytecode function with the modified hint.
        It uses patch_hint to replace the original hint with the modified one.
        The bytecode input is generated using Hypothesis, allowing for various bytecode values to be tested.

* Expected Behavior:
        The assertion assert value == 0 should fail when the value is incorrectly updated due to manipulated res values.
        An exception with the messag
Nonce is incorrectly set for reverted create txs 
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/ethereum/go-ethereum/blob/65e5ca7d8126f7a8c708f8affb64f16c22cc63c0/core/vm/evm.go#L442-L449
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1024


# Vulnerability details

## Description
If a contract create tx from the root context reverts due to an out of balance check, geth will not update the nonce: https://github.com/ethereum/go-ethereum/blob/65e5ca7d8126f7a8c708f8affb64f16c22cc63c0/core/vm/evm.go#L442-L449
However kakarot will incorrectly increment the nonce by 1: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1024
This breaks EVM conformance. 

## Recommended Mitigation Steps
Ensure that if the nonce overflows or if the value being transferred is more than allowed that the caller's nonce is not incremented.  


## Assessed type

Context