From 89760c535b16ff191cb0292fe4355735555c470a Mon Sep 17 00:00:00 2001 From: Oba Date: Wed, 28 Aug 2024 09:55:05 +0200 Subject: [PATCH] feat: execute_starknet_call (#871) * feat: execute_starknet_call * doc: use rustdoc format * style: scarb fmt * fix: return false on syscall error --- crates/contracts/src/account_contract.cairo | 22 +++++- crates/contracts/src/errors.cairo | 6 ++ .../src/kakarot_core/interface.cairo | 13 ++++ .../contracts/src/kakarot_core/kakarot.cairo | 15 ++++ .../tests/test_contract_account.cairo | 71 ++++++++++++++++++- 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/crates/contracts/src/account_contract.cairo b/crates/contracts/src/account_contract.cairo index 616c85703..39ff3356f 100644 --- a/crates/contracts/src/account_contract.cairo +++ b/crates/contracts/src/account_contract.cairo @@ -29,6 +29,7 @@ pub trait IAccount { fn storage(self: @TContractState, key: u256) -> u256; 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); } #[starknet::contract(account)] @@ -38,7 +39,7 @@ pub mod AccountContract { use contracts::components::ownable::ownable_component; use contracts::errors::{ BYTECODE_READ_ERROR, BYTECODE_WRITE_ERROR, STORAGE_READ_ERROR, STORAGE_WRITE_ERROR, - NONCE_READ_ERROR, NONCE_WRITE_ERROR, KAKAROT_VALIDATION_FAILED + NONCE_READ_ERROR, NONCE_WRITE_ERROR, KAKAROT_VALIDATION_FAILED, KAKAROT_REENTRANCY }; use contracts::kakarot_core::interface::{IKakarotCoreDispatcher, IKakarotCoreDispatcherTrait}; use contracts::storage::StorageBytecode; @@ -53,7 +54,7 @@ pub mod AccountContract { StoragePointerWriteAccess }; use core::starknet::storage_access::{storage_base_address_from_felt252, StorageBaseAddress}; - use core::starknet::syscalls::{replace_class_syscall}; + 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 @@ -271,6 +272,23 @@ pub mod AccountContract { self.ownable.assert_only_owner(); self.Account_nonce.write(nonce); } + + /// Used to preserve caller in Cairo Precompiles + /// Reentrency check is done for Kakarot contract, only get_starknet_address is allowed + /// for Solidity contracts to be able to get the corresponding Starknet address in their + /// calldata. + fn execute_starknet_call(ref self: ContractState, call: Call) -> (bool, Span) { + self.ownable.assert_only_owner(); + let kakarot_address = self.ownable.owner(); + if call.to == kakarot_address && call.selector != selector!("get_starknet_address") { + return (false, KAKAROT_REENTRANCY.span()); + } + let response = call_contract_syscall(call.to, call.selector, call.calldata); + if response.is_ok() { + return (true, response.unwrap().into()); + } + return (false, response.unwrap_err().into()); + } } #[generate_trait] diff --git a/crates/contracts/src/errors.cairo b/crates/contracts/src/errors.cairo index aca884cf3..22b37eeee 100644 --- a/crates/contracts/src/errors.cairo +++ b/crates/contracts/src/errors.cairo @@ -41,3 +41,9 @@ pub const KAKAROT_VALIDATION_FAILED: [ 'e', 'd' ]; + +pub const KAKAROT_REENTRANCY: [ + felt252 + ; 19] = [ + 'K', 'a', 'k', 'a', 'r', 'o', 't', ':', ' ', 'r', 'e', 'e', 'n', 't', 'r', 'a', 'n', 'c', 'y' +]; diff --git a/crates/contracts/src/kakarot_core/interface.cairo b/crates/contracts/src/kakarot_core/interface.cairo index 9c0363baf..b787d2ae1 100644 --- a/crates/contracts/src/kakarot_core/interface.cairo +++ b/crates/contracts/src/kakarot_core/interface.cairo @@ -56,6 +56,9 @@ pub trait IKakarotCore { fn get_block_gas_limit(self: @TContractState) -> u128; // Getter for the Base Fee fn get_base_fee(self: @TContractState) -> u128; + + // Getter for the Starknet Address + fn get_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; } #[starknet::interface] @@ -105,6 +108,16 @@ pub trait IExtendedKakarotCore { // Setter for the Generic Account Class fn set_account_class_hash(ref self: TContractState, new_class_hash: ClassHash); + fn register_account(ref self: TContractState, evm_address: EthAddress); + + // Getter for the Block Gas Limit + fn get_block_gas_limit(self: @TContractState) -> u128; + // Getter for the Base Fee + fn get_base_fee(self: @TContractState) -> u128; + + // Getter for the Starknet Address + fn get_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; + fn owner(self: @TContractState) -> ContractAddress; fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress); fn renounce_ownership(ref self: TContractState); diff --git a/crates/contracts/src/kakarot_core/kakarot.cairo b/crates/contracts/src/kakarot_core/kakarot.cairo index 8e17dc74b..fcbcd7674 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -231,6 +231,21 @@ pub mod KakarotCore { fn get_base_fee(self: @ContractState) -> u128 { self.Kakarot_base_fee.read() } + + // @notice Returns the corresponding Starknet address for a given EVM address. + // @dev Returns the registered address if there is one, otherwise returns the deterministic + // address got when Kakarot deploys an account. + // @param evm_address The EVM address to transform to a starknet address + // @return starknet_address The Starknet Account Contract address + fn get_starknet_address(self: @ContractState, evm_address: EthAddress) -> ContractAddress { + let registered_starknet_address = self.address_registry(evm_address); + if (registered_starknet_address.is_zero()) { + return registered_starknet_address; + } + + let computed_starknet_address = self.compute_starknet_address(evm_address); + return computed_starknet_address; + } } #[generate_trait] diff --git a/crates/contracts/tests/test_contract_account.cairo b/crates/contracts/tests/test_contract_account.cairo index 6360417f3..1c379610b 100644 --- a/crates/contracts/tests/test_contract_account.cairo +++ b/crates/contracts/tests/test_contract_account.cairo @@ -1,7 +1,14 @@ use contracts::account_contract::{AccountContract, IAccountDispatcher, IAccountDispatcherTrait}; +use contracts::errors::KAKAROT_REENTRANCY; use contracts::test_data::counter_evm_bytecode; -use contracts::test_utils::{setup_contracts_for_testing, deploy_contract_account}; +use contracts::test_utils::{ + setup_contracts_for_testing, deploy_contract_account, fund_account_with_native_token +}; +use core::starknet::ContractAddress; +use core::starknet::account::{Call}; +use core::starknet::testing; use evm::test_utils::{ca_address, native_token}; +use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; use snforge_std::{start_cheat_caller_address, stop_cheat_caller_address}; #[test] @@ -68,3 +75,65 @@ fn test_ca_storage() { assert(storage == expected_storage, 'wrong contract storage'); } + +#[test] +fn test_ca_external_starknet_call_native_token() { + let (native_token, 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 }; + fund_account_with_native_token(ca_address.starknet, native_token, 0x1); + + let call = Call { + to: native_token.contract_address, + selector: selector!("balanceOf"), + calldata: array![ca_address.starknet.into()].span(), + }; + start_cheat_caller_address(ca_address.starknet, kakarot_core.contract_address); + let (success, data) = contract_account.execute_starknet_call(call); + stop_cheat_caller_address(ca_address.starknet); + + assert(success == true, 'execute_starknet_call failed'); + assert(data.len() == 2, 'wrong return data length'); + let balance = native_token.balanceOf(ca_address.starknet); + assert((*data[0], *data[1]) == (balance.low.into(), balance.high.into()), 'wrong return data'); +} + +#[test] +fn test_ca_external_starknet_call_kakarot_get_starknet_address() { + 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 }; + + let call = Call { + to: kakarot_core.contract_address, + selector: selector!("get_starknet_address"), + calldata: array![ca_address.evm.address].span(), + }; + start_cheat_caller_address(ca_address.starknet, kakarot_core.contract_address); + let (success, data) = contract_account.execute_starknet_call(call); + stop_cheat_caller_address(ca_address.starknet); + + assert(success == true, 'execute_starknet_call failed'); + assert(data.len() == 1, 'wrong return data length'); + assert(*data[0] == ca_address.starknet.try_into().unwrap(), 'wrong return data'); +} + +#[test] +fn test_ca_external_starknet_call_cannot_call_kakarot_other_selector() { + 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 }; + + let call = Call { + to: kakarot_core.contract_address, + selector: selector!("get_native_token"), + calldata: array![].span(), + }; + start_cheat_caller_address(ca_address.starknet, kakarot_core.contract_address); + let (success, data) = contract_account.execute_starknet_call(call); + stop_cheat_caller_address(ca_address.starknet); + + assert(success == false, 'execute_starknet_call failed'); + assert(data.len() == 19, 'wrong return data length'); + assert(data == KAKAROT_REENTRANCY.span(), 'wrong return data'); +}