From 6badd887427972675476fa32411b5ad77959f19c Mon Sep 17 00:00:00 2001 From: Remo Date: Sun, 15 Feb 2026 18:58:52 +0200 Subject: [PATCH 1/2] EthAccount tests for initialization & upgrade --- eth_712_account/src/lib.cairo | 5 + eth_712_account/src/test.cairo | 133 +++++++++++++++++++++++++++ eth_712_account/src/test_utils.cairo | 63 +++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 eth_712_account/src/test.cairo create mode 100644 eth_712_account/src/test_utils.cairo diff --git a/eth_712_account/src/lib.cairo b/eth_712_account/src/lib.cairo index 1ee6723..f52d84d 100644 --- a/eth_712_account/src/lib.cairo +++ b/eth_712_account/src/lib.cairo @@ -2,3 +2,8 @@ pub mod eth_712_account; pub mod eth_712_utils; pub mod interface; pub mod register_interfaces_eic; + +#[cfg(test)] +mod test; +#[cfg(test)] +mod test_utils; diff --git a/eth_712_account/src/test.cairo b/eth_712_account/src/test.cairo new file mode 100644 index 0000000..99b7d89 --- /dev/null +++ b/eth_712_account/src/test.cairo @@ -0,0 +1,133 @@ +use openzeppelin::account::extensions::src9::interface::ISRC9_V2_ID; +use openzeppelin::account::interface::ISRC6_ID; +use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; +use snforge_std::{EventSpyTrait, spy_events, start_cheat_caller_address, stop_cheat_caller_address}; +use crate::interface::{IAccount712AdminDispatcher, IAccount712AdminDispatcherTrait}; +use crate::test_utils::{ + TEST_ETH_ADDRESS, WRONG_ETH_ADDRESS, deploy_eth712_account, get_invalid_signature, + get_ownership_signature, +}; + +// ================================ +// initialize tests +// ================================ + +#[test] +fn test_initialize_success() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize with valid signature + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Verify interfaces are registered + let src5 = ISRC5Dispatcher { contract_address }; + assert!(src5.supports_interface(ISRC9_V2_ID), "ISRC9_V2_ID not registered"); + assert!(src5.supports_interface(ISRC6_ID), "ISRC6_ID not registered"); +} + +#[test] +#[should_panic(expected: 'ALREADY_INITIALIZED')] +fn test_initialize_already_initialized_reverts() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // First initialization should succeed + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Second initialization should fail + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); +} + +#[test] +#[should_panic(expected: 'INVALID_OWNERSHIP_SIGNATURE')] +fn test_initialize_invalid_signature_reverts() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize with invalid signature + dispatcher.initialize(TEST_ETH_ADDRESS(), get_invalid_signature()); +} + +#[test] +#[should_panic(expected: 'INVALID_OWNERSHIP_SIGNATURE')] +fn test_initialize_wrong_address_reverts() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize with correct signature but wrong address + dispatcher.initialize(WRONG_ETH_ADDRESS(), get_ownership_signature()); +} + +// ================================ +// upgrade tests +// ================================ + +#[test] +#[should_panic(expected: 'UNAUTHORIZED')] +fn test_upgrade_unauthorized_reverts() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize first + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Try to upgrade from external caller (not self) + let new_class_hash = crate::test_utils::declare_eth712_account(); + dispatcher.upgrade(new_class_hash, Option::None); +} + +#[test] +fn test_upgrade_from_self_succeeds() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize first + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Spoof caller as self + start_cheat_caller_address(contract_address, contract_address); + + let mut spy = spy_events(); + let new_class_hash = crate::test_utils::declare_eth712_account(); + dispatcher.upgrade(new_class_hash, Option::None); + + // Verify an event was emitted (Upgraded event) + let events = spy.get_events(); + assert!(events.events.len() > 0, "No events emitted"); + + // Verify the event data contains the new class hash + let (from, event) = events.events.at(0); + assert!(*from == contract_address, "Event from wrong address"); + assert!(event.data.len() > 0, "Event has no data"); + assert!(*event.data.at(0) == new_class_hash.into(), "Wrong class hash in event"); + + stop_cheat_caller_address(contract_address); +} + +#[test] +fn test_upgrade_with_eic() { + let contract_address = deploy_eth712_account(); + let dispatcher = IAccount712AdminDispatcher { contract_address }; + + // Initialize first + dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Spoof caller as self + start_cheat_caller_address(contract_address, contract_address); + + let new_class_hash = crate::test_utils::declare_eth712_account(); + let eic_class_hash = crate::test_utils::declare_register_interfaces_eic(); + + // Register a custom interface via EIC + let custom_interface_id: felt252 = 0x12345678; + let eic_data: Span = array![custom_interface_id].span(); + + dispatcher.upgrade(new_class_hash, Option::Some((eic_class_hash, eic_data))); + + // Verify the custom interface was registered + let src5 = ISRC5Dispatcher { contract_address }; + assert!(src5.supports_interface(custom_interface_id), "Custom interface not registered"); + + stop_cheat_caller_address(contract_address); +} diff --git a/eth_712_account/src/test_utils.cairo b/eth_712_account/src/test_utils.cairo new file mode 100644 index 0000000..94345e0 --- /dev/null +++ b/eth_712_account/src/test_utils.cairo @@ -0,0 +1,63 @@ +use snforge_std::{ContractClassTrait, DeclareResultTrait}; +use starknet::secp256_trait::Signature; +use starknet::{ClassHash, ContractAddress, EthAddress, SyscallResultTrait}; + +/// Test Ethereum address corresponding to private key: +/// 0xa6d86467b6ec9e161649b27edfd8519e75a2e1cf5f4c309c628706e6999780e8 +/// Address: 0xbF60187c5dFfA627249f1C3000A4168dbB9D7A1A +pub fn TEST_ETH_ADDRESS() -> EthAddress { + 0xbF60187c5dFfA627249f1C3000A4168dbB9D7A1A_felt252.try_into().unwrap() +} + +/// A different Ethereum address for testing invalid cases. +pub fn WRONG_ETH_ADDRESS() -> EthAddress { + 0x1234567890123456789012345678901234567890_felt252.try_into().unwrap() +} + +/// Returns the ownership signature for TEST_ETH_ADDRESS. +/// This signature was generated by signing the OWNERSHIP_TRANSFER_MSG_HASH +/// (0x3ce976d55131cd0bdd49f20afbded052d8e907dc6034d95cdf117a8fd7752e3c) +/// with the test private key. +/// +/// Values from: split_raw_eth_sig_to_felts output +/// '0xda47d5165a4577a024d18b8d61c5fe53 0xe994c0e202b390bddaffacf04bdea826 +/// 0x46dd0af587023ccad738aa0a82b05f98 0x2e117624d8cc474d0641c3168944eb67 0x1' +pub fn get_ownership_signature() -> Signature { + Signature { + r: u256 { + low: 0xda47d5165a4577a024d18b8d61c5fe53, high: 0xe994c0e202b390bddaffacf04bdea826, + }, + s: u256 { + low: 0x46dd0af587023ccad738aa0a82b05f98, high: 0x2e117624d8cc474d0641c3168944eb67, + }, + y_parity: true // v % 2 == 0 + } +} + +/// Returns an invalid signature for testing. +pub fn get_invalid_signature() -> Signature { + Signature { + r: u256 { low: 0x1234567890abcdef, high: 0xfedcba0987654321 }, + s: u256 { low: 0xabcdef1234567890, high: 0x1234567890abcdef }, + y_parity: false, + } +} + +/// Declare and deploy the StarknetEth712Account contract. +pub fn deploy_eth712_account() -> ContractAddress { + let contract_class = snforge_std::declare("StarknetEth712Account") + .unwrap_syscall() + .contract_class(); + let (contract_address, _) = contract_class.deploy(@array![]).unwrap_syscall(); + contract_address +} + +/// Declare the StarknetEth712Account contract and return its class hash. +pub fn declare_eth712_account() -> ClassHash { + *snforge_std::declare("StarknetEth712Account").unwrap_syscall().contract_class().class_hash +} + +/// Declare the RegisterInterfacesEIC contract and return its class hash. +pub fn declare_register_interfaces_eic() -> ClassHash { + *snforge_std::declare("RegisterInterfacesEIC").unwrap_syscall().contract_class().class_hash +} From 56ec34e98905abca07e1ba051bc2d8f4f3beb19a Mon Sep 17 00:00:00 2001 From: Remo Date: Mon, 16 Feb 2026 15:33:16 +0200 Subject: [PATCH 2/2] review comments --- Scarb.lock | 2 + eth_712_account/Scarb.toml | 2 + eth_712_account/src/eth_712_account.cairo | 1 + eth_712_account/src/test.cairo | 113 ++++++++++------------ eth_712_account/src/test_utils.cairo | 10 +- 5 files changed, 61 insertions(+), 67 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 3f440d4..f7ee919 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -38,6 +38,8 @@ version = "0.1.0" dependencies = [ "openzeppelin", "snforge_std", + "starkware_utils_testing", + "testing_utils", ] [[package]] diff --git a/eth_712_account/Scarb.toml b/eth_712_account/Scarb.toml index f08f6b4..8b4bb7a 100644 --- a/eth_712_account/Scarb.toml +++ b/eth_712_account/Scarb.toml @@ -10,6 +10,8 @@ starknet.workspace = true [dev-dependencies] assert_macros.workspace = true snforge_std.workspace = true +starkware_utils_testing.workspace = true +testing_utils = { path = "../testing_utils" } [[target.starknet-contract]] sierra = true diff --git a/eth_712_account/src/eth_712_account.cairo b/eth_712_account/src/eth_712_account.cairo index 70f7579..bb1f89e 100644 --- a/eth_712_account/src/eth_712_account.cairo +++ b/eth_712_account/src/eth_712_account.cairo @@ -79,6 +79,7 @@ pub mod StarknetEth712Account { // Register Account interface (ISRC6) so that we can receive 721/1155 tokens. self.src5.register_interface(ISRC6_ID); } + fn upgrade( ref self: ContractState, new_class_hash: ClassHash, diff --git a/eth_712_account/src/test.cairo b/eth_712_account/src/test.cairo index 99b7d89..c7e7b94 100644 --- a/eth_712_account/src/test.cairo +++ b/eth_712_account/src/test.cairo @@ -1,12 +1,15 @@ +use eth_712_account::interface::{IAccount712AdminDispatcher, IAccount712AdminDispatcherTrait}; +use eth_712_account::test_utils::{ + TEST_ETH_ADDRESS, declare_register_interfaces_eic, deploy_eth712_account, get_invalid_signature, + get_ownership_signature, +}; use openzeppelin::account::extensions::src9::interface::ISRC9_V2_ID; use openzeppelin::account::interface::ISRC6_ID; use openzeppelin::introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; -use snforge_std::{EventSpyTrait, spy_events, start_cheat_caller_address, stop_cheat_caller_address}; -use crate::interface::{IAccount712AdminDispatcher, IAccount712AdminDispatcherTrait}; -use crate::test_utils::{ - TEST_ETH_ADDRESS, WRONG_ETH_ADDRESS, deploy_eth712_account, get_invalid_signature, - get_ownership_signature, -}; +use snforge_std::{EventSpyTrait, load, spy_events}; +use starknet::EthAddress; +use starkware_utils_testing::test_utils::cheat_caller_address_once; +use testing_utils::event_helpers::get_event_by_selector; // ================================ // initialize tests @@ -14,14 +17,19 @@ use crate::test_utils::{ #[test] fn test_initialize_success() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // Initialize with valid signature - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Verify eth_address was stored correctly by reading storage directly + let stored_values = load(account_address, selector!("eth_address"), 1); + let stored_eth_address: EthAddress = (*stored_values.at(0)).try_into().unwrap(); + assert!(stored_eth_address == TEST_ETH_ADDRESS(), "eth_address not stored correctly"); // Verify interfaces are registered - let src5 = ISRC5Dispatcher { contract_address }; + let src5 = ISRC5Dispatcher { contract_address: account_address }; assert!(src5.supports_interface(ISRC9_V2_ID), "ISRC9_V2_ID not registered"); assert!(src5.supports_interface(ISRC6_ID), "ISRC6_ID not registered"); } @@ -29,105 +37,90 @@ fn test_initialize_success() { #[test] #[should_panic(expected: 'ALREADY_INITIALIZED')] fn test_initialize_already_initialized_reverts() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // First initialization should succeed - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); // Second initialization should fail - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); } #[test] #[should_panic(expected: 'INVALID_OWNERSHIP_SIGNATURE')] fn test_initialize_invalid_signature_reverts() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // Initialize with invalid signature - dispatcher.initialize(TEST_ETH_ADDRESS(), get_invalid_signature()); -} - -#[test] -#[should_panic(expected: 'INVALID_OWNERSHIP_SIGNATURE')] -fn test_initialize_wrong_address_reverts() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; - - // Initialize with correct signature but wrong address - dispatcher.initialize(WRONG_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_invalid_signature()); } // ================================ // upgrade tests // ================================ +const DUMMY_CLASS_HASH: felt252 = 'DUMMY_CLASS_HASH'; + #[test] #[should_panic(expected: 'UNAUTHORIZED')] fn test_upgrade_unauthorized_reverts() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // Initialize first - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); - // Try to upgrade from external caller (not self) - let new_class_hash = crate::test_utils::declare_eth712_account(); - dispatcher.upgrade(new_class_hash, Option::None); + // Try to upgrade from external caller (not self) - reverts before checking class hash + account_contract.upgrade(DUMMY_CLASS_HASH.try_into().unwrap(), Option::None); } #[test] fn test_upgrade_from_self_succeeds() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, class_hash) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // Initialize first - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); - // Spoof caller as self - start_cheat_caller_address(contract_address, contract_address); + // Spoof caller as self for a single call + cheat_caller_address_once(contract_address: account_address, caller_address: account_address); let mut spy = spy_events(); - let new_class_hash = crate::test_utils::declare_eth712_account(); - dispatcher.upgrade(new_class_hash, Option::None); + account_contract.upgrade(class_hash, Option::None); - // Verify an event was emitted (Upgraded event) + // Verify Upgraded event was emitted let events = spy.get_events(); - assert!(events.events.len() > 0, "No events emitted"); - - // Verify the event data contains the new class hash - let (from, event) = events.events.at(0); - assert!(*from == contract_address, "Event from wrong address"); + let event_option = get_event_by_selector(events.events.span(), selector!("Upgraded")); + assert!(event_option.is_some(), "Upgraded event not found"); + let (from, event) = event_option.unwrap(); + assert!(*from == account_address, "Event from wrong address"); assert!(event.data.len() > 0, "Event has no data"); - assert!(*event.data.at(0) == new_class_hash.into(), "Wrong class hash in event"); - - stop_cheat_caller_address(contract_address); + let class_hash_felt: felt252 = class_hash.into(); + assert!(*event.data.at(0) == class_hash_felt, "Wrong class hash in event"); } #[test] fn test_upgrade_with_eic() { - let contract_address = deploy_eth712_account(); - let dispatcher = IAccount712AdminDispatcher { contract_address }; + let (account_address, class_hash) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; // Initialize first - dispatcher.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); - // Spoof caller as self - start_cheat_caller_address(contract_address, contract_address); + // Spoof caller as self for a single call + cheat_caller_address_once(contract_address: account_address, caller_address: account_address); - let new_class_hash = crate::test_utils::declare_eth712_account(); - let eic_class_hash = crate::test_utils::declare_register_interfaces_eic(); + let eic_class_hash = declare_register_interfaces_eic(); // Register a custom interface via EIC let custom_interface_id: felt252 = 0x12345678; let eic_data: Span = array![custom_interface_id].span(); - dispatcher.upgrade(new_class_hash, Option::Some((eic_class_hash, eic_data))); + account_contract.upgrade(class_hash, Option::Some((eic_class_hash, eic_data))); // Verify the custom interface was registered - let src5 = ISRC5Dispatcher { contract_address }; + let src5 = ISRC5Dispatcher { contract_address: account_address }; assert!(src5.supports_interface(custom_interface_id), "Custom interface not registered"); - - stop_cheat_caller_address(contract_address); } diff --git a/eth_712_account/src/test_utils.cairo b/eth_712_account/src/test_utils.cairo index 94345e0..269f5bd 100644 --- a/eth_712_account/src/test_utils.cairo +++ b/eth_712_account/src/test_utils.cairo @@ -44,17 +44,13 @@ pub fn get_invalid_signature() -> Signature { } /// Declare and deploy the StarknetEth712Account contract. -pub fn deploy_eth712_account() -> ContractAddress { +/// Returns the contract address and the class hash. +pub fn deploy_eth712_account() -> (ContractAddress, ClassHash) { let contract_class = snforge_std::declare("StarknetEth712Account") .unwrap_syscall() .contract_class(); let (contract_address, _) = contract_class.deploy(@array![]).unwrap_syscall(); - contract_address -} - -/// Declare the StarknetEth712Account contract and return its class hash. -pub fn declare_eth712_account() -> ClassHash { - *snforge_std::declare("StarknetEth712Account").unwrap_syscall().contract_class().class_hash + (contract_address, *contract_class.class_hash) } /// Declare the RegisterInterfacesEIC contract and return its class hash.