xanoutas/solidity-security-findings huggingface dataset:
The Interpreter.execute function does not judge the return value of State.add_transfer
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

When the user's balance is low, it can avoid paying the gas fee.

## Proof of Concept

The `Interpreter.execute` function determines the return value of `add_transfer` when handling `msg.value`:

```rust
    func execute{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
    }
        ......
        with state {
        ......
        let transfer = model.Transfer(sender.address, address, [value]);
@>      let success = State.add_transfer(transfer);
        }
        ......
        if (success == 0) {
            let (revert_reason_len, revert_reason) = Errors.balanceError();
            tempvar evm = EVM.stop(evm, revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT);
        } else {
            tempvar evm = evm;
        }
     }
```

However, the return value of `add_transfer` was not judged when the `gas fee` was processed:

```rust
        // 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();
        }
```

If the `add_transfer` function fails to execute, such as the account balance is insufficient,`add_transfer` will return 0, and no transfer operation will be performed.

```rust
   func add_transfer{
        syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, state: model.State*
    }(transfer: model.Transfer) -> felt {
        alloc_locals;
        // See https://docs.cairo-lang.org/0.12.0/how_cairo_works/functions.html#retrieving-registers
        let fp_and_pc = get_fp_and_pc();
        local __fp__: felt* = fp_and_pc.fp_val;

        if (transfer.sender.evm == transfer.recipient.evm) {
            return 1;
        }

        let (null_transfer) = uint256_eq(transfer.amount, Uint256(0, 0));
        if (null_transfer != 0) {
            return 1;
        }

        let sender = get_account(transfer.sender.evm);
        let (success) = uint256_le(transfer.amount, [sender.balance]);

        if (success == 0) {
@>           return success;
        }
        .....
    }

```

Therefore, when the account balance is insufficient, the user can not pay any gas.

## Tools Used
vscode

## Recommended Mitigation Steps

```diff
        with state {
-            State.add_transfer(transfer);
+            let success = State.add_transfer(transfer);
            State.finalize();
        }

+        if (success == 0) {
+            let (revert_reason_len, revert_reason) = Errors.balanceError();
+
Missing balance check in handle_l1_message leads to underflow,  inflating the balance of the transaction sender
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L362-L385
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/library.cairo#L421-L439
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L941-L950


# Vulnerability details

The `execute()` function in `interpreter.cairo` subtracts the maximum gas fee from the caller's balance without checking whether the caller's balance is sufficient. As explained in the developer comments, the balance check is omitted because `balance > effective_fee ` has already been verified in `eth_send_raw_unsigned_tx()`. There is however another entry point, `handle_l1_message()`, that doesn't have the check. As a consequence, the subtraction will underflow if the sender of the l1 message has insufficient funds to cover `max_fee`.

This will leave the caller account with an inflated balance for the entire transaction. Calls to the `balance` or `selfbalance` opcodes will return the inflated balance. The transaction will be valid as long as the sender does not transfer more than their real balance - otherwise, the transfer of the underlying Starknet token will fail and the Starknet transaction (message from L1) will revert. The impact of this issue is therefore limited to spoofing the account balance, impacting any logic that depends on balance checks.

Also, note that the caller account needs to have enough actual funds (in underlying Starknet tokens) to cover the final gas fees.

## Proof of Concept

The following test demonstrates the issue.

```
class TestL1Handler:
    @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1])
    def test_balance_underflow_in_l1_handler(self, cairo_run):
        res = cairo_run(
            "test__handle_l1_message",
            from_address=0xABC,
            l1_sender=0xABC,
            to_address=0xABC,
            value=0xABC,
            data=[],
        )

        assert int(res['accounts']['0x0000000000000000000000000000000000000aBc']['balance'], 16) == 0xffffffffffffffffffffffffffffadf8
```

```
from kakarot.model import model
from kakarot.library import Kakarot

func test__handle_l1_message{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*}()
-> model.State* {
    tempvar from_address;
    tempvar l1_sender;
    tempvar to_address;
    tempvar value;
    tempvar data_len;
    let (data) = alloc();

    %{
        ids.l1_sender = program_input["l1_sender"]
        ids.to_address = program_input["to_address"]
        ids.value = program_input["value"]
        ids.data_len = len(program_input["data"])
        segments.write_arg(ids.data, list(program_input["data"]))
    %}

    let (evm, state, gas_used, required_gas) = Kakarot.handle_l1_message(l1_sender, to_address, value, data_len, data);

    return state;
}
```

## Recommended Mitigation Steps
Malicious Starknet contract entered via Cairo precompile can replay legitimate transactions
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The Cairo precompile eventually calls `execute_starknet_call()` in the account contract associated with the calling L2 smart contract. This function has a reentrancy check that prevents callers from re-entering the Kakarot smart contract. It is however possible to call `execute_from_outside()` in any account contract. A malicious Starknet contract called via precompile can leverage this to replay any L2 transactions that invoke it. For example, a transaction that transfers native tokens could be replayed multiple times.

The attack is mitigated by the fact that Cairo precompile callers must be whitelisted. 

## Proof of Concept
Since the impact of this attack is limited and the execution rather complex I am providing a sketch of the attack rather than a full proof-of-concept.

1. The attacker observes the incoming transaction in the mempool (possibly they control a sequencer). They store the entire transaction data including the signature in the malicious Starknet contract.
2. Once the Starknet transaction is included and the attacker's precompile is executed, the attacker the precompile calls into the malicious Cairo contract which re-submits the previously stored transaction via the `execute_from_outside` function in the sender's account contract. Note that the execution will still see the previous nonce since the outer execution context has not yet committed the updated nonce to storage.
4. Repeat the process multiple times as required.
5. All nested transactions should run successfully and the nonce (increased by 1) is committed to the sender account.

## Recommended Mitigation Steps

Add a check to `execute_starknet_call()` in `account_contract.cairo` that prevents the selector for `execute_from_outside()` from being called.






## Assessed type

call/delegatecall
RIPEMD160 precompile crashes with a Cairo exception for some input lengths
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Calling the RIPEMD160 precompile with certain input lengths results in a Cairo exception. As a result, some L2 smart contracts that use this precompile cannot be executed.

The bug seems to occur in line 477 of the precompile. The relevant code:

```
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);
    tempvar start = x;

    (...)
    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);

    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);
```

The lower part of the code is reached for some input lengths. Note that `x` is redefined with the line:

```
    let (x) = default_dict_new(0);
```

However, `start` still points to the first element of the original dict `x` initialized at the start of the function. Consequently, squashing the dict with `default_dict_finalize(start, x, 0)` will fail because start and x point to different segments.

## Proof of Concept

The issue can be verified by running the following Solidity contract as an e2e-test.:

```
contract RIPEMDTest {

    function computeRipemd160(bytes memory data) public view returns (bytes20) {
        bytes20 result;
        assembly {
            // Load the free memory pointer
            let ptr := mload(0x40)

            // Get the length of the input data
            let len := mload(data)

            // Call the RIPEMD-160 precompile (address 0x03)
            let success := staticcall(
                gas(),           // Forward all available gas
                0x03,            // Address of RIPEMD-160 precompile
                add(data, 32),   // Input data starts after the length field (32 bytes)
                len,             // Length of the input data
                ptr,             // Output will be written to ptr
                32               // The precompile writes 32 bytes (even though RIPEMD-160 outputs 20 bytes)
            )

            // Check if the call was successful
            if iszero(success) {
                revert(0, 0)
            }

            // Read the first 20 bytes of the output (RIPEMD-160 outputs 20 bytes)
            result := mload(ptr)
        }
        return result;
    }

    function hashInputLength20() external view returns (bytes20) {
        bytes memory inputData = new bytes(20);

        for (uint256 i = 0;
Address aliasing should not applied to EOA account
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

## Proof of Concept

https://docs.optimism.io/chain/differences#address-aliasing

> When transactions are sent from L1 to L2 by an Externally Owned Account, the address of the sender of the transaction on L2 will be set to the address of the sender of the transaction on L1. However, the address of the sender of a transaction on L2 will be different if the transaction was triggered by a smart contract on L1.

> Because of the behavior of the CREATE opcode, it is possible to create a contract on both L1 and on L2 that share the same address but have different bytecode. Even though these contracts share the same address, they are fundamentally two different smart contracts and cannot be treated as the same contract. As a result, the sender of a transaction sent from L1 to L2 by a smart contract cannot be the address of the smart contract on L1 or the smart contract on L1 could act as if it were the smart contract on L2 (because the two contracts share the same address).

> To prevent this sort of impersonation, the sender of a transaction is slightly modified when a transaction is sent from L1 to L2 by a smart contract. Instead of appearing to be sent from the actual L1 contract address, the L2 transaction appears to be sent from an "aliased" version of the L1 contract address.

The documentation explains why the address aliasing is needed clearly.

But on the contrary, the address aliasing should not be applied to EOA account in l1.

```solidity
    function sendMessageToL2(address to, uint248 value, bytes calldata data) external payable {
        uint256 totalLength = data.length + 4;
        uint256[] memory convertedData = new uint256[](totalLength);
        convertedData[0] = uint256(uint160(AddressAliasHelper.applyL1ToL2Alias(msg.sender)));
        convertedData[1] = uint256(uint160(to));
        convertedData[2] = uint256(value);
        convertedData[3] = data.length;
        for (uint256 i = 4; i < totalLength; ++i) {
            convertedData[i] = uint256(uint8(data[i - 4]));
        }

        // Send the converted data to L2
        starknetMessaging.sendMessageToL2{value: msg.value}(kakarotAddress, HANDLE_L1_MESSAGE_SELECTOR, convertedData);
    }
```

the code always apply address aliasing no matter the msg.sender is EOA account or smart contract.

```solidity
uint256(uint160(AddressAliasHelper.applyL1ToL2Alias(msg.sender)));
```

Applying alias to EOA account means in l2, the application can never determine what is the true sender of the msg.sender in L1.

Also, the user can send some fund to the same address in l2, expecting the same address receive the fund, yet because the address alias is applied, the fund is sent to l1 address + 0x1111000000000000000000000000000000001111 address

## Recommended Mitigation Steps

```solidity
if(address(msg.sen
Malicious user can revoke native ERC20 token allowance to trigger unhandled panic when the code commits the state.
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L264
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/library.cairo#L101


# Vulnerability details

## Description

```solidity
namespace Starknet {
    // @notice Commit the current state to the underlying data backend (here, Starknet)
    // @param self The pointer to the State
    func commit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
        self: model.State*
    ) {
        alloc_locals;
        let (native_token_address) = Kakarot_native_token_address.read();

        // Accounts
        Internals._commit_accounts{state=self}(
            self.accounts_start, self.accounts, native_token_address
        );

        // Events
        Internals._emit_events(self.events_len, self.events);

        // Transfers
        Internals._transfer_eth(native_token_address, self.transfers_len, self.transfers);

        return ();
    }
```

When backend commit the state, the code [performs token transfer](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L264).

```solidity
   func _transfer_eth{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
        token_address: felt, transfers_len: felt, transfers: model.Transfer*
    ) {
        if (transfers_len == 0) {
            return ();
        }

        let transfer = [transfers];
        IERC20.transferFrom(
            token_address, transfer.sender.starknet, transfer.recipient.starknet, transfer.amount
        );
        return _transfer_eth(token_address, transfers_len - 1, transfers + model.Transfer.SIZE);
    }

```

This implementation assume that the ERC20 token transfer always succeed.

```solidity
        IERC20.transferFrom(
            token_address, transfer.sender.starknet, transfer.recipient.starknet, transfer.amount
        );
```

while when creating the account,

the code give [infinite approval](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/library.cairo#L101)
to kakarot address.

```solidity
    func initialize{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
    }(evm_address: felt) {
        alloc_locals;
        let (is_initialized) = Account_is_initialized.read();
        with_attr error_message("Account already initialized") {
            assert is_initialized = 0;
        }
        Account_is_initialized.write(1);
        Account_evm_address.write(evm_address);

        // Give infinite ETH transfer allowance to Kakarot
        let (kakarot_address) = Ownable_owner.read();
        let (native_token_address) = IKakarot.get_native_token(kakarot_address);
        let infinite = Uint256(Constants.UINT128_MAX, Constants.UINT128_MAX);
>        IERC20.approve(native_token_addr
Unchecked External Call Success in execute_starknet_call and exec_precompile May Lead to Unexpected Behavior and Reentrancy Attacks
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interfaces/interfaces.cairo#L75-L78
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo_zero/kakarot/accounts/account_contract.cairo#L333-L349
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo_zero/kakarot/precompiles/kakarot_precompiles.cairo#L243
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo/kakarot-ssj/crates/contracts/src/account_contract.cairo#L41
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/tests/utils/syscall_handler.py#L208
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo_zero/tests/src/kakarot/accounts/test_account_contract.py#L277
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo/kakarot-ssj/crates/contracts/tests/test_account_contract.cairo#L101
https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo_zero/tests/src/kakarot/accounts/test_account_contract.cairo#L18


# Vulnerability details

Proof of Concept
Scenario 1: External Call Fails Silently
In the function execute_starknet_call, an external contract call is made via the provided address (to). The function includes a success return value, but this return value is not checked to ensure the external call was successful. Without proper validation, if the call fails, the contract continues execution, potentially leading to an incorrect or inconsistent state.
Code Example:
func execute_starknet_call(
    to: felt,
    function_selector: felt,
    calldata_len: felt,
    calldata: felt*
) -> (retdata_len: felt, retdata: felt*, success: felt) {
The function does not check whether success == 1 after the external call is made. If success == 0 (indicating a failure), the contract logic will proceed as if the call succeeded, which can lead to unexpected behavior.

Proof of Impact:
If the external call fails but this is not handled, the state changes that depend on the success of the call could be incorrect. This could also allow external entities to exploit the failure by continuing actions that depend on the success of the call, leading to the possibility of draining contract funds, incorrect state transitions, or other unintended consequences.

Scenario 2: Reentrancy Attack
In the exec_precompile function, an external contract is invoked with the possibility of passing arbitrary data (data_len, data). If the external contract is malicious, it can trigger a reentrancy attack by calling back into the original contract before the original function finishes execution. Since there are no reentrancy guards in place, this could allow the attacker to exploit the contract by draining funds or manipulating state.
Code Example:

func exec_precompile(
    address: felt,
    data_len: felt,
    data: felt*
) -> (success: felt, g
Kakarot's cross chain messages can't get cancelled
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/b8f5f2a20bd733cc8885c010291885e1df7dc50e/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol#L8


# Vulnerability details


## Proof of Concept

During the contest the new bridging scope was added, via which one can send/receive message to/fro Kakarot with both of `L1KakarotMessaging.sol` & `L2KakarotMessaging.sol`

This contracts integrate with the `StarknetMessaging.sol` contracts, for e.g when trying to send a message to a contract on L2, the below is used:

https://github.com/kkrt-labs/kakarot/blob/b8f5f2a20bd733cc8885c010291885e1df7dc50e/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol#L8

```solidity
    function sendMessageToL2(address to, uint248 value, bytes calldata data) external payable {
        uint256 totalLength = data.length + 4;
        uint256[] memory convertedData = new uint256[](totalLength);
        convertedData[0] = uint256(uint160(AddressAliasHelper.applyL1ToL2Alias(msg.sender)));
        convertedData[1] = uint256(uint160(to));
        convertedData[2] = uint256(value);
        convertedData[3] = data.length;
        for (uint256 i = 4; i < totalLength; ++i) {
            convertedData[i] = uint256(uint8(data[i - 4]));
        }

        // Send the converted data to L2
        starknetMessaging.sendMessageToL2{value: msg.value}(kakarotAddress, HANDLE_L1_MESSAGE_SELECTOR, convertedData);
    }
```

Which evidently calls on the `starknetMessaging` contract to send this message:

https://github.com/kkrt-labs/kakarot/blob/b8f5f2a20bd733cc8885c010291885e1df7dc50e/solidity_contracts/src/Starknet/StarknetMessaging.sol#L126

```solidity
    function sendMessageToL2(
        uint256 toAddress,
        uint256 selector,
        uint256[] calldata payload
    ) external payable override returns (bytes32, uint256) {
        require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0");
        require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED");
        uint256 nonce = l1ToL2MessageNonce();
        NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1);
        emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value);
        bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce);
        // Note that the inclusion of the unique nonce in the message hash implies that
        // l1ToL2Messages()[msgHash] was not accessed before.
        l1ToL2Messages()[msgHash] = msg.value + 1;
        return (msgHash, nonce);
    }
```

The same case is also applicable for when we are consuming messages from the L2, or even sending messages to the L1.

Issue however is that whereas the cancellation logic is present in the StarknetMessaging contract, both the L1 and L2 messaging contracts of Kakarot do not integrate this to handle cancellations, which then means that messages that have been sent can not be cancelled, which in essence breaks bridging logic, considering a user might have attempted to send some funds