xanoutas/solidity-security-findings huggingface dataset:
Memory can still be charged for even if there is no expansion
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/ethereum/go-ethereum/blob/65e5ca7d8126f7a8c708f8affb64f16c22cc63c0/core/vm/gas_table.go#L45-L55
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/gas.cairo#L88


# Vulnerability details

## Description
Due to the following use of `is_le_felt`:https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/gas.cairo#L88 which returns true in the equality case, extra gas can be charged even if there is no memory expansion, especially combined with the previous issue that calculates the memory length incorrectly. 

## Recommended Mitigation Steps
Ensure that the memory expansion function returns 0 if `current_memory_length` is equal to `max_offset` like how geth does here: https://github.com/ethereum/go-ethereum/blob/65e5ca7d8126f7a8c708f8affb64f16c22cc63c0/core/vm/gas_table.go#L45-L55


## Assessed type

Invalid Validation
Event Limit Attack: Free Transaction Execution Due to Starknet Event Limit
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact

An attacker can execute transactions without paying gas by exploiting the mismatch between EVM and Starknet event limits. By crafting transactions that emit more than 1000 events, the attacker can:

- Execute computationally expensive operations for free
- Avoid paying any gas costs as the transaction fails at Starknet level
- The paymaster to cover Cairo steps costs for failed transactions

Please note that:
- The attack is straightforward to implement
- No special permissions required

## Proof of Concept

The vulnerability occurs due to how Kakarot processes events and commits state to Starknet. Key components:

- Event collection without limits:
```cairo
func add_event{state: model.State*}(event: model.Event) {
	assert state.events[state.events_len] = event;

	tempvar state = new model.State(
		accounts_start=state.accounts_start,
		accounts=state.accounts,
		events_len=state.events_len + 1,
		events=state.events,
		transfers_len=state.transfers_len,
		transfers=state.transfers,
	);
	return ();
}
```
[State.cairo:L344](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/state.cairo#L344)

- State commitment with events 
```cairo
func commit{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
	self: model.State*
) {
	.
	.
	// Events
	Internals._emit_events(self.events_len, self.events);
	.
	.
	return ();
}

func _emit_events{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
	events_len: felt, events: model.Event*
) {

	.
	.
	tempvar data_len = is_nn(300 - event.data_len) * (event.data_len - 300) + 300;

	emit_event(
		keys_len=event.topics_len, keys=event.topics, data_len=data_len, data=event.data
	);

	_emit_events(events_len - 1, events + model.Event.SIZE);
	return ();
}
```
[Starknet.cairo:L53](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L53), [Starknet.cairo:L244](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L244)




Attack Contract Sample:
```solidity
contract EventAttack {
    event Attack(uint256 indexed id, bytes32 data);
    
    function execute() external {
        // Emit exactly 1000 events to maximize resource usage
        for(uint i = 0; i < 1000; i++) {
            // Each event is 32 bytes to maximize gas usage
            emit Attack(i, keccak256(abi.encode(i)));
        }
        
        // Optional: Add additional expensive computations here
        // They will be executed but never paid for
    }
}
```

Attack Flow:
```mermaid
sequenceDiagram
    participant Attacker
    participant Paymaster
    participant Kakarot
    participant zkEVM
    participant Starknet

    Attacker->>Paymaster: Prepare transaction
    Paymaster->>Kakarot: Submit transaction
Incorrect SIGNEXTEND Opcode Input Validation
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details



## Impact
The SIGNEXTEND opcode in Kakarot incorrectly handles edge cases where `byteNum = 31`, leading to different behavior compared to the Geth implementation. This could cause state divergences when executing smart contracts that rely on SIGNEXTEND operations with byte number 31, potentially breaking EVM compatibility.

## Proof of Concept
The issue lies in how the input range validation is performed in 

Kakarot checks if byteNum >= 32:
```cairo
let (byte_num_gt_word_size) = uint256_le(Uint256(32, 0), byte_num);
if (byte_num_gt_word_size != 0) {
	return x;
}
```
[uint256.cairo:L317](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/utils/uint256.cairo#L317)


While Geth checks if byteNum > 30:

```go
import (
	.
	.
	"github.com/holiman/uint256"
)
// Geth uses holiman/uint256
func opSignExtend(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
	.
	num.ExtendSign(num, &back) // see body below
	.
	.
}
```
[vm/instructions.go:L80](https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go#L80)

ExtendSign function from **holiman/uint256**  
```go
func (z *Int) ExtendSign(x, byteNum *Int) *Int {
	.
	.
	if byteNum.GtUint64(30) {
		return z.Set(x)
	}
```
[uint256.go:L1234-L1239](https://github.com/holiman/uint256/blob/master/uint256.go#L1234-L1239)

This leads to different behavior if byteNum is 31. Check this demonstration using the input value 0x80:

```sh
Input: 0x80 (binary: 10000000)

Case 1: byteNum = 30
- Geth: Performs sign extension
- Kakarot: Performs sign extension
MATCH ✓

Case 2: byteNum = 31
- Geth: Returns input unchanged
- Kakarot: Performs sign extension 
MISMATCH ❌

Case 3: byteNum = 32
- Geth: Returns input unchanged
- Kakarot: Returns input unchanged
MATCH ✓

Case 4: byteNum = 33
- Geth: Returns input unchanged
- Kakarot: Returns input unchanged
MATCH ✓
```

The flow of execution:

- When byteNum = 31:
   - Geth: 31 > 30 evaluates to TRUE → returns input unchanged
   - Kakarot: 32 <= 31 evaluates to FALSE → performs sign extension

- This mismatch affects a critical edge case in the SIGNEXTEND operation, where Geth specifies that any byteNum > 30 should return the input unchanged, while Kakarot continues to perform sign extension for byteNum = 31.


*Note: check page 31 of Ethereum Yellow Paper if necessary*

## Tools Used
Manual Review

## Recommended Mitigation Steps
Update the input range check in Kakarot to match Geth's behavior by changing the condition to check if byteNum > 30 instead of byteNum >= 32.


## Assessed type

Other
Non-finalized dictionary in RIPEMD160 allows forging of output
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

When [`ripemd160.finish()`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ripemd160.cairo#L427) does not enter the `if (next_block == FALSE)` condition at L456, the dictionary `x` initialized at the beginning of the function is not finalized. Instead, `x` is reassigned to reference a new dictionary at L470:

```cairo
File: ripemd160.cairo
456:     if (next_block == FALSE) {
			 ...
462:         default_dict_finalize(start, x, 0);
463:         let (res, rsize) = compress(buf, bufsize, arr_x, 16);
464:         return (res=res, rsize=rsize);
465:     }
466:     let (local arr_x: felt*) = alloc();
467:     dict_to_array{dict_ptr=x}(arr_x, 16);
468:     let (buf, bufsize) = compress(buf, bufsize, arr_x, 16);
469:     // reset dict to all 0.
470:     let (x) = default_dict_new(0);
```

Because the old dictionary is never finalized, it is possible to insert incorrect values for read operations on the old dictionary, which allows proving an incorrect output for any input of size > 55 (56 with the fix to our separate vulnerability on this value).

Read operations on the old dictionary are performed in [`ripemd160::absorb_data`](https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/precompiles/ripemd160.cairo#L148):

```cairo
File: ripemd160.cairo
148: func absorb_data{range_check_ptr, bitwise_ptr: BitwiseBuiltin*, dict_ptr: DictAccess*}(
149:     data: felt*, len: felt, index: felt
150: ) {
151:     alloc_locals;
152:     if (index - len == 0) {
153:         return ();
154:     }
155: 
156:     let (index_4, _) = unsigned_div_rem(index, 4);
157:     let (index_and_3) = uint32_and(index, 3);
158:     let (factor) = uint32_mul(8, index_and_3);
159:     let (factor) = pow2(factor);
160:     let (tmp) = uint32_mul([data], factor);
161:     let (old_val) = dict_read{dict_ptr=dict_ptr}(index_4);
162:     let (val) = uint32_xor(old_val, tmp);
163:     dict_write{dict_ptr=dict_ptr}(index_4, val);
164: 
165:     absorb_data{dict_ptr=dict_ptr}(data + 1, len, index + 1);
166:     return ();
167: }
```

## Proof of Concept
The below test case can be added to `test_ripemd160.py` to demonstrate the vulnerability:
```python
async def test_ripemd160_output_can_be_forged(self, cairo_program, cairo_run):
	msg_bytes = bytes([0x00] * 57)
	with (
		patch_hint(
			cairo_program,
			"vm_enter_scope()",
			"try:\n"
			"    dict_tracker = __dict_manager.get_tracker(ids.dict_ptr)\n"
			"    dict_tracker.data[ids.index_4] = 1\n"
			"except Exception: pass\n"
			"vm_enter_scope()"
		)
	):
		precompile_hash = cairo_run("test__ripemd160", msg=list(msg_bytes))

		# Hash with RIPEMD-160 to compare with precompile result
		ripemd160_crypto = RIPEMD160.new()
		ripemd160_crypto.update(msg_bytes)
		expected_hash = ripemd1
Malicious EVM contracts can trigger RPC-level crashes by reverting after calling DualVmToken
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L1012
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L1252
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/kakarot/instructions/system_operations.cairo#L1302


# Vulnerability details

The Kakarot EVM has a feature that allows bridging EVM calls to Cairo contracts, most notably used by the `DualVmToken` Starknet ERC20 wrapper.

Because unlike within the Kakarot EVM, where Kakarot can revert state changes, state changes made by Cairo calls can't be undone without triggering a full revert of the transaction, the protocol has explicit checks in place that trigger a revert to keep the state consistent, for example:

```cairo
File: system_operations.cairo
1008:         if (evm.reverted != FALSE) {
1009:             // If a call to a cairo precompile has been made, the tx should be reverted
1010:             with_attr error_message(
1011:                     "EVM tx reverted, reverting SN tx because of previous calls to cairo precompiles") {
1012:                 assert cairo_precompile_called = FALSE;
1013:             }
1014:         }
```

While this is an intentional, and with the current architecture necessary measure, it creates a deviation from EVM implementations that can be exploited by malicious EVM contracts (as shown in the PoC below).

Consequently, malicious EVM contracts can cause transactions that call them to revert at RPC level, regardless of how defensively they were called. Protective measures generally considered safe in the EVM space like [ExcessivelySafeCall](https://github.com/LayerZero-Labs/endpoint-v1-solidity-examples/blob/main/contracts/libraries/ExcessivelySafeCall.sol) (which is used in LayerZero cross-chain applications to prevent channel DoSes that can permanently freeze in-flight tokens), can be bypassed by causing a RPC-level cairo revert.

## Proof of Concept
The following EVM contract can trigger an unrecoverable RPC-level failure when called in any way, no matter how defensively, from another EVM Contract:

```solidity
contract Hack {
    DualVmToken tok;

    constructor(DualVmToken _tok) {
        tok = _tok;
    }

    fallback() external {
        tok.decimals();
        revert("OK, bye!");
    }
}
```

## Recommended Mitigation Steps
Mitigating this finding can only be done without reverting the full transaction. A possible way to do so would be to have revert-able subframes within the EVM execution (i.e. by having Kakarot calling itself using the Cairo 1 [syscalls](https://docs.starknet.io/architecture-and-concepts/smart-contracts/system-calls-cairo1/#call_contract) that don't panic and can recover a revert) that can undo the Starknet calls.



## Assessed type

Other
DualVmToken allows moving native balance with zero-value EVM calls
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

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


# 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.

One special case for this wrapper is the Starknet ERC-20 used by Kakarot to settle native token transfers. By calling this specific instance of `DualVmToken`, EVM contracts are able to move native balances with an ERC-20 call with `value = 0`, with the effect of moving native balance. This deviates from standard EVM implementations, and can be used to extract value from EVM contracts as shown in the below PoC.

Consequently, EVM contracts can send native value also with zero-value calls. This is not possible in other EVM implementations, and this fact is often relied upon to secure contracts, as shown below.

## Proof of Concept
The following contract that is meant to handle only native tokens and has a utility function to rescue unexpected ERC-20 tokens, while safe on Ethereum, would be unsafe on Kakarot:

```solidity
contract Unsafe {
    mapping(address => uint) balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(address to, uint amount) external {
        balances[msg.sender] -= amount;
        payable(to).transfer(amount);
    }

    function rescueERC20(IERC20 token, address to) external {
        uint balance = token.balanceOf(address(this));
        token.transfer(to, balance);
    }
}
```

After a deposit, anyone can call `rescueERC20()` passing the native token's `DualVmToken` wrapper as argument.

## Recommended Mitigation Steps
Consider adding a check in `DualVmToken` that prevents transfers or approvals when the underlying Starknet ERC-20 is Kakarot's native token, and thoroughly test `transferFrom()` functionality for safety.

Alternatively, the possibility to move tokens this way should be documented as a critical deviation from Ethereum's EVM.


## Assessed type

Other
Return value of ERC-20 operations is not checked
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
https://github.com/kkrt-labs/kakarot/blob/7411a5520e8a00be6f5243a50c160e66ad285563/src/backend/starknet.cairo#L264


# Vulnerability details

Among the [ERC20 token behaviors in scope](https://github.com/code-423n4/2024-09-kakarot/blob/3163e06e0d4dc43919c29526468caa77b569a021/README.md?plain=1#L336), there is `Doesn't revert on failure` that is not properly accounted for and has a unique root cause.

The code in scope for the audit directly handles ERC-20s only in the following forms:

1. the Starknet ERC-20 used in Kakarot's cairo code as native token;
2. generic Starknet ERC-20s handled within the Kakarot EVM through a `DualVmToken` wrapper.

As we can see in the snippets from both cases, if the ERC-20 doesn't revert on failure, and instead returns `FALSE`, the protocol will interpret the failure as a success
```cairo
File: starknet.cairo
256:     func _transfer_eth{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
257:         token_address: felt, transfers_len: felt, transfers: model.Transfer*
258:     ) {
---          // 🚨 @audit return value is ignored
264:         IERC20.transferFrom(
265:             token_address, transfer.sender.starknet, transfer.recipient.starknet, transfer.amount
266:         );
267:         return _transfer_eth(token_address, transfers_len - 1, transfers + model.Transfer.SIZE);
268:     }
```

```solidity
File: DualVmToken.sol
216:     function _approve(uint256 spender, uint256 amount) private {
---          // 🚨 @audit return value is ignored
228:         starknetToken.delegatecallCairo("approve", approveCallData);
229:     }
---
256:     function _transfer(uint256 to, uint256 amount) private {
---          // 🚨 @audit return value is ignored
269:         starknetToken.delegatecallCairo("transfer", transferCallData);
270:     }
---
339:     function _transferFrom(uint256 from, uint256 to, uint256 amount) private {
---          // 🚨 @audit return value is ignored
353:         starknetToken.delegatecallCairo("transfer_from", transferFromCallData);
354:     }
```

EVM protocols running in Kakarot are therefore at risk of having their accounting broken when interacting through `DualVmToken` with ERC-20s that don't revert on failures.

A more unlikely, but still possible impact on the Kakarot native token, where Kakarot doesn't revert when failing to settle native tokens transactions, can be achieved when chaining this with other vulnerabilities that we reported separately.

## Proof of Concept
- Deploy a non-standard Starknet ERC-20 token that always returns `FALSE`
- Deploy a `DualVmToken` wrapper for it
- Make a `DualVmToken.transferFrom()` EVM call
- The call will return `true` despite the underlying
RIPEMD-160 precompile yields wrong hashes for large set of inputs due to off-by-one error
Audit: 2024-09-kakarot-findings
Severity: HIGH

# Lines of code

https://github.com/kkrt-labs/kakarot/blob/a4c45215f7fd5cdd9fbbfd11196a9a787c6468df/cairo_zero/kakarot/precompiles/ripemd160.cairo#L455


# Vulnerability details

The RIPEMD-160 digest integrates the input data with one additional block including the message length.

If we look at the Cairo code that achieves this:

```cairo
File: ripemd160.cairo
455:     let next_block = is_nn_le(55, len);
456:     if (next_block == FALSE) {
457:         dict_write{dict_ptr=x}(14, val);
458:         dict_write{dict_ptr=x}(15, val_15);
459: 
460:         let (local arr_x: felt*) = alloc();
461:         dict_to_array{dict_ptr=x}(arr_x, 16);
462:         default_dict_finalize(start, x, 0);
463:         let (res, rsize) = compress(buf, bufsize, arr_x, 16);
464:         return (res=res, rsize=rsize);
465:     }
466:     let (local arr_x: felt*) = alloc();
467:     dict_to_array{dict_ptr=x}(arr_x, 16);
468:     let (buf, bufsize) = compress(buf, bufsize, arr_x, 16);
```

... we see that the `if (next_block == false)` executes one block of code in case `len >= 55`, and the other otherwise.

If we compare this check with the [equivalent implementation in Go (used by Geth)](https://github.com/golang/crypto/blob/7cfb9161e8d828fd6d9f34560e78460435b63503/ripemd160/ripemd160.go#L98), for example:

```go
	if tc%64 < 56 {
		d.Write(tmp[0 : 56-tc%64])
	} else {
		d.Write(tmp[0 : 64+56-tc%64])
	}
```

we see that this time, the selection is made with `len < 56` as discriminator; if we imagine swapping the `if` and the `else` blocks of the Go implementation, the condition becomes `len >= 56`.

So for the edge case of `len == 55`, Kakarot and Geth will take different actions, which, as shown in the PoC, results in differing hashes. `len` here represents the length of the input modulo 64, hence the RIPEMD-160 precompile will yield wrong outputs for inputs of length `55 + k*64`.

Because this precompile is a cryptographic hash function, it can be used in many ways by EVM applications, including access control on funds.

## Proof of Concept
To prove the point, the following test can be added to the `test_ripemd160.py` unit test suite:

```python
    async def test_ripemd160_on_55_length_input(self, cairo_run):
        msg_bytes = bytes("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmomnopnopq", "ascii")
        precompile_hash = cairo_run("test__ripemd160", msg=list(msg_bytes))

        # Hash with RIPEMD-160 to compare with precompile result
        ripemd160_crypto = RIPEMD160.new()
        ripemd160_crypto.update(msg_bytes)
        expected_hash = ripemd160_crypto.hexdigest()

        assert expected_hash.rjust(64, "0") == bytes(precompile_hash).hex()
```

... which yields the following output:
```
>       assert expected_hash.rjust(64, "0") == bytes(precompile_hash).hex()
E       AssertionError: assert '000000000000...65c8fd54fbafd' == '000000000000...169718d24abaf'
E         
E         - 000000000000000000000000578b4715e07320701e0715dc640169718d24abaf
E         +