xanoutas/solidity-security-findings huggingface dataset:
`decode_legacy_tx` allows validation of signatures with chain_id that are larger than felt, and overflows
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

In decode_legacy_tx, data_len for chain_id is not constrained to be <31, which allows validation of signatures with chain_id that are larger than felt, and overflows


## Proof of Concept
chain_id is gotten from items[6].data via the bytes_to_felt helper function.

Looking at bytes_to_felt function, we will observe that the return value will overflow if len>31, because current would have been shifted 2^(8\*32) times, which overflows the max felt value of 2^252:

```
    func bytes_to_felt(len: felt, ptr: felt*) -> felt {
        if (len == 0) {
            return 0;
        }
        tempvar current = 0;

        // len, ptr, ?, ?, current
        // ?, ? are intermediate steps created by the compiler to unfold the
        // complex expression.
        loop:
        let len = [ap - 5];
        let ptr = cast([ap - 4], felt*);
        let current = [ap - 1];

        tempvar len = len - 1;
        tempvar ptr = ptr + 1;
        tempvar current = current * 256 + [ptr - 1];//@audit-info overflow if len>31

        static_assert len == [ap - 5];
        static_assert ptr == [ap - 4];
        static_assert current == [ap - 1];
        jmp loop if len != 0;

        return current;
    }
```

Since items[6].data_len is not constrained to be <=31, absurd values of chain_id(containing >31 data_len) can be passed, which would overflow, and pass the signature validation

## Recommended Mitigation Steps
assert that items[6].data_len<=31:

```
assertassert_nn(31 - items[6].data_len);
```



## Assessed type

Under/Overflow
handle_l1_message allows l2 tx to exceeds globally set block limit and diverges from EIP-1559
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
In an EVM chain, both gas price and gas limit should be strictly respected on all transactions. However, `handle_l1_message` allows exceptions, which diverge from EIP-1559 and may create unexpected result.
```rust
//src/kakarot/library.cairo

    func handle_l1_message{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
    }(l1_sender: felt, to_address: felt, value: felt, data_len: felt, data: felt*) -> (
        model.EVM*, model.State*, felt, felt
    ) {
        // TODO: ensure fair gas limits and prices
        let (val_high, val_low) = split_felt(value);
        tempvar value_u256 = new Uint256(low=val_low, high=val_high);
        let to = model.Option(is_some=1, value=to_address);
        let (access_list) = alloc();

        return eth_call(
|>          0, l1_sender, to, 2100000000, 1, value_u256, data_len, data, 0, access_list
        );
    }
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/library.cairo#L436)

`handle_l1_message` is part of L1 to L2 message flow which is initiated by an L1 user but directly invoked by starknet (@l1handler) on l2.

(1) global block limit:

Kakarot's block limit is [updated by admin](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L176) and strictly check in `eth_send_raw_unsigned_tx` flow ([assert_nn(block_gas_limit - tx.gas_limit)](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/eth_rpc.cairo#L279)).

However, `handle_l1_message` will hardcode 2100,000,000 gaslimit for every l2 tx from l1 to l2 message flow. Note that 2100 million gaslimit is staggering for a transaction and for reference Ethereum's block limit is 30 million. This allows `handle_l1_message` to execute tx that greatly exceeds admin set global block limit, which may cause unexpected results.

(2) gasPrice:

Based on [EIP-1559 fee market change](https://eips.ethereum.org/EIPS/eip-1559#abstract), EVM gasPrice should be based on the parent block usage. 

We see in handle_l1_message flow, L1sender will be charged kakarot EVM gas at a fixed gasPrice as 1. This means no matter how busy Kakarot EVM is or current [base_fee updated by admin](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L125). L2 tx from handle_l1_message uses the same gasPrice. Although might be intentional design, it diverges from EIP-1559.

Impacts:
`handle_l1_message` can execute l2 tx that greatly exceeds global block limit at hardcoded cheap price. In addition to divergence of EIP-1559 and admin set parameters, this is unsafe by allowing a malicious l1 sender to bypass gas limit check in eth_rpc flow, which
`ExponentiationImpl::pow()` returns 0 for 0^0
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot-ssj/blob/d4a7873d6f071813165ca7c7adb2f029287d14ca/crates/utils/src/math.cairo#L41


# Vulnerability details

The [`ExponentiationImpl::pow()`](https://github.com/kkrt-labs/kakarot-ssj/blob/d4a7873d6f071813165ca7c7adb2f029287d14ca/crates/utils/src/math.cairo#L39) function in `math.cairo` incorrectly returns 0 when computing 0^0, instead of the mathematically accepted value of 1. This breaks a fundamental mathematical convention that is relied upon in many mathematical contexts, including polynomial evaluation, Taylor series, and combinatorial calculations.

The issue occurs because the function first checks if the base is zero and returns zero if true, without considering the special case where the exponent is also zero. This early return means that 0^0 evaluates to 0 instead of 1:

```rust
fn pow(self: T, mut exponent: T) -> T {
	let zero = Zero::zero();
	if self.is_zero() {
		return zero;
	}
	...
```

The mathematical definition of 0^0 = 1 is not arbitrary - it is the natural definition that makes many mathematical formulas and theorems work correctly. For example, this definition is necessary for:
- The binomial theorem to work correctly when x=0
- Power series expansions to be valid at x=0
- Combinatorial formulas involving empty sets
- Preserving continuity in certain mathematical limits

This function is not currently being used to compute 0^0 in the code in scope. However, given the critical nature of the function and fundamental incorrectness of its output, the expectation of this issue causing vulnerabilities in [future code](https://docs.code4rena.com/awarding/judging-criteria/severity-categorization#speculation-on-future-code) is fulfilled.

## Impact
- Mathematical operations that rely on the standard convention of 0^0 = 1 will produce incorrect results
- Future code that reaches this case in core Kakarot contracts, protocols built on top of Kakarot's codebase or borrowing from it will experience material errors when processing edge cases

## Proof of Concept
N/A

## Recommended Mitigation Steps
Add a check for the 0^0 case before checking if the base is zero:

```cairo
fn pow(self: T, mut exponent: T) -> T {
    // Handle 0^0 case first
    if self.is_zero() && exponent.is_zero() {
        return One::one();
    }
    
    // Rest of the existing function...
    if self.is_zero() {
        return Zero::zero();
    }
    // ...
}
```

This change preserves the mathematically correct behavior while maintaining all other functionality of the power function.


## Assessed type

Other
Reentrancy check in account_contract can be easily circumvented
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The `account_contract` has a reentrancy check in `execute_starknet_call` to prevent calls made from the Kakarot EVM to Starknet to re-enter the Kakarot EVM.

The check is implemented by checking that `execute_starknet_call` calls Kakarot's address only with the `get_starknet_address` getter, which is indeed harmless:

```cairo
File: account_contract.cairo
332: @external
333: func execute_starknet_call{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
334:     to: felt, function_selector: felt, calldata_len: felt, calldata: felt*
335: ) -> (retdata_len: felt, retdata: felt*, success: felt) {
336:     Ownable.assert_only_owner();
337:     let (kakarot_address) = Ownable.owner();
338:     let is_get_starknet_address = Helpers.is_zero(
339:         GET_STARKNET_ADDRESS_SELECTOR - function_selector
340:     );
341:     let is_kakarot = Helpers.is_zero(kakarot_address - to);
342:     tempvar is_forbidden = is_kakarot * (1 - is_get_starknet_address);
343:     if (is_forbidden != FALSE) {
344:         let (error_len, error) = Errors.kakarotReentrancy();
345:         return (error_len, error, FALSE);
346:     }
347:     let (retdata_len, retdata) = call_contract(to, function_selector, calldata_len, calldata);
348:     return (retdata_len, retdata, TRUE);
349: }
350: 
```

However, this check leaves another possibility open, that is that the `account_contract` could call itself or another `account_contract` with a signed transaction to re-enter the Kakarot EVM, which is extremely vulnerable to reentrancy because of its extensive use of cached data.

While an exploit via this path can have critical impact on the integrity of the EVM, it's likelihood is extremely low because `execute_starknet_call` is accessible only via whitelisted contracts.

## Proof of Concept
Because Kakarot stores the effects of the execution at the very end of it via the `Starknet.commit`, it openly does not follow the check-effect-interaction pattern, so the integrity of the EVM is at risk of reentrancy via submissions of signed transactions within the `call cairo` precompile.

## Recommended Mitigation Steps
Consider removing the reentrancy check in `account_contract`, and adding a reentrancy guard on the `Kakarot.eth_call` function.



## Assessed type

Other
Kakarot's p256Verify implementation can return false negatives
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Kakarot implements a Secp256r1 verification contract `p256verify`, in compliance with the [RIP-7212 rollup implementation proposal](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md#precompiled-contract-gas-usage) also adopted by popular L2 EVMs like Optimism.

We ported the test cases used for Optimism's verification logic, and among the 781 test cases ported, we found one that failed: a signature that was expected to be found valid, was found invalid.

We did not proceed with further investigating the root causes because the libraries used for this calculation, `ark-secp256k1`, are outside of the scope of this contest, but we still reported this issue because there is a provable liveness impact on the code in scope. 

Consequently, some valid Secp256r1 elliptic curve signatures can be incorrectly rejected by Kakarot.

## Proof of Concept
To prove the point, it is sufficient to port [this Optimism p256 test case](https://github.com/ethereum-optimism/op-geth/blob/7b56c731573a32af0beedc5f38dbe7ca9d96630a/core/vm/testdata/precompiles/p256Verify.json#L4735) to the Kakarot `test_rip_7212.py` test suite, by changing the input of the `test_should_verify_secp256r1_signature` test as follows:

```python
File: test_rip_7212.py
            input_data = (
                "532eaabd9574880dbf76b9b8cc00832c20a6ec113d682299550d7a6e0f345e25"
                "000000000000000000000000000000004319055358e8617b0c46353d039cdaab"
                "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc63254e"
                "d705d16f80987e2d9b1a6957d29ce22febf7d10fa515153182415c8361baaca4"
                "b1fc105ee5ce80d514ec1238beae2037a6f83625593620d460819e8682160926"
            )
```

While this test fails with the following message, all other test cases taken from the Optimism `p256Verify.json` test inputs behave as expected.
```
==================================================================== FAILURES =====================================================================
____________________________________ TestRIP7212.TestRIP7212Integration.test_should_verify_secp256r1_signature ____________________________________
[...]
        is_valid = await p256_verify_invoker.p256verify(
            msg_hash,
            r_,
            s_,
            x_,
            y_,
            caller_eoa=owner.starknet_contract,
        )
>       assert is_valid
E       assert False

tests/end_to_end/RIP7212/test_rip_7212.py:1828: AssertionError
```

## Recommended Mitigation Steps
Consider changing the library used for Secp256r1 verification or document the possibility of false negatives in rollup signature verification.



## Assessed type

Other
Incorrect decoding of supply in DualVmToken
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L82


# Vulnerability details

The `DualVmToken` contract acts as a wrapper for Starknet ERC-20 tokens.

In its interactions with these contracts, it applies the necessary encoding/decoding to pass data to/from Starknet contracts, whose interface differs mostly due to the large use of the `felt` data type.

Among these encodings/decodings, one is insufficient: let's look at the decoding of data coming from a `balance_of` request:

```solidity
File: DualVmToken.sol
103:     function _balanceOf(uint256 starknetAddress) private view returns (uint256) {
104:         if (starknetAddress >= STARKNET_FIELD_PRIME) {
105:             revert InvalidStarknetAddress();
106:         }
107:         uint256[] memory balanceOfCallData = new uint256[](1);
108:         balanceOfCallData[0] = starknetAddress;
109:         bytes memory returnData = starknetToken.staticcallCairo("balance_of", balanceOfCallData);
110:         (uint128 valueLow, uint128 valueHigh) = abi.decode(returnData, (uint128, uint128));
111:         return uint256(valueLow) + (uint256(valueHigh) << 128);
112:     }
```

...where we see at L111 that the returned value is the concatenation of two 128-bit chunks; we compare this with the decoding for `total_supply`:
```solidity
    function totalSupply() external view returns (uint256) {
        bytes memory returnData = starknetToken.staticcallCairo("total_supply");
        return abi.decode(returnData, (uint256));
    }
```

The `total_supply` implementation is naively casting the return data to `uint256` without the necessary byte manipulation; it will therefore only return the 16 least significant bytes of the response, zeroing out the 16 most significant ones. 

## Proof of Concept
This concept can be proved easily by changing the test setup in the `test_dual_vm_token.py` suite to initially mint $2^{130} + 5$ tokens instead of $10^{18}$:

```python
@pytest_asyncio.fixture(scope="function")
async def starknet_token(owner):
    address = (
        await deploy_starknet(
            "StarknetToken", int(2**130 + 5), owner.starknet_contract.address
        )
    )["address"]
    return get_contract_starknet("StarknetToken", address=address)
```

After this, the `test_should_return_total_supply` test fails with the following output:
```python
    async def test_should_return_total_supply(self, starknet_token, dual_vm_token):
        (total_supply_starknet,) = await starknet_token.functions[
            "total_supply"
        ].call()
        total_supply_evm = await dual_vm_token.totalSupply()
>       assert total_supply_starknet == total_supply_evm
E       assert 1361129467683753853853498429727072845829 == 5

tests/end_to_end/CairoPrecompiles/test_dual_vm_token.py:69: AssertionError
```

## Recommended Mitigation Steps
Consider applying similar result decoding in `totalSupply` as done in `_balan
Prevrandao opcode does not follow the Ethereum spec
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

In its current implementation, the `prevrandao` opcode returns a constant value instead of the source of entropy that is expected as per Ethereum spec.
Because of this, the Kakarot EVM is [not compliant with the EIP-4399 spec](https://eips.ethereum.org/EIPS/eip-4399), and this difference is not listed among the known issues.

Consequently, EVM applications relying on the `prevrandao` opcode will not function properly because a constant value is provided.

## Proof of Concept
- Deploy a contract with a function that calls `prevrandao` and returns its output.
- call this function multiple times in different blocks
- the return value will be constant

## Recommended Mitigation Steps
Consider implementing `prevrandao` e.g. via a [Pragma VRF](https://starknet-by-example.voyager.online/applications/random_number_generator.html).



## Assessed type

Other
Incorrect L1 address aliasing prevents EOAs from operating their L2 wallets
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol#L35


# Vulnerability details

Within the `L1KakarotMessaging.sol` contract, the `sendMessageToL2` function allows a caller to have their corresponding address on L2 perform an arbitrary call within the Kakarot EVM.

If we look at its implementation, we see that at L35, address aliasing is applied:
```solidity
File: L1KakarotMessaging.sol
32:     function sendMessageToL2(address to, uint248 value, bytes calldata data) external payable {
33:         uint256 totalLength = data.length + 4;
34:         uint256[] memory convertedData = new uint256[](totalLength);
35:         convertedData[0] = uint256(uint160(AddressAliasHelper.applyL1ToL2Alias(msg.sender)));
36:         convertedData[1] = uint256(uint160(to));
37:         convertedData[2] = uint256(value);
38:         convertedData[3] = data.length;
39:         for (uint256 i = 4; i < totalLength; ++i) {
40:             convertedData[i] = uint256(uint8(data[i - 4]));
41:         }
42: 
43:         // Send the converted data to L2
44:         starknetMessaging.sendMessageToL2{value: msg.value}(kakarotAddress, HANDLE_L1_MESSAGE_SELECTOR, convertedData);
45:     }
```

Address aliasing is an intentional alteration that is done to protect from the situation where a same address owned by an entity on one chain in L1 is owned by a different entity on L2.

This problem is well known and is mostly applicable to wallets created via insecure factories (i.e. using `CREATE` opcode or `CREATE2` with a salt that doesn't include the caller), and never applies to EOAs.

This is why, if we look at [the similar implementation of the L1 to L2 bridge in Optimism](https://github.com/ethereum-optimism/optimism/blob/83117192d4619313b088feb7717fe9e42e61cfa1/packages/contracts-bedrock/src/L1/OptimismPortal.sol#L574), we see that aliasing is applied only when the caller is not an EOA:

```solidity
        if (msg.sender != tx.origin) {
            from = AddressAliasHelper.applyL1ToL2Alias(msg.sender);
        }
```

Because aliasing is unnecessarily applied when `msg.sender` is an EOA too, users interacting from an EOA with the `L1KakarotMessaging` contract will be unable to make calls on their behalf on Kakarot via the bridge, and submitting transactions via the Kakarot RPC will be their only way to do so.

Additionally, they will be unable to use the same address from which their messages are executed on Kakarot to execute normal transactions.

## Proof of Concept
- Start with a private key to an EVM address "Alice"
- Fund Alice's account on L1 and L2
- Use Alice's private key to sign a request via `L1KakarotMessaging` that would normally succeed
- The request will fail on L2 because Alice's request was executed on a different account

## Recommended Mitigation Steps
Remove aliasing from EOAs, similarly to how it's done in the Optimism bridges.



## Assessed type
p256verify precompile charges new account access gas
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The Kakarot EVM correctly keeps precompile addresses warm, with the goal of charging reduced "warm address access" gas for precompile calls.

If we look at the code that achieves this warm-up action, we can see that the last cached precompile is `10`:

```cairo
File: state.cairo
134:     func cache_precompiles{
135:         syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, state: model.State*
136:     }() {
137:         alloc_locals;
138:         tempvar accounts_ptr = state.accounts;
139:         with accounts_ptr {
140:             Internals._cache_precompile(1);
141:             Internals._cache_precompile(2);
142:             Internals._cache_precompile(3);
143:             Internals._cache_precompile(4);
144:             Internals._cache_precompile(5);
145:             Internals._cache_precompile(6);
146:             Internals._cache_precompile(7);
147:             Internals._cache_precompile(8);
148:             Internals._cache_precompile(9);
149:             Internals._cache_precompile(10);
150:         }
```

From the above list, the `p256verify` precompile (address `0x100` or `256`) is missing:

```cairo
File: p256verify.cairo
17:     const PRECOMPILE_ADDRESS = 0x100;
18:     const GAS_COST_P256_VERIFY = 3450;
```

Because the `0x100` address is not cached, when the `p256verify` is called, the caller will be charged `2500` extra gas.

This is also the case for the Kakarot `cairo_precompile` and `cairo_message` precompiles, however we report this impact with lower severity because unlike with `p256verify` there is no formal specification for these precompiles.

## Proof of Concept
Calls to the `p256verify` precompile will cost `2500` more gas than defined in [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md), which specifies the gas costs of the precompile, and [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929), which dictates all precompile accesses to be warm.

This undocumented deviation from spec can lead to broken functionality in contracts that e.g. rely on the expected gas cost to ensure there is always sufficient gas to finish execution after an external call.

## Recommended Mitigation Steps
Add the `p256verify`, and possibly also the Kakarot precompiles to the `cache_precompiles` targets.



## Assessed type

Other