xanoutas/solidity-security-findings huggingface dataset:
Kakarot's write_account_bytecode external function leaves stale valid jumpdests
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

Kakarot has an external, admin only utility function, `write_account_bytecode`, which serves the purpose of allowing admins to arbitrarily update the EVM bytecode of any address:

```cairo
File: kakarot.cairo
298: @external
299: func write_account_bytecode{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
300:     evm_address: felt, bytecode_len: felt, bytecode: felt*
301: ) {
302:     Ownable.assert_only_owner();
303:     return Kakarot.write_account_bytecode(evm_address, bytecode_len, bytecode);
304: }
```

If we explore the full execution flow of this function, we see that it correctly flows into a call to the `write_bytecode` function of the `account_contract.cairo` contract.

However, this execution flow lacks a call to the related `write_jumpdests` function of the account contract.

Because there is no way for admins to also update the contract's jumpdests, any call to `write_account_bytecode` will cause the target contract to have its bytecode misaligned with its jumpdests. Consequently, the new bytecode will most likely be unusable.

## Proof of Concept
- Deploy an EVM contract normally
- Update its bytecode using the `write_account_bytecode` admin function
- Call the EVM contract, execution will revert

## Recommended Mitigation Steps
Consider updating the `write_account_bytecode` admin function to also compute and push the valid jumpdests corresponding to the new bytecode.


## Assessed type

Other
L1 bridge messages can be lost due to unnecessary gas fee collection on Kakarot
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The [`kakarot.handle_l1_message()`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L370) function serves as the L1 handler of messages sent from Ethereum via the Kakarot bridge as implemented in the [`L1KakarotMessaging`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol) contract, which uses the Starknet bridge underneath. This allows users to send messages from Ethereum directly to Kakarot, which executes the message in the Kakarot EVM on Starknet.

However, when processing these L1-originated messages, Kakarot charges a gas fee on L2 by setting a gas price of 1 in the call to `eth_call()` within [`Kakarot.library.handle_l1_message()`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/library.cairo#L421). This fee is charged to the Kakarot-aliased address of the L1 sender:

```cairo
	return eth_call(
		0, l1_sender, to, 2100000000, 1, value_u256, data_len, data, 0, access_list
	);        
	          ^                       ^
	aliased sender address        gas_price
```

This means messages will fail if the sender's aliased address does not have sufficient funds on L2 to cover the gas costs, even though the actual execution costs are already paid for on L1 through the Starknet bridge. Failed messages due to insufficient L2 gas balance are irrecoverably lost.

## Impact
- Messages from L1 can be permanently lost if the sender's aliased address lacks funds on L2
- Users need to pre-fund their L2 addresses unnecessarily, creating friction
- Protocol integrations relying on the bridge can be effectively DoS'd by depleting the gas balance of target addresses

## Proof of Concept
Consider the following scenario:

1. An ERC-20 bridge contract is deployed on L1 that uses Kakarot's bridge to mint tokens on Kakarot
2. The bridge's L1 contract's aliased address on Kakarot (BRIDGE_ALIAS) contains enough funds to pay for receiving 1,000 messages
3. Attacker sends 1,000 minimum deposits of 1 token each through the bridge
5. After the attack, BRIDGE_ALIAS has 0 gas remaining
6. Alice deposits 1,000 USDC through the bridge on L1
7. The L1 transaction succeeds and her funds are locked in the bridge
8. The corresponding L2 message to mint her tokens fails due to insufficient gas
9. Alice's funds are permanently locked as the bridge has no recovery mechanism

Total cost to attacker: bridge fees for sending 1,000 messages + gas costs + dust
Impact: All user deposits after the attack result in permanently locked funds

## Recommended Mitigation Steps
Remove the gas fee charging mechanism for L1-originated messages since:
1. Execution costs are already paid on L1
2. Unlike regular transactions, Kakarot does not need
Users can permanently lose funds when L1->L2 messages fail to process
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The [`L1KakarotMessaging`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/L1L2Messaging/L1KakarotMessaging.sol) contract serves as a bridge between Ethereum (L1) and Kakarot (on Starknet), allowing users to send messages to Kakarot through the `sendMessageToL2()` function, which calls the function of the same name on the Starknet messaging contract. These functions requires users to provide ETH as `msg.value` to cover the L2 processing fees.

However, there are scenarios where L1->L2 messages can fail to be processed on L2:

1. If the provided `msg.value` becomes insufficient due to gas price spikes on L2 ([ref](https://github.com/crytic/building-secure-contracts/tree/master/not-so-smart-contracts/cairo/l1_to_l2_message_failure#l1-to-l2-message-failure))
2. There are multiple cases in which the transaction delivering the message can revert at the RPC level, e.g.:
	1. if the Kakarot contract is [paused](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/kakarot.cairo#L374)
	2. when the message or part of its execution reverts, and there was a call to a cairo precompile, Kakarot [forces a revert](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1009) at the Cairo level

We will assume the scenarios under 2. also result in the message permanently failing to be delivered, as we found no documentation regarding the retrying of messages that reverted once. If this is not the case, the impact holds for scenario 1.

In these cases, the message will remain unprocessed and any value they contain (such as cross-chain token transfers via a bridge built on top of the Kakarot bridge) will be lost, as `L1KakarotMessaging` does not implement message cancellation functionality using the  `startL1ToL2MessageCancellation()` and `cancelL1ToL2Message()` [methods](https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/#l2-l1_message_cancellation) of the Starknet messaging contract.

## Impact
Users have no recourse to recover their funds if their cross-chain messages fail to process on L2, effectively leading to permanent loss of user funds.

## Proof of Concept
Consider the following scenario:

* Alice wants to bridge 1000 USDC from L1 to L2 using a token bridge built on top of the Kakarot bridge
* Alice approves and deposits her USDC to the L1 bridge contract
* The L1 bridge contract locks Alice's USDC and calls `sendMessageToL2()`
* The L2 Kakarot contract gets paused due to a security concern
* The message cannot be processed, leaving Alice's USDC locked in L1
* No wrapped USDC is minted on L2
* The bridge has no knowledge of the state of the message containing the mint transa
Account contract does not gracefully handle panics in called contracts
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The [`DualVmToken`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol) is a utility EVM contract allowing Kakarot EVM accounts to operate Starknet ERC-20s via classic `balanceOf()`, `transfer()` etc. operations.

Calls to the `DualVmToken` contract are routed via [`CairoLib`](https://github.com/kkrt-labs/kakarot-lib/blob/c2c7cb400f85c3699a6902946bcf4428d5b4fc61/src/CairoLib.sol) -> [`kakarot_precompiles.cairo`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/kakarot_precompiles.cairo#L100) -> [`account_contract.cairo`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/accounts/account_contract.cairo#L347), which finally makes a call to the [`call_contract()` Cairo 0 syscall](https://docs.starknet.io/documentation/architecture_and_concepts/Smart_Contracts/system-calls/#call_contract) to invoke the appropriate Starknet ERC-20 contract.

If we look at how the `execute_starknet_call()` function in `account_countract.cairo` is implemented:
```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);
```

we see that the function admits graceful failures (by returning a `success: felt` value that can be `FALSE`), which are then properly handled in the calling precompile. However, the actual call to the target contract (at L347) does not return a `success` boolean, and instead uses the `call_contract` syscall which panics on failures.

This means that any call that panics in the called contract can cause a revert at RPC level.

To illustrate how this is not a hypothetical scenario, but a very likely one that can be achieved also with the DualVmToken contract that is in scope and consequently planned to be whitelisted to make this call, we make a simple example:

> an EVM contract uses DualVmToken to transfer
`DualVmToken` can be abused to cause RPC-level reverts by revoking native token approval to Kakarot
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

The [`DualVmToken`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/solidity_contracts/src/CairoPrecompiles/DualVmToken.sol) is a utility EVM contract allowing Kakarot EVM accounts to operate Starknet ERC-20s via classic `balanceOf()`, `transfer()` etc. operations.

These classic operations are offered in two flavors, one that accepts Starknet addresses (`uint256`) and one that accepts EVM addresses (`address`). In case an EVM address is provided, the corresponding Starknet address is looked up through Kakarot first.

If we look at the `approve(uint256, ...)` function in DualVmToken:
```Solidity
File: DualVmToken.sol
206:     /// @dev Approve a starknet address for a specific amount
207:     /// @param spender The starknet address to approve
208:     /// @param amount The amount of tokens to approve
209:     /// @return True if the approval was successful
210:     function approve(uint256 spender, uint256 amount) external returns (bool) {
211:         _approve(spender, amount);
212:         emit Approval(msg.sender, spender, amount);
213:         return true;
214:     }
215: 
216:     function _approve(uint256 spender, uint256 amount) private {
217:         if (spender >= STARKNET_FIELD_PRIME) {
218:             revert InvalidStarknetAddress();
219:         }
220:         // Split amount in [low, high]
221:         uint128 amountLow = uint128(amount);
222:         uint128 amountHigh = uint128(amount >> 128);
223:         uint256[] memory approveCallData = new uint256[](3);
224:         approveCallData[0] = spender;
225:         approveCallData[1] = uint256(amountLow);
226:         approveCallData[2] = uint256(amountHigh);
227: 
228:         starknetToken.delegatecallCairo("approve", approveCallData);
229:     }
```

we can see that no check whatsoever is done on the `spender` input, except for felt overflow at L217.

This means that the provided `address` could be any Starknet account, including a contract that is not an EVM account, and most importantly including the Kakarot contract.

This is particularly relevant, because native token transfers in the Kakarot EVM work under the assumption that Starknet account contracts have infinite approval granted to the Kakarot contract, as we can see from the `account_contract` initialization:

```cairo
File: library.cairo
083:     func initialize{
084:         syscall_ptr: felt*,
085:         pedersen_ptr: HashBuiltin*,
086:         range_check_ptr,
087:         bitwise_ptr: BitwiseBuiltin*,
088:     }(evm_address: felt) {
---
101:         IERC20.approve(native_token_address, kakarot_address, infinite);
```

By removing this approval and attempting to move native tokens, an EVM contract account can jeopardize EVM state finalization (that is where native token transfers are settled on t
In handle_l1_message flow, L1 sender with 0 ETH balance might either avoid paying EVM gas, or causing unhandled EVM reverts
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept
handle_l1_message is called in l1 to l2 message flow and it will be invoked by sequencer or starkent os due to (`@l1hander`) tag. 

In handle_l1_message flow, L1 sender is expected to have some L2 ETH balance([2100000000 x 1](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/library.cairo#L436)) to pay for kakarot EVM gas.

However, there are vulnerabilities in interpreter::execute which allows either L1 sender to avoid paying any EVM gas or causing tx to panic. 

Vulnerabilities: (1) handle_l1_message -> interpreter::execute has [no check on whether l1sender.balance >= max_fee](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/library.cairo#L429-L433) (2) In interpreter::execute,  [uint256_sub([sender.balance], max_fee_u256)](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/uint256.cairo#L92) allows underflow which inflates l1sender's cached balance (3) In interpreter::execute, there is one instance the return status of [State.add_transfer(transfer) is not checked](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L1035).
```rust

    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 max_fee = gas_limit * env.gas_price;
        let (fee_high, fee_low) = split_felt(max_fee);
        let max_fee_u256 = Uint256(low=fee_low, high=fee_high);

        with state {
            let sender = State.get_account(env.origin);
|>          let (local new_balance) = uint256_sub([sender.balance], max_fee_u256);
            let sender = Account.set_balance(sender, &new_balance);
...
        with state {
|>           State.add_transfer(transfer);
            State.finalize();
        }
...
```
(https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L950)
POC: 
Suppose L1sender invokes L1 to l2 message with 0 L2 eth balance. L1sender paid enough starknet fees on L1 so `handle_l1_message` is invoked by starknet:
1. No check on l1sender's balance until first accessed in interpreter::execute -> `let (local new_balance) = uint256_sub([sender.balance], max_fee_u256)`. uint256_sub underflows, `new_balance` is a very large uint256 number.
2. large `new_ba
CALL_STIPEND is injected in gas left directly when a call has value
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L820-L1040
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L339-L466
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L820-L1040


# Vulnerability details

## Impact
This leads to wrong estimates as the gas required ends up being lower than what's really needed.
## Proof of Concept
<details>
<summary>
	<a href="https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/interpreter.cairo#L820-L1040">Interpreter</ahref>
</summary>
  
```cairo
    func execute{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
    }(
        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) {
        // ...

        with stack, memory, state {
            let evm = run(evm);
        }

        let required_gas = gas_limit - evm.gas_left; <@udit
        let (max_refund, _) = unsigned_div_rem(required_gas, 5);
        let is_max_refund_le_gas_refund = is_nn(evm.gas_refund - max_refund);
        tempvar gas_refund = is_max_refund_le_gas_refund * max_refund + (
            1 - is_max_refund_le_gas_refund
        ) * evm.gas_refund;

        let total_gas_used = required_gas - gas_refund; <@udit

        // ...

        return (evm, stack, memory, state, total_gas_used, required_gas); <@udit
    }
```
</details>

<details>
<summary>
	<a href="https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L339-L466">System Operations</ahref>
</summary>
  
```cairo
    func exec_call{
        syscall_ptr: felt*,
        pedersen_ptr: HashBuiltin*,
        range_check_ptr,
        bitwise_ptr: BitwiseBuiltin*,
        stack: model.Stack*,
        memory: model.Memory*,
        state: model.State*,
    }(evm: model.EVM*) -> model.EVM* {
        // ...

        // 2. Gas
        // Memory expansion cost
        let memory_expansion = Gas.max_memory_expansion_cost(
            memory.words_len, args_offset, args_size, ret_offset, ret_size
        );

        if (memory_expansion.cost == Gas.MEMORY_COST_U32) {
            let evm = EVM.out_of_gas(evm, memory_expansion.cost);
            return evm;
        }

        // Access gas cost. The account is marked as warm in the `generic_call` function,
        // which performs a `get_account`.
        let is_account_warm = State.is_account_warm(to);
        t
Jump in creation code leads to reverting of the starknet transaction
Audit: 2024-09-kakarot-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

If the creation code consists of `JUMP` or `JUMPI` opcode for `offset` less than `creation code length`, then the starknet transaction reverts.

The [evm.cairo::jump](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/evm.cairo#L277) function is called whenever the `JUMP` or `JUMPI` opcode is executed. This function is as follows:
```cairo
// @notice Update the program counter.
// @dev The program counter is updated to a given value. This is only ever called by JUMP or JUMPI.
// @param self The pointer to the execution context.
// @param new_pc_offset The value to update the program counter by.
// @return model.EVM* The pointer to the updated execution context.
func jump{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, state: model.State*}(
    self: model.EVM*, new_pc_offset: felt
) -> model.EVM* {
->  let out_of_range = is_nn(new_pc_offset - self.message.bytecode_len);
    if (out_of_range != FALSE) {
        let (revert_reason_len, revert_reason) = Errors.invalidJumpDestError();
        let evm = EVM.stop(self, revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT);
        return evm;
    }

    let valid_jumpdests = self.message.valid_jumpdests;
    with valid_jumpdests {
->      let is_valid_jumpdest = Internals.is_valid_jumpdest(
            self.message.code_address, new_pc_offset
        );
    }

    // ...
}
```

It can be seen that, it checks for `out_of_range` which is `new_pc_offset - self.message.bytecode_len`. For the creation code, technically `bytecode_length` should be 0 but here, it will consist the length of the creation code. So, if the `new_pc_offset` is less than the `creation code length`, then this check would pass and then it checks for `is_valid_jumpdest` by calling [evm.cairo::is_valid_jumpdest](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/evm.cairo#L364), which is as follows: 
```cairo
func is_valid_jumpdest{
    syscall_ptr: felt*,
    pedersen_ptr: HashBuiltin*,
    range_check_ptr,
    valid_jumpdests: DictAccess*,
    state: model.State*,
}(code_address: model.Address*, index: felt) -> felt {
    alloc_locals;
    let (is_cached) = dict_read{dict_ptr=valid_jumpdests}(index);
    if (is_cached != 0) {
        return TRUE;
    }

    // If the account was created in the same transaction,
    // a cache miss is an invalid jumpdest as all valid jumpdests were cached on deployment.
    let code_account = State.get_account(code_address.evm);
    if (code_account.created != 0) {
        return FALSE;
    }

->  let (is_valid) = IAccount.is_valid_jumpdest(code_address.starknet, index);
    dict_write{dict_ptr=valid_jumpdests}(index, is_val