From 9ec542a48ae4169ad4f56c7ddac1956da2210680 Mon Sep 17 00:00:00 2001 From: "remy.baranx@gmail.com" Date: Mon, 19 Aug 2024 20:09:45 +0200 Subject: [PATCH] feat: SNIP-9 --- crates/contracts/src/account_contract.cairo | 86 ++++- crates/contracts/tests/lib.cairo | 2 + .../tests/test_execution_from_outside.cairo | 347 ++++++++++++++++++ 3 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 crates/contracts/tests/test_execution_from_outside.cairo diff --git a/crates/contracts/src/account_contract.cairo b/crates/contracts/src/account_contract.cairo index 39ff3356f..4dbba7f57 100644 --- a/crates/contracts/src/account_contract.cairo +++ b/crates/contracts/src/account_contract.cairo @@ -6,6 +6,15 @@ use core::starknet::account::{Call}; use core::starknet::{ContractAddress, EthAddress, ClassHash}; +#[derive(Copy, Drop, Serde, Debug)] +pub struct OutsideExecution { + pub caller: ContractAddress, + pub nonce: u64, + pub execute_after: u64, + pub execute_before: u64, + pub calls: Span +} + #[starknet::interface] pub trait IAccount { fn initialize( @@ -30,6 +39,9 @@ pub trait IAccount { fn get_nonce(self: @TContractState) -> u64; fn set_nonce(ref self: TContractState, nonce: u64); fn execute_starknet_call(ref self: TContractState, call: Call) -> (bool, Span); + fn execute_from_outside( + ref self: TContractState, outside_execution: OutsideExecution, signature: Span, + ) -> Array>; } #[starknet::contract(account)] @@ -49,6 +61,7 @@ pub mod AccountContract { use core::panic_with_felt252; use core::starknet::SyscallResultTrait; use core::starknet::account::{Call}; + use core::starknet::secp256_trait::Signature; use core::starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess @@ -57,11 +70,11 @@ pub mod AccountContract { use core::starknet::syscalls::{call_contract_syscall, replace_class_syscall}; use core::starknet::{ ContractAddress, EthAddress, ClassHash, VALIDATED, get_caller_address, get_contract_address, - get_tx_info, Store + get_tx_info, Store, get_block_timestamp }; use core::traits::TryInto; use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; - use super::{IAccountLibraryDispatcher, IAccountDispatcherTrait}; + use super::{IAccountLibraryDispatcher, IAccountDispatcherTrait, OutsideExecution}; use utils::constants::{POW_2_32}; use utils::eth_transaction::EthereumTransactionTrait; use utils::eth_transaction::{EthTransactionTrait, TransactionMetadata}; @@ -289,6 +302,75 @@ pub mod AccountContract { } return (false, response.unwrap_err().into()); } + + fn execute_from_outside( + ref self: ContractState, outside_execution: OutsideExecution, signature: Span, + ) -> Array> { + let caller = get_caller_address(); + let tx_info = get_tx_info(); + + if (outside_execution.caller.into() != 'ANY_CALLER') { + assert(caller == outside_execution.caller, 'Invalid caller'); + } + + let block_timestamp = get_block_timestamp(); + assert(block_timestamp > outside_execution.execute_after, 'Too early call'); + assert(block_timestamp < outside_execution.execute_before, 'Too late call'); + + assert(self.Account_bytecode_len.read().is_zero(), 'EOAs cannot have code'); + assert(tx_info.version.into() >= 1_u256, 'Deprecated tx version'); + assert(signature.len() == 5, 'Invalid signature length'); + + assert(outside_execution.calls.len() == 1, 'Multicall not supported'); + + let call = outside_execution.calls.at(0); + assert(*call.to == self.ownable.owner(), 'to is not kakarot core'); + assert!( + *call.selector == selector!("eth_send_transaction"), + "selector must be eth_send_transaction" + ); + + let chain_id: u128 = tx_info.chain_id.try_into().unwrap() % POW_2_32; + + let signature = deserialize_signature(signature, chain_id).expect('invalid signature'); + + let eth_caller_address: felt252 = outside_execution.caller.into(); + + let tx_metadata = TransactionMetadata { + address: eth_caller_address.try_into().unwrap(), + chain_id, + account_nonce: outside_execution.nonce.into(), + signature + }; + + let encoded_tx = deserialize_bytes(*outside_execution.calls[0].calldata) + .expect('conversion to Span failed') + .span(); + + let validation_result = EthTransactionTrait::validate_eth_tx(tx_metadata, encoded_tx) + .expect('failed to validate eth tx'); + + assert(validation_result, 'transaction validation failed'); + + let tx = EthTransactionTrait::decode(encoded_tx).expect('rlp decoding of tx failed'); + + let is_valid = match tx.try_into_fee_market_transaction() { + Option::Some(tx_fee_infos) => { self.validate_eip1559_tx(@tx, tx_fee_infos) }, + Option::None => true + }; + + let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; + + let return_data = if is_valid { + let (_, return_data, _) = kakarot.eth_send_transaction(tx); + return_data + } else { + KAKAROT_VALIDATION_FAILED.span() + }; + let return_data = serialize_bytes(return_data).span(); + + array![return_data] + } } #[generate_trait] diff --git a/crates/contracts/tests/lib.cairo b/crates/contracts/tests/lib.cairo index 0cc4ced9e..a014dfc5b 100644 --- a/crates/contracts/tests/lib.cairo +++ b/crates/contracts/tests/lib.cairo @@ -4,6 +4,8 @@ mod test_contract_account; mod test_eoa; +mod test_execution_from_outside; + mod test_kakarot_core; mod test_ownable; diff --git a/crates/contracts/tests/test_execution_from_outside.cairo b/crates/contracts/tests/test_execution_from_outside.cairo new file mode 100644 index 000000000..e501f8b6f --- /dev/null +++ b/crates/contracts/tests/test_execution_from_outside.cairo @@ -0,0 +1,347 @@ +use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait, OutsideExecution}; +use contracts::kakarot_core::interface::IExtendedKakarotCoreDispatcher; +use contracts::test_utils::{setup_contracts_for_testing, deploy_contract_account}; +use core::starknet::ContractAddress; +use core::starknet::account::Call; +use core::starknet::contract_address_const; +use core::starknet::secp256_trait::Signature; +use evm::test_utils::{ca_address, chain_id}; +use snforge_std::{ + start_cheat_caller_address, stop_cheat_caller_address, start_cheat_block_timestamp, + stop_cheat_block_timestamp, start_cheat_chain_id_global, stop_cheat_chain_id_global, + start_mock_call, stop_mock_call +}; +use utils::eth_transaction::TransactionType; +use utils::serialization::{serialize_bytes, serialize_transaction_signature}; +use utils::test_data::eip_2930_encoded_tx; + +const VALID_SIGNATURE: [felt252; 5] = [1, 2, 3, 4, 0]; + +const EIP2930_CALLER: felt252 = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf; +const VALID_EIP2930_SIGNATURE: Signature = + Signature { + r: 0xbced8d81c36fe13c95b883b67898b47b4b70cae79e89fa27856ddf8c533886d1, + s: 0x3de0109f00bc3ed95ffec98edd55b6f750cb77be8e755935dbd6cfec59da7ad0, + y_parity: true + }; + +#[derive(Destruct)] +struct CallBuilder { + call: Call +} + +#[generate_trait] +impl CallBuilderImpl of CallBuilderTrait { + fn new(kakarot_core: ContractAddress) -> CallBuilder { + CallBuilder { + call: Call { + to: kakarot_core, + selector: selector!("eth_send_transaction"), + calldata: serialize_bytes(eip_2930_encoded_tx()).span() + } + } + } + + fn with_to(mut self: CallBuilder, to: ContractAddress) -> CallBuilder { + self.call.to = to; + self + } + + fn with_selector(mut self: CallBuilder, selector: felt252) -> CallBuilder { + self.call.selector = selector; + self + } + + fn with_calldata(mut self: CallBuilder, calldata: Span) -> CallBuilder { + self.call.calldata = calldata; + self + } + + fn build(mut self: CallBuilder) -> Call { + return self.call; + } +} + +#[derive(Destruct)] +struct OutsideExecutionBuilder { + outside_execution: OutsideExecution +} + +#[generate_trait] +impl OutsideExecutionBuilderImpl of OutsideExecutionBuilderTrait { + fn new(kakarot_core: ContractAddress) -> OutsideExecutionBuilder { + OutsideExecutionBuilder { + outside_execution: OutsideExecution { + caller: 'ANY_CALLER'.try_into().unwrap(), + nonce: 0, + execute_after: 998, + execute_before: 1000, + calls: [ + CallBuilderTrait::new(kakarot_core).build(), + ].span() + } + } + } + + fn with_caller( + mut self: OutsideExecutionBuilder, caller: ContractAddress + ) -> OutsideExecutionBuilder { + self.outside_execution.caller = caller; + self + } + + fn with_nonce(mut self: OutsideExecutionBuilder, nonce: u64) -> OutsideExecutionBuilder { + self.outside_execution.nonce = nonce; + self + } + + fn with_execute_after( + mut self: OutsideExecutionBuilder, execute_after: u64 + ) -> OutsideExecutionBuilder { + self.outside_execution.execute_after = execute_after; + self + } + + fn with_execute_before( + mut self: OutsideExecutionBuilder, execute_before: u64 + ) -> OutsideExecutionBuilder { + self.outside_execution.execute_before = execute_before; + self + } + + fn with_calls(mut self: OutsideExecutionBuilder, calls: Span) -> OutsideExecutionBuilder { + self.outside_execution.calls = calls; + self + } + + fn build(mut self: OutsideExecutionBuilder) -> OutsideExecution { + return self.outside_execution; + } +} + +fn setUp() -> (IExtendedKakarotCoreDispatcher, IAccountDispatcher) { + let (_, kakarot_core) = setup_contracts_for_testing(); + let ca_address = deploy_contract_account(kakarot_core, ca_address(), [].span()); + let contract_account = IAccountDispatcher { contract_address: ca_address.starknet }; + + start_cheat_block_timestamp(ca_address.starknet, 999); + start_cheat_chain_id_global(chain_id().into()); + + (kakarot_core, contract_account) +} + +fn tearDown(contract_account: IAccountDispatcher) { + stop_cheat_chain_id_global(); + stop_cheat_block_timestamp(contract_account.contract_address); +} + +#[test] +#[should_panic(expected: 'Invalid caller')] +fn test_execute_from_outside_invalid_caller() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_caller(contract_address_const::<0xb0b>()) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'Too early call')] +fn test_execute_from_outside_too_early_call() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_execute_after(999) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'Too late call')] +fn test_execute_from_outside_too_late_call() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_execute_before(999) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'Invalid signature length')] +fn test_execute_from_outside_invalid_signature_length() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .build(); + + let _ = contract_account.execute_from_outside(outside_execution, [].span()); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'Multicall not supported')] +fn test_execute_from_outside_multicall_not_supported() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_calls( + [ + CallBuilderTrait::new(kakarot_core.contract_address).build(), + CallBuilderTrait::new(kakarot_core.contract_address).build(), + ].span() + ) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'to is not kakarot core')] +fn test_execute_from_outside_to_is_not_kakarot_core() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_calls([CallBuilderTrait::new(contract_address_const::<0xb0b>()).build()].span()) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: "selector must be eth_send_transaction")] +fn test_execute_from_outside_wrong_selector() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_calls( + [ + CallBuilderTrait::new(kakarot_core.contract_address) + .with_selector('bad_selector') + .build() + ].span() + ) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'invalid signature')] +fn test_execute_from_outside_invalid_signature() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .build(); + let signature: Span = [1, 2, 3, 4, (chain_id() * 2 + 40).into()].span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'conversion to Span failed')] +fn test_execute_from_outside_bad_raw_tx() { + let (kakarot_core, contract_account) = setUp(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_calls( + [ + CallBuilderTrait::new(kakarot_core.contract_address) + .with_calldata([1, 256].span()) + .build() + ].span() + ) + .build(); + let signature = VALID_SIGNATURE.span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +#[should_panic(expected: 'failed to validate eth tx')] +fn test_execute_from_outside_invalid_tx() { + let (kakarot_core, contract_account) = setUp(); + + let mut faulty_eip_2930_tx = eip_2930_encoded_tx(); + let _ = faulty_eip_2930_tx.pop_front(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_calls( + [ + CallBuilderTrait::new(kakarot_core.contract_address) + .with_calldata(serialize_bytes(faulty_eip_2930_tx).span()) + .build() + ].span() + ) + .build(); + + let signature = serialize_transaction_signature( + VALID_EIP2930_SIGNATURE, TransactionType::EIP2930, chain_id() + ) + .span(); + + let _ = contract_account.execute_from_outside(outside_execution, signature); + + tearDown(contract_account); +} + +#[test] +fn test_execute_from_outside() { + let (kakarot_core, contract_account) = setUp(); + + let caller = contract_address_const::(); + + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_caller(caller) + .build(); + let signature = serialize_transaction_signature( + VALID_EIP2930_SIGNATURE, TransactionType::EIP2930, chain_id() + ) + .span(); + + start_cheat_caller_address(contract_account.contract_address, caller); + + start_mock_call::< + (bool, Span, u128) + >( + kakarot_core.contract_address, + selector!("eth_send_transaction"), + (true, [1, 2, 3].span(), 0) + ); + + let data = contract_account.execute_from_outside(outside_execution, signature); + + assert(data.len() == 1, 'bad length'); + assert(*data.at(0) == [1, 2, 3].span(), 'bad data'); + + stop_mock_call(kakarot_core.contract_address, selector!("eth_send_transaction")); + stop_cheat_caller_address(contract_account.contract_address); + tearDown(contract_account); +}