xanoutas/solidity-security-findings huggingface dataset:
Missing enforcement of Lower s Value allows for signature malleability
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/697100af34444b3931c18596cec56c454caf28ed/src/utils/signature.cairo#L13-L35


# Vulnerability details

## Summary

The [verify_eth_signature_uint256](https://github.com/kkrt-labs/kakarot/blob/697100af34444b3931c18596cec56c454caf28ed/src/utils/signature.cairo#L13-L35) function in Kakarot zkEVM’s signature implementation lacks enforcement of the lower s value limit in ECDSA signature verification, which can lead to signature malleability.

## Issue Details

### Background

In Ethereum, ECDSA signatures are used to authenticate transactions and messages. A signature consists of three components:

- **`r`**: The x-coordinate of a point on the elliptic curve.
- **`s`**: A scalar value derived during the signing process.
- **`v`**: The recovery identifier, indicating the y-parity of the public key.

For security reasons, and to prevent signature malleability, Ethereum enforces that the s component must satisfy:

$$
0 < s < \frac{secp256k1n}{2} + 1
$$

where secp256k1n is the order of the secp256k1 curve.


### Current Implementation

The `verify_eth_signature_uint256` function in the Kakarot zkEVM implementation is responsible for verifying Ethereum ECDSA signatures. Below is the relevant code:

```cairo
%lang starknet

from starkware.cairo.common.cairo_builtins import BitwiseBuiltin
from starkware.cairo.common.cairo_secp.bigint3 import BigInt3
from starkware.cairo.common.cairo_secp.signature import validate_signature_entry
from starkware.cairo.common.uint256 import Uint256
from kakarot.interfaces.interfaces import ICairo1Helpers
from starkware.cairo.common.cairo_secp.bigint import uint256_to_bigint

namespace Signature {
    // A version of verify_eth_signature, with msg_hash, r, and s as Uint256
    // and using the Cairo1 helpers class.
    func verify_eth_signature_uint256{
        syscall_ptr: felt*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*
    }(
        msg_hash: Uint256,
        r: Uint256,
        s: Uint256,
        v: felt,
        eth_address: felt,
        helpers_class: felt
    ) {
        alloc_locals;
        let (msg_hash_bigint: BigInt3) = uint256_to_bigint(msg_hash);
        let (r_bigint: BigInt3) = uint256_to_bigint(r);
        let (s_bigint: BigInt3) = uint256_to_bigint(s);

        with_attr error_message("Signature out of range.") {
            validate_signature_entry(r_bigint);
            validate_signature_entry(s_bigint);
        }

        with_attr error_message("Invalid signature.") {
            let (success, recovered_address) = ICairo1Helpers.library_call_recover_eth_address(
                class_hash=helpers_class,
                msg_hash=msg_hash,
                r=r,
                s=s,
                y_parity=v
            );
            assert success = 1;
            assert eth_address = recovered_address;
        }
        return ();
    }
}
```

In this function:

- The signature components `r` and `s` are converted to `BigInt3` representation
Unauthorized Contracts Can Bypass Precompile Authorization via delegatecall in Kakarot zkEVM
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/6496ee1d18380dae5b069c27b3129d82ae282419/cairo_zero/kakarot/precompiles/precompiles.cairo#L29-L161


# Vulnerability details

## Summary
In the Kakarot zkEVM implementation exec_precompile, unauthorized contracts can bypass the precompile authorization mechanism using the `delegatecall`. This allows unauthorized contracts to execute privileged precompile functions intended only for authorized (whitelisted) contracts, compromising the security and integrity of the system.


## Vulnerability Details:

Let's take a look at the exec_precompile function with the unessential part omitted for brevity:

```cairo
func exec_precompile{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
    bitwise_ptr: BitwiseBuiltin*,
}(
    precompile_address: felt,
    input_len: felt,
    input: felt*,
    caller_code_address: felt,
    caller_address: felt,
) -> (output_len: felt, output: felt*, gas_used: felt, reverted: felt) {

    //SNIP...
    // Authorization for Kakarot precompiles
    let is_kakarot_precompile = PrecompilesHelpers.is_kakarot_precompile(precompile_address);
    if is_kakarot_precompile != 0 {
        // Check if caller is whitelisted
        let is_whitelisted = KakarotPrecompiles.is_caller_whitelisted(caller_code_address);
        if is_whitelisted == FALSE {
            jmp unauthorized_call;
        }
        // Proceed with precompile execution
        // ...
    } else {
        jmp unauthorized_call;
    }
}
````
The issue arises because the authorization mechanism relies solely on `caller_code_address`, which refers to the code being executed, and does not consider `caller_address`, the actual address of the contract making the call. This approach is insufficient and can be exploited using `delegatecall`.

**Analogy:**
If an EVM contract A delegatecalls EVM contract B, and B is allowlisted, then the call to the Cairo precompile succeeds because the allowlist is based on the code address (of B) and not on the execution address (of A).
This means that an unauthorized contract (Contract A) can exploit this behavior to execute privileged operations by delegatecalling a whitelisted contract (Contract B) and passing the authorization check incorrectly.

As such, it is an issue for other whitelisted smart contracts allowing arbitrary calls to external smart contracts. For example, this is the case of some smart contracts allowing callbacks. In those situations, a malicious user could make a DEX execute a call to the malicious smart contract that would be able to access the precompiles pretending to be the DEX.

## Impact
This issue could allow unauthorized contracts to perform operations that should be restricted to whitelisted contracts like in the case of DEX that supports callbacks and has been added to the allowistlist. Moreover, all contracts within the allowistlist must be written with the assumption that they can also be delegatecalled; i.e they must not
Failure in Transaction Execution Due to Incorrect Caller Verification in `eth_send_raw_unsigned_tx` and `execute_from_outside` functions
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/eth_rpc.cairo#L256-L263
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/account_contract.cairo#L108


# Vulnerability details

## Summary
The `eth_send_raw_unsigned_tx` and `execute_from_outside` function in the Cairo contract is vulnerable due to improper verification of the `caller_address` using the `get_caller_address()` function. According to the Cairo documentation, when a contract is called directly (rather than through another contract), `get_caller_address()` returns `0`. This can lead to transactions being executed without proper authorization or verification, creating a significant security risk. The issue is also present in other audited and non-audited contracts. To address this, it is recommended to refactor the logic into an internal function and ensure that an external function(with `@external` ) calls it, preventing unauthorized execution and verification failure.

## Proof of Concept
The function `eth_send_raw_unsigned_tx` in the `eth_rpc.cairo` contract is responsible for processing unsigned transaction data. However, there is a potential security issue arising from the use of the `get_caller_address()` function. According to the official [Cairo documentation](https://docs.cairo-lang.org/hello_starknet/user_auth.html#), when a contract is called directly (without an intermediary contract), `get_caller_address()` returns `0`. This could lead to serious security risks if the transaction is executed without proper verification.

In `eth_send_raw_unsigned_tx`, the `caller_address` is used to retrieve the nonce of the account and validate the transaction. If `caller_address` returns `0`, this could allow transactions to be executed without proper verification, which may lead to unauthorized transactions. This could allow bypassing of certain permission checks or cause failed authentication, as many contracts assume the caller address is non-zero. If the caller verification fails due to the zero address, subsequent validation might not be properly executed.

[kakarot/src/kakarot/eth_rpc.cairo:eth_send_raw_unsigned_tx-L256-L263](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/eth_rpc.cairo#L256C1-L263C6)
```cairo

// @notice The eth_send_raw_unsigned_tx. Modified version of eth_sendRawTransaction function described in the spec.
//         See https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction
// @dev This function takes the transaction data unsigned. Signature validation should be done before calling this function.
// @param tx_data_len The length of the unsigned transaction data
// @param tx_data The unsigned transaction data
// @return return_data_len The length of the return_data
// @return return_data An array of returned felts
// @return success A boolean, TRUE if the transaction succeeded, F
Prover can cheat in felt_to_bytes_little due to value underflow
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/bytes.cairo#L43-L138


# Vulnerability details

The function `felt_to_bytes_little()` in `bytes.cairo` converts a felt into an array of bytes.

The prover can cheat by returning a near arbitrary string that does not correspond to the input felt whereby the spoofed output bytes and `bytes_len` bytes must fulfill some specific conditions (but, if carefully crafted, can contain almost arbitrary sequences of bytes).

This issue affects the function `felt_to_bytes_little() as well as other functions that depend on it:

```
- felt_to_bytes()
- uint256_to_bytes_little()
- uint256_to_bytes()
- uint256_to_bytes32()
- bigint_to_bytes_array()
```

Those functions are used throughout the code, notably in `get_create_address()` and `get_create2_address()`, which an attacker could exploit to deploy L2 smart contracts from a spoofed sender address (e.g. to steal funds from wallets that use account abstraction).

### Details

First, note the following loop:

```
    body:
    let range_check_ptr = [ap - 3];
    let value = [ap - 2];
    let bytes_len = [ap - 1];
    let bytes = cast([fp - 4], felt*);
    let output = bytes + bytes_len;
    let base = 2 ** 8;
    let bound = base;

    %{
        memory[ids.output] = res = (int(ids.value) % PRIME) % ids.base
        assert res < ids.bound, f'split_int(): Limb {res} is out of range.'
    %}
    let byte = [output];
    with_attr error_message("felt_to_bytes_little: byte value is too big") {
        assert_nn_le(byte, bound - 1);
    }
    tempvar value = (value - byte) / base;

    tempvar range_check_ptr = range_check_ptr;
    tempvar value = value;
    tempvar bytes_len = bytes_len + 1;

    jmp body if value != 0;
```

We observe that:

1. Value can underflow if the byte returned in the last iteration is greater than the value remaining in the felt. For example, if the remaining value is 1 but the prover/hint returns 2, value will underflow into `STARKNET_PRIME - 1`. Consequently, the loop will continue running as `value != 0`.

2. In the following iterations of the loop, the prover can return arbitrary values, they just need to ensure that value eventually becomes 0. They can use as many iterations as required, but it is important that `bytes_len` ends up at a specific value (see 3).

3. Finally, at the end of the function, there is a check that `bytes_len` is the minimal one possible to represent the value. The lower bound and upper bound for this check is read from `pow256_table` at the offset `bytes_len`. The malicious prover must ensure that the value at this memory address (somewhere into the code segment) is lower than `initial_value`. Any nearby position where the Cairo bytecode contains a zero works.

## Proof of Concept

To pass the bounds check, we need a code offset that contains the value `0`. Fortunately (from a malicious prover's perspective) there are many zero-value locations in the cod
Lack of Validation for r and s in ec_recover Precompile Allows Signature Forgery and Denial of Service
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ec_recover.cairo#L61-L74


# Vulnerability details

Vulnerability Details:
In the PrecompileEcRecover contract's run function, the v value is checked to ensure it matches either 27 or 28, which is part of the standard ECDSA signature format in Ethereum. However, no checks are performed on the r and s values, which represent crucial parts of the signature.

Key Issues:
No validation for r and s: The code assumes that the r and s values are correctly formatted and valid without checking whether they lie within the correct range for the secp256k1 elliptic curve.
Consequences of invalid r or s:
Signature Forgery: If r and s are not validated, an attacker could forge signatures, as invalid values could bypass the cryptographic checks.
Denial of Service (DoS): Invalid r or s values could cause the elliptic curve operations to behave unexpectedly, possibly resulting in excessive gas usage or failure of the recovery process, disrupting the contract's functionality.
Relevant Code:
let r = Helpers.bytes_to_uint256(32, input_padded + 32 * 2);
let s = Helpers.bytes_to_uint256(32, input_padded + 32 * 3);

// v is validated, but r and s are not.
if ((v - 27) * (v - 28) != 0) {
    let (output) = alloc();
    return (0, output, GAS_COST_EC_RECOVER, 0);
}

The lack of range validation for r and s introduces a critical vulnerability, allowing attackers to exploit the signature recovery process.

Proof of Concept:
An attacker could supply invalid or out-of-range values for r or s to exploit this vulnerability. For example:
let malicious_r = <invalid_r_value>;   // r should be within the secp256k1 curve order
let malicious_s = <invalid_s_value>;   // s should be in the range 0 < s < secp256k1 order
let v = 27;                            // Valid v value
let input = [msg_hash, malicious_r, malicious_s, v];

Potential impacts:
Signature Forgery: The elliptic curve recovery might return an invalid or incorrect public key, allowing an attacker to bypass signature verification.
Denial of Service (DoS): Supplying out-of-range values for r or s could lead to excessive computation, resulting in a gas drain or unexpected contract behavior.

Recommended Mitigation Steps:
Validate r and s Values:
Add checks to ensure that r and s conform to the secp256k1 curve requirements before performing elliptic curve recovery. For example:
Ensure that r and s are both positive and non-zero.
Ensure that r and s are smaller than the secp256k1 curve order (a constant value).

Example validation:
let secp256k1_order = <secp256k1_order_value>;
if (r == 0 or r >= secp256k1_order or s == 0 or s >= secp256k1_order) {
    revert();  // Invalid signature values
}

Range Check for s:
Ensure that s is checked to lie within 0 < s < secp256k1 order, following EIP-2 (which mandates lower s values to prevent signature malleability).
Graceful Error Handling:
Add error-handling logic
Lack of Validation on evm_address in Account Functions
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Proof of Concept
The contract does not validate evm_address in critical functions like deploy_externally_owned_account, register_account, and write_account_bytecode. An invalid or zero address could lead to incorrect account deployments or unauthorized modifications.

```
@external
func deploy_externally_owned_account{
    syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}(evm_address: felt) -> (starknet_contract_address: felt) {
    Pausable.assert_not_paused();
    return Kakarot.deploy_externally_owned_account(evm_address);
}
```

If a malicious or invalid evm_address is passed, an incorrect StarkNet account could be deployed or registered, potentially leading to loss of assets or account hijacking.

Impact:

Invalid account deployments or registration could result in loss of funds or control over user accounts.



## Recommended Mitigation Steps
Validate the evm_address to ensure it is non-zero and follows the correct address format:
```
@external
func deploy_externally_owned_account{
    syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr
}(evm_address: felt) -> (starknet_contract_address: felt) {
    Pausable.assert_not_paused();
    assert evm_address != 0; // Add validation here
    return Kakarot.deploy_externally_owned_account(evm_address);
}
```



## Assessed type

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

# Lines of code

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


# Vulnerability details

**Summary**:  
An out-of-bounds read vulnerability exists in the `parse_access_list` function, where inadequate validation of RLP (Recursive Length Prefix) encoded data leads to potential memory overreads. A malicious prover could exploit this flaw to manipulate transaction data, undermining system integrity.

**Description**:  
When processing transactions via `eth_send_raw_unsigned_tx`, the transaction data is decoded using `EthTransaction.decode`. Depending on the transaction type, it is further processed by one of the following functions: `decode_legacy_tx`, `decode_2930`, or `decode_1559`. The decoded transaction is represented in the form of an `RLP.Item` structure:

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

It is essential that `data_len` be validated before accessing the `data` field to prevent out-of-bounds memory access. However, in the `parse_access_list` function, this validation is missing, which leads to multiple potential out-of-bounds reads.

The `decode_raw` function that processes RLP data is responsible for ensuring correct decoding, but in some cases, the `data_len` of an `RLP.Item` may be zero, particularly when dealing with lists of zero length:

```cairo
func decode_raw{range_check_ptr}(items: Item*, data_len: felt, data: felt*) -> felt {
    alloc_locals;

    if (data_len == 0) {
        return 0;
    }

    let (rlp_type, offset, len) = decode_type_unsafe(data);
    assert [range_check_ptr] = offset + len;
    local remaining_data_len = data_len - [range_check_ptr];
    assert_nn(remaining_data_len);

    if (rlp_type == TYPE_LIST) {
        let (sub_items: Item*) = alloc();
        let sub_items_len = decode_raw(items=sub_items, data_len=len, data=data + offset);
        assert [items] = Item(sub_items_len, cast(sub_items, felt*), TRUE);
    } else {
        assert [items] = Item(data_len=len, data=data + offset, is_list=FALSE);
    }
    return 1 + sub_items_len;
}
```

This logic can lead to situations where an `RLP.Item` has a `data_len` of zero, particularly when the decoded data consists of just one byte (e.g., `0xc0`, which is interpreted as an empty list). The function `decode_type_unsafe` confirms that such data can result in:

```cairo
(TYPE_LIST, 1, 0)  // Meaning the list is empty, with data_len = 0
```

However, this possibility is not properly handled in the `parse_access_list` function.

The vulnerable code in `parse_access_list` is as follows:

```cairo
func parse_access_list{range_check_ptr}(
    parsed_list: felt*, access_list_len: felt, access_list: RLP.Item*
) -> felt {
    alloc_locals;
    if (access_list_len == 0) {
        return 0;
    }

    // Address
    let address_item = cast(access_list.data, RLP.Item*);
    let address = Helpers.bytes20_to_felt(address_item.data);

    // List<StorageK
Users lose out on their funds if they self-destruct in the same tx
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/6496ee1d18380dae5b069c27b3129d82ae282419/cairo_zero/backend/starknet.cairo#L164


# Vulnerability details


## Proof of Concept

Take a look at https://github.com/kkrt-labs/kakarot/blob/6496ee1d18380dae5b069c27b3129d82ae282419/cairo_zero/backend/starknet.cairo#L164

```
    func _commit_account{
        syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, state: model.State*
    }(self: model.Account*, native_token_address) {
        alloc_locals;

        let is_precompile = PrecompilesHelpers.is_precompile(self.address.evm);
        if (is_precompile != FALSE) {
            return ();
        }

..snip

        // @dev: EIP-6780 - If selfdestruct on an account created, dont commit data
        // and burn any leftover balance.
        let is_created_selfdestructed = self.created * self.selfdestruct;
        if (is_created_selfdestructed != 0) {
            let starknet_address = Account.compute_starknet_address(Constants.BURN_ADDRESS);
            tempvar burn_address = new model.Address(
                starknet=starknet_address, evm=Constants.BURN_ADDRESS
            );
            let transfer = model.Transfer(self.address, burn_address, [self.balance]);
            State.add_transfer(transfer);
            return ();
        }

        let has_code_or_nonce = Account.has_code_or_nonce(self);
        if (has_code_or_nonce == FALSE) {
            // Nothing to commit
            return ();
        }

..snip
        return ();
    }
```

This function is used to commit the account to the storage backend at given address, now it's been hinted that this should follow `EIP-6780`, issue however is that in this case if the contract is selfdestructed and then [funds are sent **after**, the funds are then burnt](https://github.com/kkrt-labs/kakarot/blob/6496ee1d18380dae5b069c27b3129d82ae282419/cairo_zero/backend/starknet.cairo#L159), this goes against both the EIP and also classic EVM implementation considering.

Considering funds can actually be sent to any address be it it has smart contract code or not.

Also going to the implementation of the [EIP](https://eips.ethereum.org/EIPS/eip-6780), we can see the below hinted whenever selfdestruct is called in the same tx as the creation:

> 2. When `SELFDESTRUCT` is executed in the same transaction as the contract was created:

> - `SELFDESTRUCT` continues to behave as it did prior to this EIP, this includes the following actions
>   - The current execution frame halts.
>   - `SELFDESTRUCT` deletes data as previously specified.
>   - `SELFDESTRUCT` transfers the entire account balance to the target
> - The account balance of the contact calling `SELFDESTRUCT` is set to `0`.
> - Note that if the target is the same as the contract calling `SELFDESTRUCT` that Ether will be burnt.
> - Note that no refund is given since [EIP-3529](./eip-3529.md).
> - Note that the rules of [EIP-2929](./eip-2929.md) regarding `SELFDESTRUCT` remain unchanged
Current memory length is computed incorrectly inside `calculate_gas_extend_memory`
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description
The calculate_gas_extend_memory function computes the `current_memory_length` with the following calculation: https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/gas.cairo#L87
However the correct calculation should be: `let current_memory_length = (words_len * 32) * is_memory_length_not_zero;`
This can lead to computing the length incorrectly and incorrectly undercharging for gas during memory expansion

## Recommended Mitigation Steps
Calculate the memory length without subtracting 1 from the words_len 


## Assessed type

Invalid Validation