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/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..c7e7b94 --- /dev/null +++ b/eth_712_account/src/test.cairo @@ -0,0 +1,126 @@ +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, 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 +// ================================ + +#[test] +fn test_initialize_success() { + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // Initialize with valid 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: account_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 (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // First initialization should succeed + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Second initialization should fail + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); +} + +#[test] +#[should_panic(expected: 'INVALID_OWNERSHIP_SIGNATURE')] +fn test_initialize_invalid_signature_reverts() { + let (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // Initialize with invalid 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 (account_address, _) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // Initialize first + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // 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 (account_address, class_hash) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // Initialize first + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // 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(); + account_contract.upgrade(class_hash, Option::None); + + // Verify Upgraded event was emitted + let events = spy.get_events(); + 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"); + 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 (account_address, class_hash) = deploy_eth712_account(); + let account_contract = IAccount712AdminDispatcher { contract_address: account_address }; + + // Initialize first + account_contract.initialize(TEST_ETH_ADDRESS(), get_ownership_signature()); + + // Spoof caller as self for a single call + cheat_caller_address_once(contract_address: account_address, caller_address: account_address); + + 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(); + + account_contract.upgrade(class_hash, Option::Some((eic_class_hash, eic_data))); + + // Verify the custom interface was registered + let src5 = ISRC5Dispatcher { contract_address: account_address }; + assert!(src5.supports_interface(custom_interface_id), "Custom interface not registered"); +} diff --git a/eth_712_account/src/test_utils.cairo b/eth_712_account/src/test_utils.cairo new file mode 100644 index 0000000..269f5bd --- /dev/null +++ b/eth_712_account/src/test_utils.cairo @@ -0,0 +1,59 @@ +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. +/// 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, *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 +}