xanoutas/solidity-security-findings huggingface dataset:
Return value of `transfer` and `transferFrom` not checked in `DualVmToken` contract
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

As per the [ERC20 token behaviors in scope](https://github.com/code-423n4/2024-09-kakarot?tab=readme-ov-file#erc20-token-behaviors-in-scope), `Doesn't revert on failure	In scope` is a behavior that is in scope. This behavior is related to this issue.

The `transfer` and `transferFrom` functions in the `DualVmToken` contract do not check the return value of the function of the `ERC20` contract. This can lead to a situation where the `transfer` or `transferFrom` function of the `ERC20` contract ( on starknet ) fails, but the `transfer` or `transferFrom` function of the `DualVmToken` contract still returns `true`. This can lead to a situation where the `DualVmToken` contract is in an inconsistent state.

For reference, [`_transfer`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L256) function is as follows:

```solidity
function _transfer(uint256 to, uint256 amount) private {
    if (to >= STARKNET_FIELD_PRIME) {
        revert InvalidStarknetAddress();
    }
    // Split amount in [low, high]
    uint128 amountLow = uint128(amount);
    uint128 amountHigh = uint128(amount >> 128);

    uint256[] memory transferCallData = new uint256[](3);
    transferCallData[0] = to;
    transferCallData[1] = uint256(amountLow);
    transferCallData[2] = uint256(amountHigh);

    starknetToken.delegatecallCairo("transfer", transferCallData);
}
```

The [`transferFrom`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol#L339) function is as follows:

```solidity
function _transferFrom(uint256 from, uint256 to, uint256 amount) private {
    if (from >= STARKNET_FIELD_PRIME || to >= STARKNET_FIELD_PRIME) {
        revert InvalidStarknetAddress();
    }
    // Split amount in [low, high]
    uint128 amountLow = uint128(amount);
    uint128 amountHigh = uint128(amount >> 128);

    uint256[] memory transferFromCallData = new uint256[](4);
    transferFromCallData[0] = from;
    transferFromCallData[1] = to;
    transferFromCallData[2] = uint256(amountLow);
    transferFromCallData[3] = uint256(amountHigh);

    starknetToken.delegatecallCairo("transfer_from", transferFromCallData);
}
```

It can be seen that both these functions doesn't check the return value of the `starknetToken.delegatecallCairo` function.

## Proof of Concept

Consider the following scenario:
1. The `transfer` function of the `ERC20` contract ( on starknet ) doesn't revert but returns `false` when the transfer fails.
2. The `DualVmToken` contract calls the `transfer` function of the `
If evm reverted, user might avoid paying any EVM gas due to vulnerable fee implementation in interpreter::execute
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Proof of Concept
When evm reverts, in some cases sender can avoid paying EVM gas due to vulnerabilities in interpreter::execute.

Vulnerabilities: In interpreter::execute, (1) there is one instance where return status of State.add_transfer(transfer) is not checked; (2) when evm reverts, in the cached state, at one point user is charged (max_fee + actual_fee) due to calculation of `paid_fee_u256`;
```rust
//kakarot/src/kakarot/interpreter.cairo
    func execute{
...
    }(
        env: model.Environment*,
        address: model.Address*,
        is_deploy_tx: felt,
        bytecode_len: felt,
        bytecode: felt*,
        calldata_len: felt,
        calldata: felt*,
        value: Uint256*,
        gas_limit: felt,
        access_list_len: felt,
        access_list: felt*,
    ) -> (model.EVM*, model.Stack*, model.Memory*, model.State*, felt, felt) {
    ...
        let is_reverted = is_not_zero(evm.reverted);
        let success = 1 - is_reverted;
|>      let paid_fee_u256 = Uint256(max_fee_u256.low * success, max_fee_u256.high * success);

        with state {
            let sender = State.get_account(env.origin);
            uint256_add([sender.balance], paid_fee_u256);
            let (ap_val) = get_ap();

            let sender = Account.set_balance(sender, cast(ap_val - 3, Uint256*));

            let sender = Account.set_nonce(sender, sender.nonce + is_reverted);
            State.update_account(sender);
        }

        // 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);
           ...
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1035)

Case: max_fee < sender.balance < (max_fee + actual_fee) 
The vulnerable condition is the sender doesn't have enough balance to cover `max_fee + actual_fee` and EVM reverts. 

POC: 
Suppose sender's balance == max_fee and tx.amount == 0. sender invokes `eth_send_raw_unsigned_tx`:
1. Under the above condition, the sender's tx will pass `eth_send_raw_unsigned_tx` check of [sender.balance >= max_fee](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/eth_rpc.cairo#L299) at first.
2. In interpreter::execute, sender's cached balance will be first [deducted max_fee](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interp
Underconstrained output in `felt_to_bytes_little()` allows contract deployments to incorrect addresses
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

To explain this finding, we compare two utility functions that are very similar in their implementation: [`load_bytecode()`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/library.cairo#L486) in `kakarot/src/kakarot/accounts/library.cairo` and [`felt_to_bytes_little()`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/bytes.cairo#L46) in `kakarot/src/utils/bytes.cairo`. Both contain logic for un-packing up to 31 bytes from a `felt` into an array of single bytes.

If we isolate only the few interesting lines of the inner loop in `load_bytecode()`, we have the following:

```cairo
File: library.cairo
531:         tempvar count = BYTES_PER_FELT;
---
541:         body:
---
576:         tempvar value = (value - [output]) / base;
---
582:         tempvar count = count - 1;
---
593:         jmp body if count != 0;
---
594:         with_attr error_message("Value is not empty") {
595:             assert value = 0;
596:         }
```

The loop starts with `count = BYTES_PER_FELT` (which is `31`), then at every cycle `count` is decreased by `1`, and when it reaches the value `0`, the loop ends, finally asserting that `value = 0`.

This correctly ensures the `[output]` values provided by the prover are honest, because if `[output]` wasn't effectively containing the last byte of `value`, the field division at L576 would have increased `value` (instead of decreasing it) to a value that couldn't be brought down to `0` with at most `31` iterations of chopping off the least significant byte.

To summarize, `load_bytecode` is safe because:
1. `[output]` is checked to be between 0-255 values (code omitted for brevity)
2. no more than 31 iterations are allowed
3. `value` is checked to be `0` at the end of the loop.

Now, we compare this to the corresponding loop in `felt_to_bytes_little()`:

```cairo
File: bytes.cairo
57:     body:
---
70:     let byte = [output];
---
74:     tempvar value = (value - byte) / base;
---
78:     tempvar bytes_len = bytes_len + 1;
---
80:     jmp body if value != 0;
```

This time, the terminating condition is `value = 0`, where `value` is the input `felt` after the iterative field divisions. Also in this case, `value` can not only shrink, but also grow larger at L74 if the `[output]` provided by the prover is not honest.

If we compare with the three conditions that make `load_bytecode` safe, we see that the second condition `2. no more than 31 iterations are allowed` does not hold in this case, so the prover is effectively allowed to provide an arbitrary number of bytes, as long as these are followed by more bytes that bring `value` back to zero.

In other words, the lack of check that `bytes_len` is at most `31` leaves space for the prover to provide arbitrary bytes in output, making
Kakarot precompiles can be abused by malicious contracts by delegate-calling whitelisted contracts
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

Calls to Kakarot precompiles (`cairo_precompile` and `cairo_message`) are restricted by a whitelist that is validated with the following check:

```cairo
File: precompiles.cairo
134:         kakarot_precompile:
135:         let is_whitelisted = KakarotPrecompiles.is_caller_whitelisted(caller_code_address);
136:         tempvar is_not_authorized = 1 - is_whitelisted;
```

This validation uses `caller_code_address`, which represents the EVM address holding the bytecode that initiated the call to the precompile.

This means that if an EVM contract, which we can call `Legit`, is whitelisted, any `Other` contract can pass this same check as long as their calls to the Kakarot precompiles are made by `delgatecall`'ing the `Legit` contract. This is most concerning because the `calldata` passed to the `delegatecall` to `Legit` can be arbitrarily determined by the `Other` contract, while preserving the origin `msg.sender`.

This vulnerability impacts both the `L2KakarotMessaging` and the `DualVmToken` contracts with different impacts listed below.

- `DualVmToken`: When a user (`victim`) makes an EVM call to a malicious contract (`attack`), with any calldata, the `attack` contract can directly control on their behalf any quantity of any one of the ERC20 tokens held by the `victim`'s account contract, with the sole condition that the ERC20 has an authorized `DualVmToken` wrapper. This particular scenario is proved with the coded PoC below.
- `L2KakarotMessaging`: When `victim` makes an EVM call to an `attack` contract with any calldata, the `attack` contract can send messages to L1 with arbitrary target addresses and payloads; these messages appear as originated by `victim`'s EVM address.

It is worth mentioning that users calling malicious contracts is a relatively weak requirement, because any legitimate-looking contract can be upgraded to frontrun a user call, and this is especially easy to achieve because the `calldata` provided by the victim doesn't matter for the attack to work.

## Proof of Concept

A runnable PoC in the form of an end-to-end test can be found in [this GitHub Gist](https://gist.github.com/3docSec/c3fed197469c3f0174ac24e6fa0d0af2).

The PoC has:
- a victim calling an attack contract on its `hello()` function
- the attack contract exploiting this call to make an arbitrary call to `DualVmToken` and self-approve `1337` of the victim's tokens

## Recommended Mitigation Steps

Make sure that `DualVmToken`, `L2KakarotMessaging`, as well as any other future contract using Kakarot precompiles, make extensive use of [`noDelegateCall` modifiers](https://github.com/Uniswap/v3-core/blob/main/contracts/NoDelegateCall.sol).


## Assessed type

call/delegatecall
Wrong RIPEMD-160 hash when (input length & 63) is exactly 55
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ripemd160.cairo#L42-L83


# Vulnerability details

## Proof of Concept
### Root Cause
<details open>
<summary>
	<a href="https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ripemd160.cairo#L42-L83">RIPEMD-160</ahref>
</summary>
  
```cairo
    func run{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
    }(_address: felt, input_len: felt, input: felt*) -> (
        output_len: felt, output: felt*, gas_used: felt, reverted: felt
    ) {
        // ...

        // 3. finish hash
        let (res, _) = finish(res, rsize, new_msg, input_len, 0); <@udit
        // ...
    }
    
	func finish{range_check_ptr, bitwise_ptr: BitwiseBuiltin*}(
	    buf: felt*, bufsize: felt, data: felt*, dsize: felt, mswlen: felt
	) -> (res: felt*, rsize: felt) {
	    alloc_locals;
	    let (x) = default_dict_new(0); <@udit
	    tempvar start = x;
	
	    // put data into x.
	    let (local len) = uint32_and(dsize, 63); <@udit
	    absorb_data{dict_ptr=x}(data, len, 0);
	
	    // append the bit m_n == 1.
	    let (index_4, _) = unsigned_div_rem(dsize, 4);
	    let (local index) = uint32_and(index_4, 15);
	    let (old_val) = dict_read{dict_ptr=x}(index);
	    let (local ba_3) = uint32_and(dsize, 3);
	    let (factor) = uint32_add(8 * ba_3, 7);
	    let (tmp) = pow2(factor);
	    let (local val) = uint32_xor(old_val, tmp);
	    dict_write{dict_ptr=x}(index, val);
	
	    // length goes to next block.
	    let (val) = uint32_mul(dsize, 8);
	    let (pow2_29) = pow2(29);
	    let (factor, _) = unsigned_div_rem(dsize, pow2_29);
	    let len_8 = mswlen * 8;
	    let (val_15) = uint32_or(factor, len_8);
	
	    let next_block = is_nn_le(55, len); <@udit
	    if (next_block == FALSE) {
	        dict_write{dict_ptr=x}(14, val);
	        dict_write{dict_ptr=x}(15, val_15);
	
	        let (local arr_x: felt*) = alloc();
	        dict_to_array{dict_ptr=x}(arr_x, 16);
	        default_dict_finalize(start, x, 0);
	        let (res, rsize) = compress(buf, bufsize, arr_x, 16);
	        return (res=res, rsize=rsize);
	    }
	    let (local arr_x: felt*) = alloc();
	    dict_to_array{dict_ptr=x}(arr_x, 16);
	    let (buf, bufsize) = compress(buf, bufsize, arr_x, 16);
	    // reset dict to all 0.
	    let (x) = default_dict_new(0);
	    tempvar start = x; <@udit ADDED TO FIX ANOTHER ISSUE WITH RIPEMD-160
                                                 
	    dict_write{dict_ptr=x}(14, val);
	    dict_write{dict_ptr=x}(15, val_15);
	
	    let (local arr_x: felt*) = alloc();
	    dict_to_array{dict_ptr=x}(arr_x, 16);
	    default_dict_finalize(start, x, 0);
	    let (res, rsize) = compress(buf, bufsize, arr_x, 16);
	    return (res=res, rsize=rsize);
	}
```
</details> 

When `input_length & 63 == 55`, the `finish` function executes logic for
Nonce will be set incorrectly due to implementation error
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Proof of Concept
The issue is `interpreter::execute` will incorrectly set nonce post evm run due to an error in implementation.

In interpreter::exexcute, after run(EVM) , cached states including account states are set. When setting nonce, sender.nonce + is_reverted as the new nonce value. This is incorrect.

`is_reverted` denotes whether the EVM reverted. If EVM reverted ⇒ is_reverted == 1, cached state is initialized (`tempvar state = State.init()`;). The subsequent call `let sender = State.get_account(env.origin)` will pull sender’s state from account storage with the sender’s current nonce (before current tx).

Case success: (is_reverted ==0)
Sender’s nonce will be (sender.nonce + 0). Sender’s nonce will not be changed even though tx succeeds.

Case revert: (is_reverted ==1)
Sender’s nonce will be (sender.nonce + 1). Sender’s nonce will be increased by 1 even though tx reverts. 

In either case sender’s nonce will be incorrectly set.
```rust
//kakarot/src/kakarot/interpreter.cairo

    func execute{
...
    }(
        env: model.Environment*,
        address: model.Address*,
        is_deploy_tx: felt,
        bytecode_len: felt,
        bytecode: felt*,
        calldata_len: felt,
        calldata: felt*,
        value: Uint256*,
        gas_limit: felt,
        access_list_len: felt,
        access_list: felt*,
    ) -> (model.EVM*, model.Stack*, model.Memory*, model.State*, felt, felt) {
...
        if (evm.reverted != 0) {
            with_attr error_message(
                    "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles") {
                assert evm.message.cairo_precompile_called = FALSE;
            }
            tempvar state = State.init();
        } else {
            tempvar state = state;
        }
...
        let is_reverted = is_not_zero(evm.reverted);
        let success = 1 - is_reverted;
...
        with state {
            let sender = State.get_account(env.origin);
            uint256_add([sender.balance], paid_fee_u256);
            let (ap_val) = get_ap();
            let sender = Account.set_balance(sender, cast(ap_val - 3, Uint256*));
 |>         let sender = Account.set_nonce(sender, sender.nonce + is_reverted);
            State.update_account(sender);
        }
...
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1024)

Impact:

interpreter::execute is called in all entry_points, this means sender’s nonce will be incorrectly set after a tx execution whether or not the tx succeeds. In addition, this might enable transaction replay attacks. 

## Recommended Mitigation Steps
Change into let sender = Account.set_nonce(sender, sender.nonce + success);







## Assessed type

Error
Loops make CairoVM run out of steps
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
This leads to the Starknet transaction reverting and can DoS contracts relying on loops.

Additionally, right now, it can be used to drain Kakarot Relayers by sending multiple transactions and making them pay the Starknet fees without any counterpart cost.
## Proof of Concept
<details>
<summary>Solidity</summary>

Deploy the following contract and call run, for me this issue happens with 99 iterations :
```solidity
contract Loop {
    function run(uint256 loopFor) public {
        for (uint256 i; i < loopFor; i++) {}
    }
}
```
  
</details>

<details>
<summary>Bytecode</summary>

Deploy the following bytecode :
`5b5f56`
  
</details>

<details>
<summary>Transaction Receipt</summary>

```json
{
	// ...
	"execution_status": "REVERTED",
	"revert_reason": "Transaction execution has failed:\n0: Error in the called contract (contract address: 0x00e29882a1fcba1e7e10cad46212257fea5c752a4f9b1b1ec683c503a2cf5c8a, class hash: 0x05400e90f7e0ae78bd02c77cd75527280470e2fe19c54970dd79dc37a9d3645c, selector: 0x015d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad):\nError at pc=0:4573:\nCairo traceback (most recent call last):\nUnknown location (pc=0:67)\nUnknown location (pc=0:1835)\nUnknown location (pc=0:2478)\nUnknown location (pc=0:3255)\nUnknown location (pc=0:3795)\n\n1: Error in the called contract (contract address: 0x000f5a977c35e283d790be1ea2f212926f20f0ae8199da774fa625dc65065746, class hash: 0x07b2de5e73ff7eb338d76c967dd5aa3f3004574d326b8c1402bb819d4983b8b6, selector: 0x007ec457cd7ed1630225a8328f826a29a327b19486f6b2882b4176545ebdbe3d):\nError at pc=0:22:\nCairo traceback (most recent call last):\nUnknown location (pc=0:273)\nUnknown location (pc=0:259)\n\n2: Error in a library call (contract address: 0x000f5a977c35e283d790be1ea2f212926f20f0ae8199da774fa625dc65065746, class hash: 0x01c8acbb634afbe8e494faba9ca2d8c6ad385372d5bfdd32a88506fffc709707, selector: 0x007ec457cd7ed1630225a8328f826a29a327b19486f6b2882b4176545ebdbe3d):\nError at pc=0:32:\nCairo traceback (most recent call last):\nUnknown location (pc=0:3247)\nUnknown location (pc=0:3180)\nUnknown location (pc=0:2614)\nUnknown location (pc=0:445)\n\n3: Error in the called contract (contract address: 0x077a903e264f73adf8536559cb7886a94aed4a65220018ecf3205b3c7926250f, class hash: 0x02c303dbbde90379442214325407a67c7a0f77c50020aaa7c1d7eb9b94583b4f, selector: 0x01df22f179266780b29ce26485abf97cbd909fd15ce74a37b793bcd261f84b14):\nError at pc=0:6077:\nCairo traceback (most recent call last):\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:23536)\nUnknown location (pc=0:2