Skip to content

Commit

Permalink
fix: check numbers of calls multicall
Browse files Browse the repository at this point in the history
precompile
  • Loading branch information
obatirou committed Nov 29, 2024
1 parent 306d391 commit b7a834c
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 24 deletions.
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

0 comments on commit b7a834c

Please sign in to comment.