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

Commit 9203b9c

Browse files
authored
feat: safe get evm address (#1018)
* feat: safe get evm address * ignore untestable tests
1 parent ef4de1e commit 9203b9c

File tree

9 files changed

+175
-111
lines changed

9 files changed

+175
-111
lines changed

crates/contracts/src/kakarot_core/eth_rpc.cairo

+107-52
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use core::num::traits::Zero;
22
use core::starknet::get_tx_info;
3-
use core::starknet::{EthAddress, get_caller_address};
3+
use core::starknet::{EthAddress, get_caller_address, ContractAddress};
44
use crate::account_contract::{IAccountDispatcher, IAccountDispatcherTrait};
55
use crate::kakarot_core::interface::IKakarotCore;
66
use crate::kakarot_core::kakarot::{KakarotCore, KakarotCore::{KakarotCoreState}};
77
use evm::backend::starknet_backend;
88
use evm::backend::validation::validate_eth_tx;
9+
use evm::model::account::AccountTrait;
910
use evm::model::{TransactionResult, Address};
1011
use evm::{EVMTrait};
1112
use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait};
@@ -84,21 +85,6 @@ pub trait IEthRPC<T> {
8485
/// * The estimated gas as a u64
8586
fn eth_estimate_gas(self: @T, origin: EthAddress, tx: Transaction) -> (bool, Span<u8>, u64);
8687

87-
//TODO: make this an internal function. The account contract should call
88-
//eth_send_raw_transaction.
89-
/// Executes a transaction and possibly modifies the state.
90-
///
91-
/// # Arguments
92-
///
93-
/// * `tx` - The transaction object
94-
///
95-
/// # Returns
96-
///
97-
/// A tuple containing:
98-
/// * A boolean indicating success
99-
/// * The return data as a Span<u8>
100-
/// * The amount of gas used as a u64
101-
fn eth_send_transaction(ref self: T, tx: Transaction) -> (bool, Span<u8>, u64);
10288

10389
/// Executes an unsigned transaction.
10490
///
@@ -153,9 +139,7 @@ pub impl EthRPC<
153139
core::panic_with_felt252('fn must be called, not invoked');
154140
};
155141

156-
let origin = Address {
157-
evm: origin, starknet: kakarot_state.compute_starknet_address(origin)
158-
};
142+
let origin = Address { evm: origin, starknet: kakarot_state.get_starknet_address(origin) };
159143

160144
let TransactionResult { success, return_data, gas_used, state: _state } =
161145
EVMTrait::process_transaction(
@@ -171,16 +155,49 @@ pub impl EthRPC<
171155
panic!("unimplemented")
172156
}
173157

174-
//TODO: make this one internal, and the eth_send_raw_unsigned_tx one public
175-
fn eth_send_transaction(
176-
ref self: TContractState, mut tx: Transaction
158+
//TODO: we can't really unit-test this with foundry because we can't generate the RLP-encoding
159+
//in Cairo Find another way - perhaps test-data gen with python?
160+
fn eth_send_raw_unsigned_tx(
161+
ref self: TContractState, mut tx_data: Span<u8>
177162
) -> (bool, Span<u8>, u64) {
163+
let tx = TransactionTrait::decode_enveloped(ref tx_data).expect('EOA: could not decode tx');
164+
EthRPCInternal::eth_send_transaction(ref self, tx)
165+
}
166+
}
167+
168+
trait EthRPCInternal<T> {
169+
/// Executes a transaction and possibly modifies the state.
170+
///
171+
/// This function implements the `eth_sendTransaction` method as described in the Ethereum
172+
/// JSON-RPC specification.
173+
/// The nonce is taken from the corresponding account contract.
174+
///
175+
/// # Arguments
176+
///
177+
/// * `tx` - A `Transaction` struct
178+
///
179+
/// # Returns
180+
///
181+
/// A tuple containing:
182+
/// * A boolean indicating success (TRUE if the transaction succeeded, FALSE otherwise)
183+
/// * The return data as a `Span<u8>`
184+
/// * The amount of gas used by the transaction as a `u64`
185+
fn eth_send_transaction(ref self: T, tx: Transaction) -> (bool, Span<u8>, u64);
186+
}
187+
188+
impl EthRPCInternalImpl<
189+
TContractState, impl KakarotState: KakarotCoreState<TContractState>, +Drop<TContractState>
190+
> of EthRPCInternal<TContractState> {
191+
fn eth_send_transaction(ref self: TContractState, tx: Transaction) -> (bool, Span<u8>, u64) {
178192
let mut kakarot_state = KakarotState::get_state();
179193
let intrinsic_gas = validate_eth_tx(@kakarot_state, tx);
180194

181195
let starknet_caller_address = get_caller_address();
182-
let account = IAccountDispatcher { contract_address: starknet_caller_address };
183-
let origin = Address { evm: account.get_evm_address(), starknet: starknet_caller_address };
196+
// panics if the caller is a spoofer of an EVM address.
197+
//TODO: e2e test this! :) Send a transaction from an account that is not Kakarot's account
198+
//(e.g. deploy an account but not from Kakarot)
199+
let origin_evm_address = safe_get_evm_address(@self, starknet_caller_address);
200+
let origin = Address { evm: origin_evm_address, starknet: starknet_caller_address };
184201

185202
let TransactionResult { success, return_data, gas_used, mut state } =
186203
EVMTrait::process_transaction(
@@ -189,29 +206,40 @@ pub impl EthRPC<
189206
starknet_backend::commit(ref state).expect('Committing state failed');
190207
(success, return_data, gas_used)
191208
}
192-
193-
//TODO: we can't really unit-test this with foundry because we can't generate the RLP-encoding
194-
//in Cairo Find another way - perhaps test-data gen with python?
195-
fn eth_send_raw_unsigned_tx(
196-
ref self: TContractState, mut tx_data: Span<u8>
197-
) -> (bool, Span<u8>, u64) {
198-
let tx = TransactionTrait::decode_enveloped(ref tx_data).expect('EOA: could not decode tx');
199-
Self::eth_send_transaction(ref self, tx)
200-
}
201209
}
202210

203-
trait IEthRPCInternal<T> {
204-
fn eth_send_transaction(
205-
ref self: T, origin: EthAddress, tx: Transaction
206-
) -> (bool, Span<u8>, u64);
207-
}
208211

209-
impl EthRPCInternalImpl<TContractState, +Drop<TContractState>> of IEthRPCInternal<TContractState> {
210-
fn eth_send_transaction(
211-
ref self: TContractState, origin: EthAddress, tx: Transaction
212-
) -> (bool, Span<u8>, u64) {
213-
panic!("unimplemented")
214-
}
212+
/// Returns the EVM address associated with a Starknet account deployed by Kakarot.
213+
///
214+
/// This function prevents cases where a Starknet account has an entrypoint `get_evm_address()`
215+
/// but isn't part of the Kakarot system. It also mitigates re-entrancy risk with the Cairo Interop
216+
/// module.
217+
///
218+
/// # Arguments
219+
///
220+
/// * `starknet_address` - The Starknet address of the account
221+
///
222+
/// # Returns
223+
///
224+
/// * `EthAddress` - The associated EVM address
225+
///
226+
/// # Panics
227+
///
228+
/// Panics if the declared corresponding EVM address (retrieved with `get_evm_address`)
229+
/// does not recompute into the actual caller address.
230+
fn safe_get_evm_address<
231+
TContractState, impl KakarotState: KakarotCoreState<TContractState>, +Drop<TContractState>
232+
>(
233+
self: @TContractState, starknet_address: ContractAddress
234+
) -> EthAddress {
235+
let account = IAccountDispatcher { contract_address: starknet_address };
236+
let evm_address = account.get_evm_address();
237+
let safe_starknet_address = AccountTrait::get_starknet_address(evm_address);
238+
assert!(
239+
safe_starknet_address == starknet_address,
240+
"Kakarot: caller contract is not a Kakarot Account"
241+
);
242+
evm_address
215243
}
216244

217245
fn is_view(self: @KakarotCore::ContractState) -> bool {
@@ -228,16 +256,19 @@ fn is_view(self: @KakarotCore::ContractState) -> bool {
228256

229257
#[cfg(test)]
230258
mod tests {
259+
use core::ops::DerefMut;
260+
use core::starknet::EthAddress;
261+
use core::starknet::storage::{StoragePathEntry, StoragePointerWriteAccess};
231262
use crate::kakarot_core::KakarotCore;
232263
use crate::kakarot_core::eth_rpc::IEthRPC;
233-
use crate::kakarot_core::interface::IExtendedKakarotCoreDispatcherTrait;
264+
use crate::kakarot_core::interface::{IKakarotCore, IExtendedKakarotCoreDispatcherTrait};
234265
use crate::test_utils::{setup_contracts_for_testing, fund_account_with_native_token};
235266
use evm::test_utils::{sequencer_evm_address, evm_address, uninitialized_account};
236267
use snforge_std::{
237268
start_mock_call, start_cheat_chain_id_global, stop_cheat_chain_id_global, test_address
238269
};
270+
use super::safe_get_evm_address;
239271
use utils::constants::POW_2_53;
240-
use utils::helpers::compute_starknet_address;
241272

242273
fn set_up() -> KakarotCore::ContractState {
243274
// Define the kakarot state to access contract functions
@@ -253,12 +284,7 @@ mod tests {
253284
#[test]
254285
fn test_eth_get_transaction_count() {
255286
let kakarot_state = set_up();
256-
// Deployed eoa should return a zero nonce
257-
let starknet_address = compute_starknet_address(
258-
test_address(),
259-
evm_address(),
260-
0.try_into().unwrap() // Using 0 as the kakarot storage is empty
261-
);
287+
let starknet_address = kakarot_state.get_starknet_address(evm_address());
262288
start_mock_call::<u256>(starknet_address, selector!("get_nonce"), 1);
263289
assert_eq!(kakarot_state.eth_get_transaction_count(evm_address()), 1);
264290
}
@@ -304,4 +330,33 @@ mod tests {
304330
);
305331
tear_down();
306332
}
333+
334+
#[test]
335+
fn test_safe_get_evm_address_succeeds() {
336+
let kakarot_state = set_up();
337+
// no registry - returns the computed address
338+
let starknet_address = kakarot_state.get_starknet_address(evm_address());
339+
start_mock_call::<
340+
EthAddress
341+
>(starknet_address, selector!("get_evm_address"), evm_address());
342+
let safe_evm_address = safe_get_evm_address(@kakarot_state, starknet_address);
343+
assert_eq!(safe_evm_address, evm_address());
344+
}
345+
346+
#[test]
347+
#[should_panic(expected: "Kakarot: caller contract is not a Kakarot Account")]
348+
fn test_safe_get_evm_address_panics_when_caller_is_not_kakarot_account() {
349+
let mut kakarot_state = set_up();
350+
let mut kakarot_storage = kakarot_state.deref_mut();
351+
352+
// Calling get_evm_address() on a fake starknet account that will return `evm_address()`.
353+
// Then, when computing the deterministic starknet_address with get_starknet_address(), it
354+
// will return a different address.
355+
// This should fail.
356+
let fake_starknet_account = 'fake_account'.try_into().unwrap();
357+
start_mock_call::<
358+
EthAddress
359+
>(fake_starknet_account, selector!("get_evm_address"), evm_address());
360+
safe_get_evm_address(@kakarot_state, fake_starknet_account);
361+
}
307362
}

crates/contracts/src/kakarot_core/interface.cairo

+12-11
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ pub trait IKakarotCore<TContractState> {
99
/// Gets the native token used by the Kakarot smart contract
1010
fn get_native_token(self: @TContractState) -> ContractAddress;
1111

12-
/// Deterministically computes a Starknet address for a given EVM address
13-
/// The address is computed as the Starknet address corresponding to the deployment of an EOA,
14-
/// Using its EVM address as salt, and KakarotCore as deployer.
15-
fn compute_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress;
16-
1712
/// Checks into KakarotCore storage if an EOA or a CA has been deployed for
1813
/// a particular EVM address and. If so returns its corresponding address,
1914
/// otherwise returns 0
@@ -47,7 +42,18 @@ pub trait IKakarotCore<TContractState> {
4742
/// Setter for the base fee
4843
fn set_base_fee(ref self: TContractState, base_fee: u64);
4944

50-
// Getter for the Starknet Address
45+
/// Returns the corresponding Starknet address for a given EVM address.
46+
///
47+
/// Returns the registered address if there is one, otherwise returns the deterministic
48+
/// address got when Kakarot deploys an account.
49+
///
50+
/// # Arguments
51+
///
52+
/// * `evm_address` - The EVM address to transform to a starknet address
53+
///
54+
/// # Returns
55+
///
56+
/// * `ContractAddress` - The Starknet Account Contract address
5157
fn get_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress;
5258
}
5359

@@ -59,11 +65,6 @@ pub trait IExtendedKakarotCore<TContractState> {
5965
/// Gets the native token used by the Kakarot smart contract
6066
fn get_native_token(self: @TContractState) -> ContractAddress;
6167

62-
/// Deterministically computes a Starknet address for a given EVM address
63-
/// The address is computed as the Starknet address corresponding to the deployment of an EOA,
64-
/// Using its EVM address as salt, and KakarotCore as deployer.
65-
fn compute_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress;
66-
6768
/// Checks into KakarotCore storage if an EOA or a CA has been deployed for
6869
/// a particular EVM address and. If so returns its corresponding address,
6970
/// otherwise returns 0

crates/contracts/src/kakarot_core/kakarot.cairo

+8-22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod KakarotCore {
1616
use crate::kakarot_core::eth_rpc;
1717
use crate::kakarot_core::interface::IKakarotCore;
1818
use evm::backend::starknet_backend;
19+
use evm::model::account::AccountTrait;
1920
use utils::helpers::compute_starknet_address;
2021

2122
component!(path: ownable_component, storage: ownable, event: OwnableEvent);
@@ -134,15 +135,6 @@ pub mod KakarotCore {
134135
self.Kakarot_native_token_address.read()
135136
}
136137

137-
fn compute_starknet_address(
138-
self: @ContractState, evm_address: EthAddress
139-
) -> ContractAddress {
140-
let kakarot_address = get_contract_address();
141-
compute_starknet_address(
142-
kakarot_address, evm_address, self.Kakarot_uninitialized_account_class_hash.read()
143-
)
144-
}
145-
146138
fn address_registry(self: @ContractState, evm_address: EthAddress) -> ContractAddress {
147139
self.Kakarot_evm_to_starknet_address.read(evm_address)
148140
}
@@ -184,7 +176,11 @@ pub mod KakarotCore {
184176
let existing_address = self.Kakarot_evm_to_starknet_address.read(evm_address);
185177
assert(existing_address.is_zero(), 'Account already exists');
186178

187-
let starknet_address = self.compute_starknet_address(evm_address);
179+
let starknet_address = compute_starknet_address(
180+
get_contract_address(),
181+
evm_address,
182+
self.Kakarot_uninitialized_account_class_hash.read()
183+
);
188184
assert!(
189185
starknet_address == get_caller_address(), "Account must be registered by the caller"
190186
);
@@ -206,19 +202,9 @@ pub mod KakarotCore {
206202
self.Kakarot_base_fee.read()
207203
}
208204

209-
// @notice Returns the corresponding Starknet address for a given EVM address.
210-
// @dev Returns the registered address if there is one, otherwise returns the deterministic
211-
// address got when Kakarot deploys an account.
212-
// @param evm_address The EVM address to transform to a starknet address
213-
// @return starknet_address The Starknet Account Contract address
214-
fn get_starknet_address(self: @ContractState, evm_address: EthAddress) -> ContractAddress {
215-
let registered_starknet_address = self.address_registry(evm_address);
216-
if (!registered_starknet_address.is_zero()) {
217-
return registered_starknet_address;
218-
}
219205

220-
let computed_starknet_address = self.compute_starknet_address(evm_address);
221-
return computed_starknet_address;
206+
fn get_starknet_address(self: @ContractState, evm_address: EthAddress) -> ContractAddress {
207+
AccountTrait::get_starknet_address(evm_address)
222208
}
223209
}
224210

crates/contracts/tests/test_execution_from_outside.cairo

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use snforge_std::{
2020
start_cheat_caller_address, stop_cheat_caller_address, start_cheat_transaction_hash, spy_events,
2121
EventSpyTrait, CheatSpan, cheat_caller_address, stop_cheat_block_timestamp,
2222
start_cheat_block_timestamp, start_cheat_chain_id_global, stop_cheat_chain_id_global,
23-
start_cheat_caller_address_global, stop_cheat_caller_address_global
23+
stop_cheat_caller_address_global
2424
};
2525

2626
use snforge_utils::snforge_utils::EventsFilterBuilderTrait;

0 commit comments

Comments
 (0)