diff --git a/crates/contracts/src/account_contract.cairo b/crates/contracts/src/account_contract.cairo index 88882fc72..1e136892b 100644 --- a/crates/contracts/src/account_contract.cairo +++ b/crates/contracts/src/account_contract.cairo @@ -57,6 +57,7 @@ pub mod AccountContract { use core::panic_with_felt252; use core::starknet::SyscallResultTrait; use core::starknet::account::{Call}; + use core::starknet::eth_signature::verify_eth_signature; use core::starknet::storage::{ Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess @@ -73,6 +74,7 @@ pub mod AccountContract { use utils::eth_transaction::validation::validate_eth_tx; use utils::eth_transaction::{TransactionMetadata}; use utils::serialization::{deserialize_signature, deserialize_bytes, serialize_bytes}; + use core::cmp::min; use utils::traits::DefaultSignature; // Add ownable component @@ -163,106 +165,17 @@ pub mod AccountContract { // EOA functions fn __validate__(ref self: ContractState, calls: Array) -> felt252 { - let tx_info = get_tx_info().unbox(); - assert(get_caller_address().is_zero(), 'EOA: reentrant call'); - assert(calls.len() == 1, 'EOA: multicall not supported'); - // todo: activate check once using snfoundry - // assert(tx_info.version.try_into().unwrap() >= 1_u128, 'EOA: deprecated tx version'); - assert(self.Account_bytecode_len.read().is_zero(), 'EOAs: Cannot have code'); - assert(tx_info.signature.len() == 5, 'EOA: invalid signature length'); - - let call = calls.at(0); - assert(*call.to == self.ownable.owner(), 'to is not kakarot core'); - assert!( - *call.selector == selector!("eth_send_transaction"), - "Validate: selector must be eth_send_transaction" - ); - - let chain_id: u64 = tx_info.chain_id.try_into().unwrap() % POW_2_32.try_into().unwrap(); - let signature = deserialize_signature(tx_info.signature, chain_id) - .expect('EOA: invalid signature'); - - let tx_metadata = TransactionMetadata { - address: self.Account_evm_address.read(), - chain_id, - account_nonce: tx_info.nonce.try_into().unwrap(), - signature - }; - - let mut encoded_tx = deserialize_bytes(*call.calldata) - .expect('conversion to Span failed') - .span(); - let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx) - .expect('EOA: could not decode tx'); - let validation_result = validate_eth_tx(tx_metadata, unsigned_transaction) - .expect('failed to validate eth tx'); - - assert(validation_result, 'transaction validation failed'); - - VALIDATED + panic!("EOA: __validate__ not supported") } /// Validate Declare is not used for Kakarot fn __validate_declare__(self: @ContractState, class_hash: felt252) -> felt252 { - panic_with_felt252('Cannot Declare EOA') + panic!("EOA: declare not supported") } fn __execute__(ref self: ContractState, calls: Array) -> Array> { - let caller = get_caller_address(); - let tx_info = get_tx_info().unbox(); - assert(caller.is_zero(), 'EOA: reentrant call'); - assert(calls.len() == 1, 'EOA: multicall not supported'); - // todo: activate check once using snfoundry - // assert(tx_info.version.try_into().unwrap() >= 1_u128, 'EOA: deprecated tx version'); - - let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; - let latest_class = kakarot.get_account_contract_class_hash(); - let this_class = self.Account_implementation.read(); - - if (latest_class != this_class) { - self.Account_implementation.write(latest_class); - let response = IAccountLibraryDispatcher { class_hash: latest_class } - .__execute__(calls); - replace_class_syscall(latest_class).unwrap_syscall(); - return response; - } - - // Increment nonce to match protocol's nonce for EOAs. - self.Account_nonce.write(tx_info.nonce.try_into().unwrap() + 1); - - let call: @Call = calls[0]; - let _encoded_tx_data = deserialize_bytes(*call.calldata) - .expect('conversion failed') - .span(); - - let _chain_id: u64 = tx_info - .chain_id - .try_into() - .unwrap() % POW_2_32 - .try_into() - .unwrap(); - - //TODO: add a type for unsigned transaction - let mut encoded_tx = deserialize_bytes(*call.calldata) - .expect('conversion to Span failed') - .span(); - let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx) - .expect('EOA: could not decode tx'); - - //TODO: validation of EIP-1559 transactions - // Not done because this endpoint will end up deprecated after EIP-1559 - let is_valid = true; - - let (success, return_data, gas_used) = if is_valid { - kakarot.eth_send_transaction(unsigned_transaction.transaction) - } else { - (false, KAKAROT_VALIDATION_FAILED.span(), 0) - }; - let return_data = serialize_bytes(return_data).span(); - - self.emit(TransactionExecuted { response: return_data, success: success, gas_used }); - - array![return_data] + panic!("EOA: __execute__ not supported"); + array![] } fn write_bytecode(ref self: ContractState, bytecode: Span) { @@ -315,6 +228,7 @@ pub mod AccountContract { let caller = get_caller_address(); let tx_info = get_tx_info(); + // SNIP-9 Validation if (outside_execution.caller.into() != 'ANY_CALLER') { assert(caller == outside_execution.caller, 'SNIP9: Invalid caller'); } @@ -323,20 +237,18 @@ pub mod AccountContract { assert(block_timestamp > outside_execution.execute_after, 'SNIP9: Too early call'); assert(block_timestamp < outside_execution.execute_before, 'SNIP9: Too late call'); - assert(outside_execution.calls.len() == 1, 'Multicall not supported'); - 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'); + // Kakarot-Specific Validation + assert(outside_execution.calls.len() == 1, 'KKRT: Multicall not supported'); + assert(tx_info.version.into() >= 1_u256, 'KKRT: Deprecated tx version: 0'); + + // EOA Validation + assert(self.Account_bytecode_len.read().is_zero(), 'EOA: cannot have code'); - 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: u64 = tx_info.chain_id.try_into().unwrap() % POW_2_32.try_into().unwrap(); + assert(signature.len() == 5, 'Invalid signature length'); let signature = deserialize_signature(signature, chain_id) .expect('EOA: invalid signature'); + let mut encoded_tx_data = deserialize_bytes((*outside_execution.calls[0]).calldata) .expect('conversion to Span failed') .span(); @@ -344,37 +256,22 @@ pub mod AccountContract { ref encoded_tx_data ) .expect('EOA: could not decode tx'); - // TODO(execute-from-outside): move validation to KakarotCore - let tx_metadata = TransactionMetadata { - address: self.Account_evm_address.read(), - chain_id, - account_nonce: self.Account_nonce.read().into(), - signature - }; - - let validation_result = validate_eth_tx(tx_metadata, unsigned_transaction) - .expect('failed to validate eth tx'); - - assert(validation_result, 'transaction validation failed'); - - //TODO: validate eip1559 transactions - // 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 is_valid = true; - let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; + let address = self.Account_evm_address.read(); + verify_eth_signature(unsigned_transaction.hash, signature, address); - let return_data = if is_valid { - let (_, return_data, _) = kakarot - .eth_send_transaction(unsigned_transaction.transaction); - return_data - } else { - KAKAROT_VALIDATION_FAILED.span() - }; + let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; + let (success, return_data, gas_used) = kakarot + .eth_send_transaction(unsigned_transaction.transaction); let return_data = serialize_bytes(return_data).span(); + // See Argent account + // https://github.com/argentlabs/argent-contracts-starknet/blob/1352198956f36fb35fa544c4e46a3507a3ec20e3/src/presets/user_account.cairo#L211-L213 + // See 300 max data_len for events + // https://github.com/starkware-libs/blockifier/blob/9bfb3d4c8bf1b68a0c744d1249b32747c75a4d87/crates/blockifier/resources/versioned_constants.json + // The whole data_len should be less than 300, so it's the return_data should be less + // than 297 (+3 for return_data_len, success, gas_used) + self.emit(TransactionExecuted { response: return_data.slice(0, min(297, return_data.len())), success: success, gas_used }); array![return_data] } } diff --git a/crates/contracts/src/kakarot_core/kakarot.cairo b/crates/contracts/src/kakarot_core/kakarot.cairo index 954a40468..c4829ee7c 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -18,6 +18,7 @@ pub mod KakarotCore { get_caller_address }; use evm::backend::starknet_backend; + use evm::backend::validation::validate_eth_tx; use evm::errors::{EVMError, ensure, EVMErrorTrait,}; use evm::gas; use evm::model::account::AccountTrait; @@ -27,9 +28,12 @@ pub mod KakarotCore { use evm::precompiles::eth_precompile_addresses; use evm::state::StateTrait; use evm::{EVMTrait}; + use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; use utils::address::compute_contract_address; + use utils::constants::{POW_2_32}; use utils::eth_transaction::common::TxKind; use utils::eth_transaction::eip2930::{AccessListItem, AccessListItemTrait}; + use utils::eth_transaction::get_effective_gas_price; use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; use utils::helpers::compute_starknet_address; use utils::set::{Set, SetTrait}; @@ -164,6 +168,8 @@ pub mod KakarotCore { } fn eth_send_transaction(ref self: ContractState, tx: Transaction) -> (bool, Span, u64) { + validate_eth_tx(@self, tx); + let starknet_caller_address = get_caller_address(); let account = IAccountDispatcher { contract_address: starknet_caller_address }; let origin = Address { @@ -275,6 +281,8 @@ pub mod KakarotCore { let gas_fee = gas_limit.into() * gas_price; let mut sender_account = env.state.get_account(origin.evm); let sender_balance = sender_account.balance(); + sender_account.set_nonce(sender_account.nonce() + 1); + env.state.set_account(sender_account); match ensure( sender_balance >= gas_fee.into() + tx.value(), EVMError::InsufficientBalance ) { diff --git a/crates/evm/src/backend.cairo b/crates/evm/src/backend.cairo index f9215a84b..49f713b44 100644 --- a/crates/evm/src/backend.cairo +++ b/crates/evm/src/backend.cairo @@ -1 +1,2 @@ pub mod starknet_backend; +pub mod validation; diff --git a/crates/evm/src/backend/validation.cairo b/crates/evm/src/backend/validation.cairo new file mode 100644 index 000000000..25b28360a --- /dev/null +++ b/crates/evm/src/backend/validation.cairo @@ -0,0 +1,59 @@ +use contracts::IKakarotCore; +use starknet::storage::StorageTrait; +use core::ops::SnapshotDeref; +use contracts::kakarot_core::KakarotCore; +use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; +use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait}; +use core::starknet::{get_caller_address, get_tx_info}; +use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; +use utils::constants::POW_2_32; +use core::starknet::storage::{StoragePointerReadAccess}; +use utils::eth_transaction::get_effective_gas_price; + +pub fn validate_eth_tx(kakarot_state: @KakarotCore::ContractState, tx: Transaction){ + let kakarot_storage = kakarot_state.snapshot_deref().storage(); + // Validate transaction + + // Validate chain_id for post eip155 + let tx_chain_id = tx.chain_id(); + let kakarot_chain_id: u64 = get_tx_info() + .chain_id + .try_into() + .unwrap() % POW_2_32 + .try_into() + .unwrap(); + if (tx_chain_id.is_some()) { + assert(tx_chain_id.unwrap() == kakarot_chain_id, 'Invalid chain id'); + } + + // Validate nonce + let starknet_caller_address = get_caller_address(); + let account = IAccountDispatcher { contract_address: starknet_caller_address }; + assert(account.get_nonce() == tx.nonce(), 'Invalid nonce'); + + // Validate gas + assert(tx.gas_limit() <= kakarot_state.get_block_gas_limit(), 'Tx gas > Block gas'); + let block_base_fee = kakarot_storage.Kakarot_base_fee.read(); + assert(tx.max_fee_per_gas() <= block_base_fee.into(), 'Max fee per gas too low'); + assert( + tx.max_priority_fee_per_gas().unwrap_or(0) <= tx.max_fee_per_gas(), + 'Max prio fee > max fee per gas' + ); + + // Validate balance + let evm_address = account.get_evm_address(); + let balance = IERC20CamelDispatcher { + contract_address: kakarot_storage.Kakarot_native_token_address.read() + } + .balanceOf(starknet_caller_address); + let max_gas_fee = tx.gas_limit().into() * tx.max_fee_per_gas(); + let tx_cost = tx.value() + max_gas_fee.into(); + assert(tx_cost <= balance, 'Not enough ETH'); + + let effective_gas_price = get_effective_gas_price( + Option::Some(tx.max_fee_per_gas()), + tx.max_priority_fee_per_gas(), + block_base_fee.into() + ); + assert(effective_gas_price.is_ok(), 'Invalid effective gas price'); +} diff --git a/crates/utils/src/eth_transaction.cairo b/crates/utils/src/eth_transaction.cairo index 927b9aa2e..89449c99c 100644 --- a/crates/utils/src/eth_transaction.cairo +++ b/crates/utils/src/eth_transaction.cairo @@ -31,9 +31,9 @@ pub enum TransactTo { /// Get the effective gas price of a transaction as specfified in EIP-1559 with relevant /// checks. -fn get_effective_gas_price( - max_fee_per_gas: Option, max_priority_fee_per_gas: Option, block_base_fee: u256, -) -> Result { +pub fn get_effective_gas_price( + max_fee_per_gas: Option, max_priority_fee_per_gas: Option, block_base_fee: u128, +) -> Result { match max_fee_per_gas { Option::Some(max_fee) => { let max_priority_fee_per_gas = max_priority_fee_per_gas.unwrap_or(0); diff --git a/crates/utils/src/serialization.cairo b/crates/utils/src/serialization.cairo index 2ccf4d15e..c29b2f9f2 100644 --- a/crates/utils/src/serialization.cairo +++ b/crates/utils/src/serialization.cairo @@ -42,7 +42,7 @@ pub fn serialize_transaction_signature( let value = match tx_type { TxType::Legacy(_) => { sig.y_parity.into() + 2 * chain_id + 35 }, - TxType::Eip2930(_)| TxType::Eip1559(_) => { sig.y_parity.into() } + TxType::Eip2930(_) | TxType::Eip1559(_) => { sig.y_parity.into() } }; res.append(value.into());