Skip to content
This repository has been archived by the owner on Jan 9, 2025. It is now read-only.

fix: check numbers of calls multicall precompile #1647

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions cairo_zero/kakarot/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ namespace Errors {

func precompileInputError() -> (error_len: felt, error: felt*) {
let (error) = get_label_location(precompile_input_error_message);
return (27, error);
return (23, error);

precompile_input_error_message:
dw 'P';
Expand All @@ -697,21 +697,17 @@ namespace Errors {
dw 'e';
dw ':';
dw ' ';
dw 'w';
dw 'r';
dw 'o';
dw 'n';
dw 'g';
dw ' ';
dw 'i';
dw 'n';
dw 'p';
dw 'u';
dw 't';
dw '_';
dw 'l';
dw ' ';
dw 'e';
dw 'n';
dw 'r';
dw 'r';
dw 'o';
dw 'r';
}

func precompileFlagError() -> (error_len: felt, error: felt*) {
Expand Down
40 changes: 29 additions & 11 deletions cairo_zero/kakarot/precompiles/kakarot_precompiles.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const CAIRO_PRECOMPILE_GAS = 10000;
// ! and the sending of transactions to L1.
// !
// ! There are various considerations that one must take into account when using these precompiles.
// ! We currently have 4 different "precompiles".
// ! We currently have 3 different "precompiles".
// ! - 0x75001: Whitelisted Cairo Precompile. Allows any whitelisted caller to execute a Cairo call.
// ! The whitelisting is based on the address of the caller. 75001 can be called using DELEGATECALL
// ! / CALLCODE. Any contract calling 75001 must be whitelisted, as malicious contract would be able
Expand Down Expand Up @@ -107,6 +107,10 @@ namespace KakarotPrecompiles {
// @param input_len The length of the input in bytes.
// @param input The input data.
// @param caller_address The address of the caller of the precompile.
// @returns output_len The length in bytes of the output of the first call that reverted else 0.
// @returns output The output data of the first call that reverted else empty.
// @returns gas_used The gas used.
// @returns reverted Errors.EXCEPTIONAL_HALT if a call reverted, FALSE otherwise.
func cairo_multicall_precompile{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
Expand Down Expand Up @@ -141,9 +145,17 @@ namespace KakarotPrecompiles {
let calls_ptr = number_of_calls_ptr + NUMBER_OF_CALLS_BYTES;
let calls_len = input_len - NUMBER_OF_CALLS_BYTES;

let (output_len, output, reverted) = Internals.execute_multiple_cairo_calls(
caller_address, calls_len, calls_ptr
);
let (
output_len, output, reverted, nb_executed_calls
) = Internals.execute_multiple_cairo_calls(caller_address, calls_len, calls_ptr, 0);

if (reverted == FALSE and nb_executed_calls != number_of_calls) {
let (revert_reason_len, revert_reason) = Errors.precompileInputError();
return (
revert_reason_len, revert_reason, CAIRO_PRECOMPILE_GAS, Errors.EXCEPTIONAL_HALT
);
}

return (output_len, output, gas_cost, reverted);
}
}
Expand All @@ -154,28 +166,31 @@ namespace Internals {
// @param caller_address The address of the caller of the precompile.
// @param calls_len The length of the calls array.
// @param calls The calls to execute.
// @returns output_len The length in bytes of the output of the first call that reverted else 0.
// @returns output The output data of the first call that reverted else empty.
// @returns gas_used The gas used.
// @returns reverted Errors.EXCEPTIONAL_HALT if reverted, FALSE otherwise.
func execute_multiple_cairo_calls{
syscall_ptr: felt*,
pedersen_ptr: HashBuiltin*,
range_check_ptr,
bitwise_ptr: BitwiseBuiltin*,
}(caller_address: felt, calls_len: felt, calls: felt*) -> (
output_len: felt, output: felt*, reverted: felt
}(caller_address: felt, calls_len: felt, calls: felt*, nb_executed_calls: felt) -> (
output_len: felt, output: felt*, reverted: felt, nb_executed_calls: felt
) {
alloc_locals;

if (calls_len == 0) {
let (output) = alloc();
return (0, output, FALSE);
return (0, output, FALSE, nb_executed_calls);
}

// Ensure that the current remaining calls_len >= MIN_EVM_ENCODED_STARKNET_CALL_BYTES
// Otherwise the input is malformed
let is_input_invalid = is_nn(MIN_EVM_ENCODED_STARKNET_CALL_BYTES - (calls_len + 1));
if (is_input_invalid != 0) {
let (revert_reason_len, revert_reason) = Errors.outOfBoundsRead();
return (revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT);
return (revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT, nb_executed_calls);
}

let (
Expand All @@ -184,20 +199,23 @@ namespace Internals {

if (is_err != FALSE) {
let (revert_reason_len, revert_reason) = Errors.precompileInputError();
return (revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT);
return (revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT, nb_executed_calls);
}

let (output_len, output, gas_used, reverted) = execute_cairo_call(
caller_address, to_address, selector, calldata_len, calldata, skip_returndata=TRUE
);

if (reverted != FALSE) {
return (output_len, output, reverted);
return (output_len, output, reverted, nb_executed_calls + 1);
}

// Move to the next call
return execute_multiple_cairo_calls(
caller_address, calls_len - next_call_offset, calls + next_call_offset
caller_address,
calls_len - next_call_offset,
calls + next_call_offset,
nb_executed_calls + 1,
);
}

Expand Down
2 changes: 1 addition & 1 deletion cairo_zero/tests/src/kakarot/precompiles/test_blake2f.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class TestBlake2f:
def test_should_fail_when_input_len_is_not_213(self, cairo_run):
output = cairo_run("test_should_fail_when_input_is_not_213")
assert bytes(output) == b"Precompile: wrong input_len"
assert bytes(output) == b"Precompile: input error"

def test_should_fail_when_flag_is_not_0_or_1(self, cairo_run):
output = cairo_run("test_should_fail_when_flag_is_not_0_or_1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import pytest
import pytest_asyncio
from eth_abi import encode
from hypothesis import given, settings
from hypothesis import assume, given, settings
from hypothesis import strategies as st
from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME

from kakarot_scripts.utils.kakarot import deploy, eth_send_transaction
from kakarot_scripts.utils.starknet import get_contract, invoke
Expand Down Expand Up @@ -93,6 +94,30 @@ async def test_should_set_and_increase_counter_in_batch(
expected_count = new_counter + 1
assert new_count == expected_count

@given(wrong_nb_calls=st.integers(min_value=0, max_value=DEFAULT_PRIME - 1))
async def test_should_fail_when_number_of_calls_mismatch_actual_calls(
self, cairo_counter, owner, wrong_nb_calls
):
assume(wrong_nb_calls != 2)
calls = [
cairo_counter.functions["set_counter"].prepare_call(1),
cairo_counter.functions["inc"].prepare_call(),
]
tx_data = prepare_transaction_data(calls)
# modify the number of calls to be different than the actual calls
tx_data = f"{wrong_nb_calls:064x}" + tx_data[64:]

_, response, success, _ = await eth_send_transaction(
to=f"0x{0x75003:040x}",
gas=21000 + 20000 * (len(calls)),
data=tx_data,
value=0,
caller_eoa=owner.starknet_contract,
)

assert not success
assert "Precompile: input error".encode() == bytes(response)

async def test_should_increase_counter_single_call_from_solidity(
self, cairo_counter, multicall_cairo_counter_caller
):
Expand Down
2 changes: 1 addition & 1 deletion tests/end_to_end/PlainOpcodes/test_plain_opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ class TestFallbackFunctions:
async def test_should_revert_on_fallbacks(
self, revert_on_fallbacks, data, value, message, other
):
receipt, response, success, gas_used = await eth_send_transaction(
_, response, success, _ = await eth_send_transaction(
to=revert_on_fallbacks.address,
gas=200_000,
data=data,
Expand Down