From 88022fd7ea0c72d668dc14a6f0c5d960223eba69 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 7 Nov 2024 14:48:36 +0100 Subject: [PATCH] feat: add src9 component to account presets --- docs/modules/ROOT/pages/api/account.adoc | 8 +- packages/presets/Scarb.toml | 1 + packages/presets/src/account.cairo | 15 +- packages/presets/src/eth_account.cairo | 16 +- packages/presets/src/interfaces/account.cairo | 7 + .../presets/src/interfaces/eth_account.cairo | 7 + packages/presets/src/tests/test_account.cairo | 218 ++++++++++++++- .../presets/src/tests/test_eth_account.cairo | 253 ++++++++++++++++-- packages/test_common/src/mocks/simple.cairo | 8 + 9 files changed, 497 insertions(+), 36 deletions(-) diff --git a/docs/modules/ROOT/pages/api/account.adoc b/docs/modules/ROOT/pages/api/account.adoc index 2c6643bf5..f1004e1dc 100644 --- a/docs/modules/ROOT/pages/api/account.adoc +++ b/docs/modules/ROOT/pages/api/account.adoc @@ -704,7 +704,8 @@ Initializes the account by registering the `ISRC9_V2` interface Id. use openzeppelin_presets::AccountUpgradeable; ``` -Upgradeable account contract leveraging xref:#AccountComponent[AccountComponent]. +Upgradeable account which can change its public key and declare, deploy, or call +contracts. Supports outside execution by implementing xref:#SRC9Component[SRC9]. include::../utils/_class_hashes.adoc[] @@ -726,6 +727,7 @@ include::../utils/_class_hashes.adoc[] .AccountComponent * xref:#AccountComponent-Embeddable-Mixin-Impl[`++AccountMixinImpl++`] +* xref:#SRC9Component-Embeddable-Impls-OutsideExecutionV2Impl[`++OutsideExecutionV2Impl++`] -- [.contract-index] @@ -765,7 +767,8 @@ Requirements: use openzeppelin_presets::EthAccountUpgradeable; ``` -Upgradeable account contract leveraging xref:#EthAccountComponent[EthAccountComponent]. +Upgradeable account which can change its public key and declare, deploy, or call contracts, using Ethereum +signing keys. Supports outside execution by implementing xref:#SRC9Component[SRC9]. NOTE: The `EthPublicKey` type is an alias for `starknet::secp256k1::Secp256k1Point`. @@ -789,6 +792,7 @@ include::../utils/_class_hashes.adoc[] .EthAccountComponent * xref:#EthAccountComponent-Embeddable-Mixin-Impl[`++EthAccountMixinImpl++`] +* xref:#SRC9Component-Embeddable-Impls-OutsideExecutionV2Impl[`++OutsideExecutionV2Impl++`] -- [.contract-index] diff --git a/packages/presets/Scarb.toml b/packages/presets/Scarb.toml index 7ed4414f4..505d8e3bb 100644 --- a/packages/presets/Scarb.toml +++ b/packages/presets/Scarb.toml @@ -49,6 +49,7 @@ build-external-contracts = [ "openzeppelin_test_common::mocks::account::DualCaseAccountMock", "openzeppelin_test_common::mocks::account::SnakeAccountMock", "openzeppelin_test_common::mocks::account::SnakeEthAccountMock", + "openzeppelin_test_common::mocks::simple::SimpleMock", "openzeppelin_test_common::mocks::erc20::DualCaseERC20Mock", "openzeppelin_test_common::mocks::erc20::SnakeERC20Mock", "openzeppelin_test_common::mocks::erc721::SnakeERC721Mock", diff --git a/packages/presets/src/account.cairo b/packages/presets/src/account.cairo index 135573428..57a73a9a8 100644 --- a/packages/presets/src/account.cairo +++ b/packages/presets/src/account.cairo @@ -4,10 +4,11 @@ /// # Account Preset /// /// OpenZeppelin's upgradeable account which can change its public key and declare, deploy, or call -/// contracts. +/// contracts. Supports outside execution by implementing SRC9. #[starknet::contract(account)] pub mod AccountUpgradeable { use openzeppelin_account::AccountComponent; + use openzeppelin_account::extensions::SRC9Component; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_upgrades::UpgradeableComponent; use openzeppelin_upgrades::interface::IUpgradeable; @@ -15,6 +16,7 @@ pub mod AccountUpgradeable { component!(path: AccountComponent, storage: account, event: AccountEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: SRC9Component, storage: src9, event: SRC9Event); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); // Account Mixin @@ -23,6 +25,12 @@ pub mod AccountUpgradeable { AccountComponent::AccountMixinImpl; impl AccountInternalImpl = AccountComponent::InternalImpl; + // SRC9 + #[abi(embed_v0)] + impl OutsideExecutionV2Impl = + SRC9Component::OutsideExecutionV2Impl; + impl OutsideExecutionInternalImpl = SRC9Component::InternalImpl; + // Upgradeable impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; @@ -33,6 +41,8 @@ pub mod AccountUpgradeable { #[substorage(v0)] pub src5: SRC5Component::Storage, #[substorage(v0)] + pub src9: SRC9Component::Storage, + #[substorage(v0)] pub upgradeable: UpgradeableComponent::Storage } @@ -44,12 +54,15 @@ pub mod AccountUpgradeable { #[flat] SRC5Event: SRC5Component::Event, #[flat] + SRC9Event: SRC9Component::Event, + #[flat] UpgradeableEvent: UpgradeableComponent::Event } #[constructor] pub fn constructor(ref self: ContractState, public_key: felt252) { self.account.initializer(public_key); + self.src9.initializer(); } #[abi(embed_v0)] diff --git a/packages/presets/src/eth_account.cairo b/packages/presets/src/eth_account.cairo index a1a6bc36c..a1bef3d84 100644 --- a/packages/presets/src/eth_account.cairo +++ b/packages/presets/src/eth_account.cairo @@ -4,10 +4,12 @@ /// # EthAccount Preset /// /// OpenZeppelin's upgradeable account which can change its public key and declare, -/// deploy, or call contracts, using Ethereum signing keys. +/// deploy, or call contracts, using Ethereum signing keys. Supports outside execution by +/// implementing SRC9. #[starknet::contract(account)] pub(crate) mod EthAccountUpgradeable { use openzeppelin_account::EthAccountComponent; + use openzeppelin_account::extensions::SRC9Component; use openzeppelin_account::interface::EthPublicKey; use openzeppelin_introspection::src5::SRC5Component; use openzeppelin_upgrades::UpgradeableComponent; @@ -16,6 +18,7 @@ pub(crate) mod EthAccountUpgradeable { component!(path: EthAccountComponent, storage: eth_account, event: EthAccountEvent); component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: SRC9Component, storage: src9, event: SRC9Event); component!(path: UpgradeableComponent, storage: upgradeable, event: UpgradeableEvent); // EthAccount Mixin @@ -24,6 +27,12 @@ pub(crate) mod EthAccountUpgradeable { EthAccountComponent::EthAccountMixinImpl; impl EthAccountInternalImpl = EthAccountComponent::InternalImpl; + // SRC9 + #[abi(embed_v0)] + impl OutsideExecutionV2Impl = + SRC9Component::OutsideExecutionV2Impl; + impl OutsideExecutionInternalImpl = SRC9Component::InternalImpl; + // Upgradeable impl UpgradeableInternalImpl = UpgradeableComponent::InternalImpl; @@ -34,6 +43,8 @@ pub(crate) mod EthAccountUpgradeable { #[substorage(v0)] pub src5: SRC5Component::Storage, #[substorage(v0)] + pub src9: SRC9Component::Storage, + #[substorage(v0)] pub upgradeable: UpgradeableComponent::Storage } @@ -45,12 +56,15 @@ pub(crate) mod EthAccountUpgradeable { #[flat] SRC5Event: SRC5Component::Event, #[flat] + SRC9Event: SRC9Component::Event, + #[flat] UpgradeableEvent: UpgradeableComponent::Event } #[constructor] pub(crate) fn constructor(ref self: ContractState, public_key: EthPublicKey) { self.eth_account.initializer(public_key); + self.src9.initializer(); } #[abi(embed_v0)] diff --git a/packages/presets/src/interfaces/account.cairo b/packages/presets/src/interfaces/account.cairo index c7713c8e7..37b834320 100644 --- a/packages/presets/src/interfaces/account.cairo +++ b/packages/presets/src/interfaces/account.cairo @@ -1,3 +1,4 @@ +use openzeppelin_account::extensions::src9::interface::OutsideExecution; use starknet::ClassHash; use starknet::account::Call; @@ -11,6 +12,12 @@ pub trait AccountUpgradeableABI { // ISRC5 fn supports_interface(self: @TState, interface_id: felt252) -> bool; + // ISRC9 + fn execute_from_outside_v2( + ref self: TState, outside_execution: OutsideExecution, signature: Span, + ) -> Array>; + fn is_valid_outside_execution_nonce(self: @TState, nonce: felt252) -> bool; + // IDeclarer fn __validate_declare__(self: @TState, class_hash: felt252) -> felt252; diff --git a/packages/presets/src/interfaces/eth_account.cairo b/packages/presets/src/interfaces/eth_account.cairo index 72aedb498..acdcf5575 100644 --- a/packages/presets/src/interfaces/eth_account.cairo +++ b/packages/presets/src/interfaces/eth_account.cairo @@ -1,3 +1,4 @@ +use openzeppelin_account::extensions::src9::interface::OutsideExecution; use openzeppelin_account::interface::EthPublicKey; use starknet::ClassHash; use starknet::account::Call; @@ -12,6 +13,12 @@ pub trait EthAccountUpgradeableABI { // ISRC5 fn supports_interface(self: @TState, interface_id: felt252) -> bool; + // ISRC9 + fn execute_from_outside_v2( + ref self: TState, outside_execution: OutsideExecution, signature: Span, + ) -> Array>; + fn is_valid_outside_execution_nonce(self: @TState, nonce: felt252) -> bool; + // IDeclarer fn __validate_declare__(self: @TState, class_hash: felt252) -> felt252; diff --git a/packages/presets/src/tests/test_account.cairo b/packages/presets/src/tests/test_account.cairo index 541bd1757..3297641d4 100644 --- a/packages/presets/src/tests/test_account.cairo +++ b/packages/presets/src/tests/test_account.cairo @@ -4,6 +4,10 @@ use crate::interfaces::account::{ AccountUpgradeableABISafeDispatcher, AccountUpgradeableABISafeDispatcherTrait }; use crate::interfaces::{AccountUpgradeableABIDispatcher, AccountUpgradeableABIDispatcherTrait}; +use openzeppelin_account::account::AccountComponent::AccountMixinImpl; +use openzeppelin_account::extensions::SRC9Component::{OutsideExecutionV2Impl, SNIP12MetadataImpl}; +use openzeppelin_account::extensions::src9::interface::{OutsideExecution, ISRC9_V2_ID}; +use openzeppelin_account::extensions::src9::snip12_utils::OutsideExecutionStructHash; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_introspection::interface::ISRC5_ID; use openzeppelin_test_common::account::{ @@ -15,18 +19,22 @@ use openzeppelin_testing as utils; use openzeppelin_testing::constants::stark::{KEY_PAIR, KEY_PAIR_2}; use openzeppelin_testing::constants::{ SALT, ZERO, CALLER, RECIPIENT, OTHER, QUERY_OFFSET, QUERY_VERSION, MIN_TRANSACTION_VERSION, - CLASS_HASH_ZERO + CLASS_HASH_ZERO, FELT_VALUE }; +use openzeppelin_testing::signing::SerializedSigning; use openzeppelin_testing::signing::StarkKeyPair; use openzeppelin_token::erc20::interface::IERC20DispatcherTrait; +use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::{ + spy_events, test_address, load, CheatSpan, start_cheat_caller_address, cheat_caller_address +}; use snforge_std::{ start_cheat_signature_global, start_cheat_transaction_version_global, - start_cheat_transaction_hash_global + start_cheat_transaction_hash_global, start_cheat_block_timestamp_global }; -use snforge_std::{spy_events, test_address, start_cheat_caller_address}; use starknet::account::Call; -use starknet::{ContractAddress, ClassHash}; +use starknet::{contract_address_const, ContractAddress, ClassHash}; // // Setup @@ -60,6 +68,10 @@ fn setup_dispatcher_with_data( (account_dispatcher, account_class.class_hash.into()) } +fn setup_simple_mock() -> ContractAddress { + utils::declare_and_deploy("SimpleMock", array![]) +} + // // constructor // @@ -74,14 +86,17 @@ fn test_constructor() { spy.assert_only_event_owner_added(account_address, key_pair.public_key); - let public_key = AccountUpgradeable::AccountMixinImpl::get_public_key(@state); + let public_key = state.get_public_key(); assert_eq!(public_key, key_pair.public_key); - let supports_isrc5 = AccountUpgradeable::AccountMixinImpl::supports_interface(@state, ISRC5_ID); + let supports_isrc5 = state.supports_interface(ISRC5_ID); assert!(supports_isrc5); - let supports_isrc6 = AccountUpgradeable::AccountMixinImpl::supports_interface(@state, ISRC6_ID); + let supports_isrc6 = state.supports_interface(ISRC6_ID); assert!(supports_isrc6); + + let supports_isrc9 = state.supports_interface(ISRC9_V2_ID); + assert!(supports_isrc9); } // @@ -568,3 +583,192 @@ fn test_state_persists_after_upgrade() { assert_eq!(snake_public_key, expected_public_key); } + +// +// execute_from_outside_v2 +// + +#[test] +fn test_execute_from_outside_v2_any_caller() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); +} + +#[test] +fn test_execute_from_outside_v2_specific_caller() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let mut outside_execution = setup_outside_execution(simple_mock, false); + outside_execution.caller = CALLER(); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + + cheat_caller_address(account_address, CALLER(), CheatSpan::TargetCalls(1)); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); +} + +#[test] +fn test_execute_from_outside_v2_uses_nonce() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let is_valid_nonce = dispatcher.is_valid_outside_execution_nonce(outside_execution.nonce); + assert!(is_valid_nonce); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); + + let is_invalid_nonce = !dispatcher.is_valid_outside_execution_nonce(outside_execution.nonce); + assert!(is_invalid_nonce); +} + +#[test] +#[should_panic(expected: 'SRC9: invalid caller')] +fn test_execute_from_outside_v2_caller_mismatch() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let mut outside_execution = setup_outside_execution(account_address, false); + outside_execution.caller = CALLER(); + + start_cheat_caller_address(account_address, OTHER()); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now >= execute_before')] +fn test_execute_from_outside_v2_call_after_execute_before() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(25); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now >= execute_before')] +fn test_execute_from_outside_v2_call_equal_to_execute_before() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(20); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: ('SRC9: now <= execute_after',))] +fn test_execute_from_outside_v2_call_before_execute_after() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(5); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now <= execute_after')] +fn test_execute_from_outside_v2_call_equal_to_execute_after() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(10); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: duplicated nonce')] +fn test_execute_from_outside_v2_invalid_nonce() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: invalid signature')] +fn test_execute_from_outside_v2_invalid_signature() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + let invalid_signature = array![*signature.at(0), *signature.at(1) + 1]; + + dispatcher.execute_from_outside_v2(outside_execution, invalid_signature.span()); +} + +#[test] +#[should_panic(expected: "Some error")] +fn test_execute_from_outside_v2_panics_when_inner_call_panic() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, true); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); +} + +// +// Helpers +// + +fn setup_outside_execution(target: ContractAddress, panic: bool) -> OutsideExecution { + let call = Call { + to: target, + selector: selector!("set_balance"), + calldata: array![FELT_VALUE, panic.into()].span(), + }; + let caller = contract_address_const::<'ANY_CALLER'>(); + let nonce = 5; + let execute_after = 10; + let execute_before = 20; + let calls = array![call].span(); + + // Set a valid timestamp for the execution time span + start_cheat_block_timestamp_global(15); + + OutsideExecution { caller, nonce, execute_after, execute_before, calls } +} + +fn assert_value(target: ContractAddress, expected_value: felt252) { + let value = *load(target, selector!("balance"), 1).at(0); + assert_eq!(value, expected_value); +} diff --git a/packages/presets/src/tests/test_eth_account.cairo b/packages/presets/src/tests/test_eth_account.cairo index 7b457edfd..4a7fae491 100644 --- a/packages/presets/src/tests/test_eth_account.cairo +++ b/packages/presets/src/tests/test_eth_account.cairo @@ -6,6 +6,10 @@ use crate::interfaces::eth_account::{ use crate::interfaces::{ EthAccountUpgradeableABIDispatcher, EthAccountUpgradeableABIDispatcherTrait }; +use openzeppelin_account::eth_account::EthAccountComponent::EthAccountMixinImpl; +use openzeppelin_account::extensions::SRC9Component::{OutsideExecutionV2Impl, SNIP12MetadataImpl}; +use openzeppelin_account::extensions::src9::interface::{OutsideExecution, ISRC9_V2_ID}; +use openzeppelin_account::extensions::src9::snip12_utils::OutsideExecutionStructHash; use openzeppelin_account::interface::ISRC6_ID; use openzeppelin_account::utils::secp256_point::{DebugSecp256Point, Secp256PointPartialEq}; use openzeppelin_introspection::interface::ISRC5_ID; @@ -17,21 +21,25 @@ use openzeppelin_test_common::eth_account::{ use openzeppelin_test_common::upgrades::UpgradeableSpyHelpers; use openzeppelin_testing as utils; use openzeppelin_testing::constants::secp256k1::{KEY_PAIR, KEY_PAIR_2}; -use openzeppelin_testing::constants::{CLASS_HASH_ZERO, ZERO, RECIPIENT, CALLER, OTHER}; +use openzeppelin_testing::constants::{CLASS_HASH_ZERO, ZERO, RECIPIENT, CALLER, OTHER, FELT_VALUE}; use openzeppelin_testing::constants::{SALT, QUERY_VERSION, MIN_TRANSACTION_VERSION}; use openzeppelin_testing::signing::Secp256k1KeyPair; +use openzeppelin_testing::signing::SerializedSigning; use openzeppelin_token::erc20::interface::IERC20DispatcherTrait; +use openzeppelin_utils::cryptography::snip12::OffchainMessageHash; use openzeppelin_utils::serde::SerializedAppend; +use snforge_std::{ + spy_events, test_address, load, CheatSpan, cheat_caller_address, start_cheat_caller_address +}; use snforge_std::{ start_cheat_signature_global, start_cheat_transaction_version_global, - start_cheat_transaction_hash_global, start_cheat_caller_address + start_cheat_transaction_hash_global, start_cheat_block_timestamp_global }; -use snforge_std::{spy_events, test_address}; -use starknet::ClassHash; use starknet::SyscallResultTrait; use starknet::account::Call; use starknet::secp256_trait::Secp256Trait; use starknet::secp256k1::Secp256k1Point; +use starknet::{contract_address_const, ContractAddress, ClassHash}; fn declare_v2_class() -> ClassHash { utils::declare_class("SnakeEthAccountMock").class_hash @@ -41,12 +49,16 @@ fn declare_v2_class() -> ClassHash { // Setup // -fn setup_dispatcher(key_pair: Secp256k1KeyPair) -> EthAccountUpgradeableABIDispatcher { +fn setup_dispatcher( + key_pair: Secp256k1KeyPair +) -> (ContractAddress, EthAccountUpgradeableABIDispatcher) { let mut calldata = array![]; calldata.append_serde(key_pair.public_key); let contract_address = utils::declare_and_deploy("EthAccountUpgradeable", calldata); - EthAccountUpgradeableABIDispatcher { contract_address } + let dispatcher = EthAccountUpgradeableABIDispatcher { contract_address }; + + (contract_address, dispatcher) } fn setup_dispatcher_with_data( @@ -68,6 +80,10 @@ fn setup_dispatcher_with_data( (dispatcher, contract_class.class_hash.into()) } +fn setup_simple_mock() -> ContractAddress { + utils::declare_and_deploy("SimpleMock", array![]) +} + // // constructor // @@ -82,17 +98,17 @@ fn test_constructor() { spy.assert_only_event_owner_added(test_address(), key_pair.public_key); - let public_key = EthAccountUpgradeable::EthAccountMixinImpl::get_public_key(@state); + let public_key = state.get_public_key(); assert_eq!(public_key, key_pair.public_key); - let supports_isrc5 = EthAccountUpgradeable::EthAccountMixinImpl::supports_interface( - @state, ISRC5_ID - ); + let supports_isrc5 = state.supports_interface(ISRC5_ID); assert!(supports_isrc5); - let supports_isrc6 = EthAccountUpgradeable::EthAccountMixinImpl::supports_interface( - @state, ISRC6_ID - ); + + let supports_isrc6 = state.supports_interface(ISRC6_ID); assert!(supports_isrc6); + + let supports_isrc9 = state.supports_interface(ISRC9_V2_ID); + assert!(supports_isrc9); } // @@ -102,7 +118,7 @@ fn test_constructor() { #[test] fn test_public_key_setter_and_getter() { let key_pair = KEY_PAIR(); - let dispatcher = setup_dispatcher(key_pair); + let (_, dispatcher) = setup_dispatcher(key_pair); let contract_address = dispatcher.contract_address; let mut spy = spy_events(); @@ -122,7 +138,7 @@ fn test_public_key_setter_and_getter() { #[test] fn test_public_key_setter_and_getter_camel() { let key_pair = KEY_PAIR(); - let dispatcher = setup_dispatcher(key_pair); + let (_, dispatcher) = setup_dispatcher(key_pair); let contract_address = dispatcher.contract_address; let mut spy = spy_events(); @@ -141,7 +157,7 @@ fn test_public_key_setter_and_getter_camel() { #[test] #[should_panic(expected: ('EthAccount: unauthorized',))] fn test_set_public_key_different_account() { - let dispatcher = setup_dispatcher(KEY_PAIR()); + let (_, dispatcher) = setup_dispatcher(KEY_PAIR()); dispatcher.set_public_key(KEY_PAIR_2().public_key, array![].span()); } @@ -149,7 +165,7 @@ fn test_set_public_key_different_account() { #[test] #[should_panic(expected: ('EthAccount: unauthorized',))] fn test_setPublicKey_different_account() { - let dispatcher = setup_dispatcher(KEY_PAIR()); + let (_, dispatcher) = setup_dispatcher(KEY_PAIR()); dispatcher.setPublicKey(KEY_PAIR_2().public_key, array![].span()); } @@ -160,7 +176,7 @@ fn test_setPublicKey_different_account() { fn is_valid_sig_dispatcher() -> (EthAccountUpgradeableABIDispatcher, felt252, Array) { let key_pair = KEY_PAIR(); - let dispatcher = setup_dispatcher(key_pair); + let (_, dispatcher) = setup_dispatcher(key_pair); let data = SIGNED_TX_DATA(key_pair); let mut serialized_signature = array![]; @@ -208,7 +224,7 @@ fn test_isValidSignature_bad_sig() { #[test] fn test_supports_interface() { let key_pair = KEY_PAIR(); - let dispatcher = setup_dispatcher(key_pair); + let (_, dispatcher) = setup_dispatcher(key_pair); let supports_isrc5 = dispatcher.supports_interface(ISRC5_ID); assert!(supports_isrc5); @@ -438,7 +454,7 @@ fn test_multicall_zero_calls() { #[test] #[should_panic(expected: ('EthAccount: invalid caller',))] fn test_account_called_from_contract() { - let account = setup_dispatcher(KEY_PAIR()); + let (_, account) = setup_dispatcher(KEY_PAIR()); let calls = array![]; start_cheat_caller_address(account.contract_address, CALLER()); @@ -453,14 +469,14 @@ fn test_account_called_from_contract() { #[test] #[should_panic(expected: ('EthAccount: unauthorized',))] fn test_upgrade_access_control() { - let v1 = setup_dispatcher(KEY_PAIR()); + let (_, v1) = setup_dispatcher(KEY_PAIR()); v1.upgrade(CLASS_HASH_ZERO()); } #[test] #[should_panic(expected: ('Class hash cannot be zero',))] fn test_upgrade_with_class_hash_zero() { - let v1 = setup_dispatcher(KEY_PAIR()); + let (_, v1) = setup_dispatcher(KEY_PAIR()); start_cheat_caller_address(v1.contract_address, v1.contract_address); v1.upgrade(CLASS_HASH_ZERO()); @@ -468,7 +484,7 @@ fn test_upgrade_with_class_hash_zero() { #[test] fn test_upgraded_event() { - let v1 = setup_dispatcher(KEY_PAIR()); + let (_, v1) = setup_dispatcher(KEY_PAIR()); let contract_address = v1.contract_address; let mut spy = spy_events(); @@ -482,7 +498,7 @@ fn test_upgraded_event() { #[test] #[feature("safe_dispatcher")] fn test_v2_missing_camel_selector() { - let v1 = setup_dispatcher(KEY_PAIR()); + let (_, v1) = setup_dispatcher(KEY_PAIR()); let contract_address = v1.contract_address; let v2_class_hash = declare_v2_class(); @@ -498,7 +514,7 @@ fn test_v2_missing_camel_selector() { #[test] fn test_state_persists_after_upgrade() { let key_pair = KEY_PAIR(); - let v1 = setup_dispatcher(key_pair); + let (_, v1) = setup_dispatcher(key_pair); let contract_address = v1.contract_address; start_cheat_caller_address(contract_address, contract_address); @@ -520,10 +536,197 @@ fn test_state_persists_after_upgrade() { assert_eq!(snake_public_key, new_key_pair.public_key); } +// +// execute_from_outside_v2 +// + +#[test] +fn test_execute_from_outside_v2_any_caller() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); +} + +#[test] +fn test_execute_from_outside_v2_specific_caller() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let mut outside_execution = setup_outside_execution(simple_mock, false); + outside_execution.caller = CALLER(); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + + cheat_caller_address(account_address, CALLER(), CheatSpan::TargetCalls(1)); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); +} + +#[test] +fn test_execute_from_outside_v2_uses_nonce() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let is_valid_nonce = dispatcher.is_valid_outside_execution_nonce(outside_execution.nonce); + assert!(is_valid_nonce); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + + assert_value(simple_mock, FELT_VALUE); + + let is_invalid_nonce = !dispatcher.is_valid_outside_execution_nonce(outside_execution.nonce); + assert!(is_invalid_nonce); +} + +#[test] +#[should_panic(expected: 'SRC9: invalid caller')] +fn test_execute_from_outside_v2_caller_mismatch() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let mut outside_execution = setup_outside_execution(account_address, false); + outside_execution.caller = CALLER(); + + start_cheat_caller_address(account_address, OTHER()); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now >= execute_before')] +fn test_execute_from_outside_v2_call_after_execute_before() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(25); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now >= execute_before')] +fn test_execute_from_outside_v2_call_equal_to_execute_before() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(20); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: ('SRC9: now <= execute_after',))] +fn test_execute_from_outside_v2_call_before_execute_after() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(5); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: now <= execute_after')] +fn test_execute_from_outside_v2_call_equal_to_execute_after() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + start_cheat_block_timestamp_global(10); + + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: duplicated nonce')] +fn test_execute_from_outside_v2_invalid_nonce() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); + dispatcher.execute_from_outside_v2(outside_execution, array![].span()); +} + +#[test] +#[should_panic(expected: 'SRC9: invalid signature')] +fn test_execute_from_outside_v2_invalid_signature() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let outside_execution = setup_outside_execution(account_address, false); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + let invalid_signature = array![ + *signature.at(0), *signature.at(1), *signature.at(2), *signature.at(3) + 1 + ]; + + dispatcher.execute_from_outside_v2(outside_execution, invalid_signature.span()); +} + +#[test] +#[should_panic(expected: "Some error")] +fn test_execute_from_outside_v2_panics_when_inner_call_panic() { + let key_pair = KEY_PAIR(); + let (account_address, dispatcher) = setup_dispatcher(key_pair); + let simple_mock = setup_simple_mock(); + let outside_execution = setup_outside_execution(simple_mock, true); + + let msg_hash = outside_execution.get_message_hash(account_address); + let signature = key_pair.serialized_sign(msg_hash.into()); + + dispatcher.execute_from_outside_v2(outside_execution, signature.span()); +} + // // Helpers // +fn setup_outside_execution(target: ContractAddress, panic: bool) -> OutsideExecution { + let call = Call { + to: target, + selector: selector!("set_balance"), + calldata: array![FELT_VALUE, panic.into()].span(), + }; + let caller = contract_address_const::<'ANY_CALLER'>(); + let nonce = 5; + let execute_after = 10; + let execute_before = 20; + let calls = array![call].span(); + + // Set a valid timestamp for the execution time span + start_cheat_block_timestamp_global(15); + + OutsideExecution { caller, nonce, execute_after, execute_before, calls } +} + +fn assert_value(target: ContractAddress, expected_value: felt252) { + let value = *load(target, selector!("balance"), 1).at(0); + assert_eq!(value, expected_value); +} + fn get_points() -> (Secp256k1Point, Secp256k1Point) { let curve_size = Secp256Trait::::get_curve_size(); let point_1 = Secp256Trait::secp256_ec_get_point_from_x_syscall(curve_size, true) diff --git a/packages/test_common/src/mocks/simple.cairo b/packages/test_common/src/mocks/simple.cairo index d1eab519a..4ee015a61 100644 --- a/packages/test_common/src/mocks/simple.cairo +++ b/packages/test_common/src/mocks/simple.cairo @@ -1,6 +1,7 @@ #[starknet::interface] pub trait ISimpleMock { fn increase_balance(ref self: TContractState, amount: felt252) -> bool; + fn set_balance(ref self: TContractState, value: felt252, panic: bool); fn get_balance(self: @TContractState) -> felt252; } @@ -20,6 +21,13 @@ pub mod SimpleMock { true } + fn set_balance(ref self: ContractState, value: felt252, panic: bool) { + if panic { + panic!("Some error"); + } + self.balance.write(value); + } + fn get_balance(self: @ContractState) -> felt252 { self.balance.read() }