Repository: OpenZeppelin/cairo-contracts (language: python)

Issue:
Implementation of SNIP-9
**🧐 Motivation**
SNIP-9 Outside Execution (https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md) is implemented in ArgentX & Braavos account contracts.

**📝 Details**
Do you plan to implement soon this very useful feature?




Discussion / hints:
Hey @PhilippeR26, this is something we want to include for sure. Thanks for opening the issue.
I have implemented SNIP-9 in Starknet.js. So, if you have questions about SNIP-9, don't hesitate to contact me.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a82af1c3..36638c27c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,14 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - `into_base_16_string_no_padding` function to the test helpers (#1137)
-- Vesting component and VestingWallet preset (#334)
+- SRC9 (OutsideExecution) component (#1150)
+- Vesting component and VestingWallet preset (#1116)
 
-### Changed
+### Changed (Breaking)
 
 - Bump scarb to v2.8.2 (#1137)
 - Bump snforge to 0.30.0 (#1137)
 - `declare_class`, `declare_and_deploy`, and `declare_and_deploy_at` will skip declaration if the contract is already declared (#1137)
 - Bump edition to 2024_07 (#1138)
+- `execute_calls` function from account utils (#1150)
+  - calls param type changed from `Array<Call>` to `Span<Call>`
 
 ## 0.16.0 (2024-08-30)
 
diff --git a/docs/modules/ROOT/pages/accounts.adoc b/docs/modules/ROOT/pages/accounts.adoc
index bf064a01d..12ffd0dae 100644
--- a/docs/modules/ROOT/pages/accounts.adoc
+++ b/docs/modules/ROOT/pages/accounts.adoc
@@ -1,5 +1,5 @@
 :snip-5: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-5.md
-:snip-6: https://github.com/ericnordelo/SNIPs/blob/feat/standard-account/SNIPS/snip-6.md
+:snip-6: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md
 :counterfactual: xref:/guides/deployment.adoc[counterfactual deployments]
 
 = Accounts
diff --git a/docs/modules/ROOT/pages/api/account.adoc b/docs/modules/ROOT/pages/api/account.adoc
index ff1942a37..dbe63843b 100644
--- a/docs/modules/ROOT/pages/api/account.adoc
+++ b/docs/modules/ROOT/pages/api/account.adoc
@@ -1,5 +1,7 @@
 :github-icon: pass:[<svg class="icon"><use href="#github-icon"/></svg>]
-:snip6: https://github.com/ericnordelo/SNIPs/blob/feat/standard-account/SNIPS/snip-6.md[SNIP-6]
+:snip6: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md[SNIP-6]
+:snip9: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-9.md[SNIP-9]
+:snip12: https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-12.md[SNIP-12]
 :inner-src5: xref:api/introspection.adoc#ISRC5[SRC5 ID]
 
 = Account
@@ -566,6 +568,132 @@ Emitted when a `public_key` is added.
 
 Emitted when a `public_key` is removed.
 
+== Extensions
+
+[.contract]
+[[ISRC9_V2]]
+=== `++ISRC9_V2++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.16.0/packages/account/src/extensions/src9/interface.cairo[{github-icon},role=heading-link]
+
+```cairo
+use openzeppelin::account::extensions::src9::ISRC9_V2;
+```
+
+Interface of the SRC9 Standard as defined in the {snip9}.
+
+[.contract-index]
+.{inner-src5}
+--
+0x1d1144bb2138366ff28d8e9ab57456b1d332ac42196230c3a602003c89872
+--
+
+[.contract-index]
+.Functions
+--
+* xref:#ISRC9_V2-execute_from_outside_v2[`++execute_from_outside_v2(outside_execution, signature)++`]
+* xref:#ISRC9_V2-is_valid_outside_execution_nonce[`++is_valid_outside_execution_nonce(nonce)++`]
+--
+
+[#ISRC9_V2-Functions]
+==== Functions
+
+[.contract-item]
+[[ISRC9_V2-execute_from_outside_v2]]
+==== `[.contract-item-name]#++execute_from_outside_v2++#++(outside_execution: OutsideExecution, signature: Span<felt252>,) → Array<Span<felt252>>++` [.item-kind]#external#
+
+Allows anyone to submit a transaction on behalf of the account as long as they have the relevant signatures.
+
+This method allows reentrancy. A call to `\\__execute__` or `execute_from_outside_v2` can trigger
+another nested transaction to `execute_from_outside_v2` thus the implementation MUST verify
+that the provided `signature` matches the hash of `outside_execution` and that `nonce` was
+not already used.
+
+The implementation should expect version to be set to 2 in the domain separator.
+
+Arguments:
+
+- `outside_execution` - The parameters of the transaction to execute.
+- `signature` - A valid signature on the {snip12} message encoding of `outside_execution`.
+
+[.contract-item]
+[[ISRC9_V2-is_valid_outside_execution_nonce]]
+==== `[.contract-item-name]#++is_valid_outside_execution_nonce++#++(nonce: felt252) → bool++` [.item-kind]#external#
+
+Get the status of a given nonce. `true` if the nonce is available to use.
+
+[.contract]
+[[SRC9Component]]
+=== `++SRC9Component++` link:https://github.com/OpenZeppelin/cairo-contracts/blob/release-v0.16.0/packages/account/src/extensions/src9/src9.cairo[{github-icon},role=heading-link]
+
+```cairo
+use openzeppelin::account::extensions::SRC9Component;
+```
+
+OutsideExecution component implementing xref:ISRC9_V2[`ISRC9_V2`].
+
+NOTE: This component is signature-agnostic, meaning it can be integrated into any account contract, as long as the
+account implements the ISRC6 interface.
+
+[.contract-index#SRC9Component-Embeddable-Impls]
+.Embeddable Implementations
+--
+[.sub-index#SRC9Component-Embeddable-Impls-OutsideExecutionV2Impl]
+.OutsideExecutionV2Impl
+
+* xref:#SRC9Component-execute_from_outside_v2[`++execute_from_outside_v2(self, outside_execution, signature)++`]
+* xref:#SRC9Component-is_valid_outside_execution_nonce[`++is_valid_outside_execution_nonce(self, nonce)++`]
+--
+
+[.contract-index]
+.Internal Implementations
+--
+.InternalImpl
+
+* xref:#SRC9Component-initializer[`++initializer(self)++`]
+--
+
+[#SRC9Component-Embeddable-Functions]
+==== Embeddable functions
+
+[.contract-item]
+[[SRC9Component-execute_from_outside_v2]]
+==== `[.contract-item-name]#++execute_from_outside_v2++#++(ref self: ContractState, outside_execution: OutsideExecution, signature: Span<felt252>) → Array<Span<felt252>>++` [.item-kind]#external#
+
+Allows anyone to submit a transaction on behalf of the account as long as they
+have the relevant signatures.
+
+This method allows reentrancy. A call to `\\__execute__` or `execute_from_outside_v2` can
+trigger another nested transaction to `execute_from_outside_v2`. This implementation
+verifies that the provided `signature` matches the hash of `outside_execution` and that
+`nonce` was not already used.
+
+Arguments:
+
+- `outside_execution` - The parameters of the transaction to execute.
+- `signature` - A valid signature on the {snip12} message encoding of
+`outside_execution`.
+
+Requirements:
+
+- The caller must be the `outside_execution.caller` unless 'ANY_CALLER' is used.
+- The current time must be within the `outside_execution.execute_after` and `outside_execution.execute_before` span.
+- The `outside_execution.nonce` must not be used before.
+- The `signature` must be valid.
+
+[.contract-item]
+[[SRC9Component-is_valid_outside_execution_nonce]]
+==== `[.contract-item-name]#++is_valid_outside_execution_nonce++#++(self: @ContractState, nonce: felt252) → bool++` [.item-kind]#external#
+
+Returns the status of a given nonce. `true` if the nonce is available to use.
+
+[#SRC9Component-Internal-Functions]
+==== Internal functions
+
+[.contract-item]
+[[SRC9Component-initializer]]
+==== `[.contract-item-name]#++initializer++#++(ref self: ComponentState)++` [.item-kind]#internal#
+
+Initializes the account by registering the `ISRC9_V2` interface Id.
+
 == Presets
 
 [.contract]
diff --git a/docs/modules/ROOT/pages/api/utilities.adoc b/docs/modules/ROOT/pages/api/utilities.adoc
index b968dcab7..494e8bddd 100644
--- a/docs/modules/ROOT/pages/api/utilities.adoc
+++ b/docs/modules/ROOT/pages/api/utilities.adoc
@@ -317,7 +317,9 @@ that implements the `Serde` trait.
 use openzeppelin::utils::cryptography::nonces::NoncesComponent;
 ```
 
-Simple component for managing nonces.
+This component provides a simple mechanism for handling incremental
+nonces for a set of addresses. It is commonly used to prevent replay attacks
+when contracts accept signatures as input.
 
 [.contract-index#NoncesComponent-Embeddable-Impls]
 .Embeddable Implementations
diff --git a/packages/account/README.md b/packages/account/README.md
index 15eb51d5c..2d30148b3 100644
--- a/packages/account/README.md
+++ b/packages/account/README.md
@@ -13,8 +13,10 @@ This crate provides components to implement account contracts that can be used f
 ### Interfaces
 
 - `ISRC6`
+- `ISRC9_V2`
 
 ### Components
 
 - `AccountComponent`
 - `EthAccountComponent`
+- `SRC9Component`
diff --git a/packages/account/src/account.cairo b/packages/account/src/account.cairo
index 312979eb3..37022a049 100644
--- a/packages/account/src/account.cairo
+++ b/packages/account/src/account.cairo
@@ -90,7 +90,7 @@ pub mod AccountComponent {
                 assert(MIN_TRANSACTION_VERSION <= tx_version, Errors::INVALID_TX_VERSION);
             }
 
-            execute_calls(calls)
+            execute_calls(calls.span())
         }
 
         /// Verifies the validity of the signature for the current transaction.
diff --git a/packages/account/src/eth_account.cairo b/packages/account/src/eth_account.cairo
index 4fdab40a5..e56ce634b 100644
--- a/packages/account/src/eth_account.cairo
+++ b/packages/account/src/eth_account.cairo
@@ -94,7 +94,7 @@ pub mod EthAccountComponent {
                 assert(MIN_TRANSACTION_VERSION <= tx_version, Errors::INVALID_TX_VERSION);
             }
 
-            execute_calls(calls)
+            execute_calls(calls.span())
         }
 
         /// Verifies the validity of the signature for the current transaction.
diff --git a/packages/account/src/extensions.cairo b/packages/account/src/extensions.cairo
new file mode 100644
index 000000000..6df042299
--- /dev/null
+++ b/packages/account/src/extensions.cairo
@@ -0,0 +1,3 @@
+pub mod src9;
+
+pub use src9::SRC9Component;
diff --git a/packages/account/src/extensions/src9.cairo b/packages/account/src/extensions/src9.cairo
new file mode 100644
index 000000000..7dd8a862d
--- /dev/null
+++ b/packages/account/src/extensions/src9.cairo
@@ -0,0 +1,7 @@
+pub mod interface;
+pub mod snip12_utils;
+pub mod src9;
+
+pub use interface::{ISRC9_V2, OutsideExecution};
+pub use interface::{ISRC9_V2Dispatcher, ISRC9_V2DispatcherTrait};
+pub use src9::SRC9Component;
diff --git a/packages/account/src/extensions/src9/interface.cairo b/packages/account/src/extensions/src9/interface.cairo
new file mode 100644
index 000000000..53990778d
--- /dev/null
+++ b/packages/account/src/extensions/src9/interface.cairo
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.16.0 (account/extensions/src9/interface.cairo)
+
+use starknet::ContractAddress;
+use starknet::account::Call;
+
+pub const ISRC9_V2_ID: felt252 = 0x1d1144bb2138366ff28d8e9ab57456b1d332ac42196230c3a602003c89872;
+
+#[derive(Copy, Drop, Serde)]
+pub struct OutsideExecution {
+    pub caller: ContractAddress,
+    pub nonce: felt252,
+    pub execute_after: u64,
+    pub execute_before: u64,
+    pub calls: Span<Call>
+}
+
+#[starknet::interface]
+pub trait ISRC9_V2<TContractState> {
+    /// Allows anyone to submit a transaction on behalf of the account as long as they
+    /// have the relevant signatures.
+    ///
+    /// This method allows reentrancy. A call to `__execute__` or `execute_from_outside_v2` can
+    /// trigger another nested transaction to `execute_from_outside_v2` thus the implementation MUST
+    /// verify that the provided `signature` matches the hash of `outside_execution` and that
+    /// `nonce` was not already used.
+    ///
+    /// The implementation should expect version to be set to 2 in the domain separator.
+    ///
+    /// Arguments:
+    ///
+    /// - `outside_execution` - The parameters of the transaction to execute.
+    /// - `signature` - A valid signature on the SNIP-12 message encoding of `outside_execution`.
+    fn execute_from_outside_v2(
+        ref self: TContractState, outside_execution: OutsideExecution, signature: Span<felt252>,
+    ) -> Array<Span<felt252>>;
+
+    /// Get the status of a given nonce. `true` if the nonce is available to use.
+    fn is_valid_outside_execution_nonce(self: @TContractState, nonce: felt252) -> bool;
+}
diff --git a/packages/account/src/extensions/src9/snip12_utils.cairo b/packages/account/src/extensions/src9/snip12_utils.cairo
new file mode 100644
index 000000000..f55aa048d
--- /dev/null
+++ b/packages/account/src/extensions/src9/snip12_utils.cairo
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.16.0 (account/extensions/src9/snip12.cairo)
+
+use core::hash::{HashStateTrait, HashStateExTrait};
+use core::poseidon::{PoseidonTrait, poseidon_hash_span};
+use crate::extensions::src9::OutsideExecution;
+use openzeppelin_utils::cryptography::snip12::StructHash;
+use starknet::account::Call;
+
+// sn_keccak(
+//   "\"OutsideExecution\"(\"Caller\":\"ContractAddress\",\"Nonce\":\"felt\",
+//   \"Execute After\":\"u128\",\"Execute Before\":\"u128\",\"Calls\":\"Call*\")
+//   \"Call\"(\"To\":\"ContractAddress\",
+//   \"Selector\":\"selector\",\"Calldata\":\"felt*\")"
+// )
+pub const OUTSIDE_EXECUTION_TYPE_HASH: felt252 =
+    0x312b56c05a7965066ddbda31c016d8d05afc305071c0ca3cdc2192c3c2f1f0f;
+
+// sn_keccak("\"Call\"(\"To\":\"ContractAddress\",\"Selector\":\"selector\",\"Calldata\":\"felt*\")");
+pub const CALL_TYPE_HASH: felt252 =
+    0x3635c7f2a7ba93844c0d064e18e487f35ab90f7c39d00f186a781fc3f0c2ca9;
+
+pub impl CallStructHash of StructHash<Call> {
+    fn hash_struct(self: @Call) -> felt252 {
+        let hash_state = PoseidonTrait::new();
+        hash_state
+            .update_with(CALL_TYPE_HASH)
+            .update_with(*self.to)
+            .update_with(*self.selector)
+            .update_with(poseidon_hash_span(*self.calldata))
+            .finalize()
+    }
+}
+
+pub impl OutsideExecutionStructHash of StructHash<OutsideExecution> {
+    fn hash_struct(self: @OutsideExecution) -> felt252 {
+        let mut calls_span = *self.calls;
+        let mut hashed_calls = array![];
+
+        for call in calls_span {
+            hashed_calls.append(call.hash_struct());
+        };
+
+        let hash_state = PoseidonTrait::new();
+        hash_state
+            .update_with(OUTSIDE_EXECUTION_TYPE_HASH)
+            .update_with(*self.caller)
+            .update_with(*self.nonce)
+            .update_with(*self.execute_after)
+            .update_with(*self.execute_before)
+            .update_with(poseidon_hash_span(hashed_calls.span()))
+            .finalize()
+    }
+}
diff --git a/packages/account/src/extensions/src9/src9.cairo b/packages/account/src/extensions/src9/src9.cairo
new file mode 100644
index 000000000..bfc4b8805
--- /dev/null
+++ b/packages/account/src/extensions/src9/src9.cairo
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.16.0 (account/extensions/src9/src9.cairo)
+
+/// # SRC9 Component (Outside Execution)
+///
+/// The SRC9 component allows a protocol to submit transactions on behalf of a user account,
+/// as long as they provide the relevant signatures.
+///
+/// This component is designed to be account agnostic, as long as the contract implements the ISRC6
+/// interface.
+#[starknet::component]
+pub mod SRC9Component {
+    use crate::extensions::src9::OutsideExecution;
+    use crate::extensions::src9::interface;
+    use crate::extensions::src9::snip12_utils::OutsideExecutionStructHash;
+    use crate::utils::execute_calls;
+    use openzeppelin_account::dual_account::{DualCaseAccount, DualCaseAccountTrait};
+    use openzeppelin_introspection::src5::SRC5Component::InternalTrait as SRC5InternalTrait;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata};
+    use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
+
+    #[storage]
+    pub struct Storage {
+        pub SRC9_nonces: Map<felt252, bool>
+    }
+
+    pub mod Errors {
+        pub const INVALID_CALLER: felt252 = 'SRC9: invalid caller';
+        pub const INVALID_AFTER: felt252 = 'SRC9: now <= execute_after';
+        pub const INVALID_BEFORE: felt252 = 'SRC9: now >= execute_before';
+        pub const DUPLICATED_NONCE: felt252 = 'SRC9: duplicated nonce';
+        pub const INVALID_SIGNATURE: felt252 = 'SRC9: invalid signature';
+    }
+
+    // Name and version as defined in the SNIP-9
+    pub impl SNIP12MetadataImpl of SNIP12Metadata {
+        fn name() -> felt252 {
+            'Account.execute_from_outside'
+        }
+        fn version() -> felt252 {
+            2
+        }
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(OutsideExecutionV2Impl)]
+    impl OutsideExecutionV2<
+        TContractState,
+        +HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of interface::ISRC9_V2<ComponentState<TContractState>> {
+        /// Allows anyone to submit a transaction on behalf of the account as long as they
+        /// provide the relevant signatures.
+        ///
+        /// This method allows reentrancy. A call to `__execute__` or `execute_from_outside_v2` can
+        /// trigger another nested transaction to `execute_from_outside_v2`. This implementation
+        /// verifies that the provided `signature` matches the hash of `outside_execution` and that
+        /// `nonce` was not already used.
+        ///
+        /// Arguments:
+        ///
+        /// - `outside_execution` - The parameters of the transaction to execute.
+        /// - `signature` - A valid signature on the SNIP-12 message encoding of
+        /// `outside_execution`.
+        ///
+        /// Requirements:
+        ///
+        /// - The caller must be the `outside_execution.caller` unless 'ANY_CALLER' is used.
+        /// - The current time must be within the `outside_execution.execute_after` and
+        /// `outside_execution.execute_before` span, excluding boundaries.
+        /// - The `outside_execution.nonce` must not be used before.
+        /// - The `signature` must be valid.
+        fn execute_from_outside_v2(
+            ref self: ComponentState<TContractState>,
+            outside_execution: OutsideExecution,
+            signature: Span<felt252>,
+        ) -> Array<Span<felt252>> {
+            // 'ANY_CALLER' can be used to bypass the caller validation
+            if outside_execution.caller.into() != 'ANY_CALLER' {
+                assert(
+                    starknet::get_caller_address() == outside_execution.caller,
+                    Errors::INVALID_CALLER
+                );
+            }
+
+            // 1. Validate the execution time span
+            let now = starknet::get_block_timestamp();
+            assert(outside_execution.execute_after < now, Errors::INVALID_AFTER);
+            assert(now < outside_execution.execute_before, Errors::INVALID_BEFORE);
+
+            // 2. Validate the nonce
+            assert(!self.SRC9_nonces.read(outside_execution.nonce), Errors::DUPLICATED_NONCE);
+
+            // 3. Mark the nonce as used
+            self.SRC9_nonces.write(outside_execution.nonce, true);
+
+            // 4. Validate the signature
+            let this = starknet::get_contract_address();
+            let outside_tx_hash = outside_execution.get_message_hash(this);
+
+            // Make this component agnostic to the account implementation, as long
+            // as the contract implements the SRC6 interface.
+            let is_valid_signature_felt = DualCaseAccount { contract_address: this }
+                .is_valid_signature(outside_tx_hash, signature.into());
+
+            // Check either 'VALID' or true for backwards compatibility.
+            let is_valid_signature = is_valid_signature_felt == starknet::VALIDATED
+                || is_valid_signature_felt == 1;
+
+            assert(is_valid_signature, Errors::INVALID_SIGNATURE);
+
+            // 5. Execute the calls
+            execute_calls(outside_execution.calls)
+        }
+
+        /// Returns the status of a given nonce. `true` if the nonce is available to use.
+        fn is_valid_outside_execution_nonce(
+            self: @ComponentState<TContractState>, nonce: felt252
+        ) -> bool {
+            self.SRC9_nonces.read(nonce) == false
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        impl SRC5: SRC5Component::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the account by registering the ISRC9_V2 interface Id.
+        fn initializer(ref self: ComponentState<TContractState>) {
+            let mut src5_component = get_dep_component_mut!(ref self, SRC5);
+            src5_component.register_interface(interface::ISRC9_V2_ID);
+        }
+    }
+}
diff --git a/packages/account/src/lib.cairo b/packages/account/src/lib.cairo
index 1504180fb..6e4ed8a32 100644
--- a/packages/account/src/lib.cairo
+++ b/packages/account/src/lib.cairo
@@ -2,6 +2,7 @@ pub mod account;
 pub mod dual_account;
 pub mod dual_eth_account;
 pub mod eth_account;
+pub mod extensions;
 pub mod interface;
 
 pub mod tests;
diff --git a/packages/account/src/utils.cairo b/packages/account/src/utils.cairo
index 1b9ef915c..d50192882 100644
--- a/packages/account/src/utils.cairo
+++ b/packages/account/src/utils.cairo
@@ -14,7 +14,7 @@ pub const QUERY_OFFSET: u256 = 0x100000000000000000000000000000000;
 // QUERY_OFFSET + TRANSACTION_VERSION
 pub const QUERY_VERSION: u256 = 0x100000000000000000000000000000001;
 
-pub fn execute_calls(mut calls: Array<Call>) -> Array<Span<felt252>> {
+pub fn execute_calls(mut calls: Span<Call>) -> Array<Span<felt252>> {
     let mut res = array![];
     loop {
         match calls.pop_front() {
@@ -28,7 +28,7 @@ pub fn execute_calls(mut calls: Array<Call>) -> Array<Span<felt252>> {
     res
 }
 
-fn execute_single_call(call: Call) -> Span<felt252> {
-    let Call { to, selector, calldata } = call;
+fn execute_single_call(call: @Call) -> Span<felt252> {
+    let Call { to, selector, calldata } = *call;
     starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall()
 }
diff --git a/packages/finance/src/vesting/interface.cairo b/packages/finance/src/vesting/interface.cairo
index 2bba85c8b..ee3a7b95f 100644
--- a/packages/finance/src/vesting/interface.cairo
+++ b/packages/finance/src/vesting/interface.cairo
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-// OpenZeppelin Contracts for Cairo v0.15.1 (finance/vesting/interface.cairo)
+// OpenZeppelin Contracts for Cairo v0.16.0 (finance/vesting/interface.cairo)
 
 use starknet::ContractAddress;
 
diff --git a/packages/finance/src/vesting/vesting.cairo b/packages/finance/src/vesting/vesting.cairo
index 23a1a5d1e..843b3394e 100644
--- a/packages/finance/src/vesting/vesting.cairo
+++ b/packages/finance/src/vesting/vesting.cairo
@@ -1,5 +1,7 @@
 // SPDX-License-Identifier: MIT
-// OpenZeppelin Contracts for Cairo v0.15.1 (finance/vesting/vesting.cairo)
+// OpenZeppelin Contracts for Cairo v0.16.0 (finance/vesting/vesting.cairo)
+
+use starknet::ContractAddress;
 
 /// # Vesting Component
 ///
@@ -25,9 +27,6 @@
 /// - When using this contract with any token whose balance is adjusted automatically
 ///   (i.e. a rebase token), make sure to account for the supply/balance adjustment in the
 ///   vesting schedule to ensure the vested amount is as intended.
-
-use starknet::ContractAddress;
-
 #[starknet::component]
 pub mod VestingComponent {
     use openzeppelin_access::ownable::OwnableComponent::OwnableImpl;
diff --git a/packages/presets/src/vesting.cairo b/packages/presets/src/vesting.cairo
index 2ef8b6891..0c5199430 100644
--- a/packages/presets/src/vesting.cairo
+++ b/packages/presets/src/vesting.cairo
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-// OpenZeppelin Contracts for Cairo v0.15.1 (presets/vesting.cairo)
+// OpenZeppelin Contracts for Cairo v0.16.0 (presets/vesting.cairo)
 
 #[starknet::contract]
 pub mod VestingWallet {
diff --git a/packages/token/src/erc1155.cairo b/packages/token/src/erc1155.cairo
index 637ef5bfc..68a61043a 100644
--- a/packages/token/src/erc1155.cairo
+++ b/packages/token/src/erc1155.cairo
@@ -4,6 +4,5 @@ pub mod erc1155;
 pub mod erc1155_receiver;
 pub mod interface;
 
-pub use erc1155::ERC1155Component;
-pub use erc1155::ERC1155HooksEmptyImpl;
+pub use erc1155::{ERC1155Component, ERC1155HooksEmptyImpl};
 pub use erc1155_receiver::ERC1155ReceiverComponent;
diff --git a/packages/token/src/erc20.cairo b/packages/token/src/erc20.cairo
index 01fffac1f..e40fbb2bb 100644
--- a/packages/token/src/erc20.cairo
+++ b/packages/token/src/erc20.cairo
@@ -3,7 +3,5 @@ pub mod erc20;
 pub mod extensions;
 pub mod interface;
 
-pub use erc20::ERC20Component;
-pub use erc20::ERC20HooksEmptyImpl;
-pub use interface::ERC20ABIDispatcher;
-pub use interface::ERC20ABIDispatcherTrait;
+pub use erc20::{ERC20Component, ERC20HooksEmptyImpl};
+pub use interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait};
diff --git a/packages/token/src/erc721.cairo b/packages/token/src/erc721.cairo
index 28c210486..3a08d821a 100644
--- a/packages/token/src/erc721.cairo
+++ b/packages/token/src/erc721.cairo
@@ -5,8 +5,6 @@ pub mod erc721_receiver;
 pub mod extensions;
 pub mod interface;
 
-pub use erc721::ERC721Component;
-pub use erc721::ERC721HooksEmptyImpl;
+pub use erc721::{ERC721Component, ERC721HooksEmptyImpl};
 pub use erc721_receiver::ERC721ReceiverComponent;
-pub use interface::ERC721ABIDispatcher;
-pub use interface::ERC721ABIDispatcherTrait;
+pub use interface::{ERC721ABIDispatcher, ERC721ABIDispatcherTrait};
diff --git a/packages/utils/src/cryptography/nonces.cairo b/packages/utils/src/cryptography/nonces.cairo
index 94951a26f..2473c3f33 100644
--- a/packages/utils/src/cryptography/nonces.cairo
+++ b/packages/utils/src/cryptography/nonces.cairo
@@ -1,6 +1,11 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts for Cairo v0.16.0 (utils/cryptography/nonces.cairo)
 
+/// # Nonces Component
+///
+/// The Nonces component provides a simple mechanism for handling incremental
+/// nonces for a set of addresses. It is commonly used to prevent replay attacks
+/// when contracts accept signatures as input.
 #[starknet::component]
 pub mod NoncesComponent {
     use crate::interfaces::INonces;


Test patch:
diff --git a/packages/account/src/tests.cairo b/packages/account/src/tests.cairo
index fe26cd923..c02171f01 100644
--- a/packages/account/src/tests.cairo
+++ b/packages/account/src/tests.cairo
@@ -1,3 +1,4 @@
+mod extensions;
 pub(crate) mod mocks;
 
 #[cfg(test)]
diff --git a/packages/account/src/tests/extensions.cairo b/packages/account/src/tests/extensions.cairo
new file mode 100644
index 000000000..ea5d3f334
--- /dev/null
+++ b/packages/account/src/tests/extensions.cairo
@@ -0,0 +1,4 @@
+#[cfg(test)]
+mod test_snip12_utils;
+#[cfg(test)]
+mod test_src9;
diff --git a/packages/account/src/tests/extensions/test_snip12_utils.cairo b/packages/account/src/tests/extensions/test_snip12_utils.cairo
new file mode 100644
index 000000000..1cf37af3a
--- /dev/null
+++ b/packages/account/src/tests/extensions/test_snip12_utils.cairo
@@ -0,0 +1,81 @@
+use crate::extensions::src9::interface::OutsideExecution;
+use crate::extensions::src9::snip12_utils::{CallStructHash, OutsideExecutionStructHash};
+use crate::extensions::src9::snip12_utils::{OUTSIDE_EXECUTION_TYPE_HASH, CALL_TYPE_HASH};
+use openzeppelin_testing::constants::{RECIPIENT, OWNER};
+use snforge_std::start_cheat_chain_id_global;
+use starknet::account::Call;
+
+#[test]
+fn test_outside_execution_type_hash() {
+    let expected = selector!(
+        "\"OutsideExecution\"(\"Caller\":\"ContractAddress\",\"Nonce\":\"felt\",\"Execute After\":\"u128\",\"Execute Before\":\"u128\",\"Calls\":\"Call*\")\"Call\"(\"To\":\"ContractAddress\",\"Selector\":\"selector\",\"Calldata\":\"felt*\")"
+    );
+    assert_eq!(OUTSIDE_EXECUTION_TYPE_HASH, expected);
+}
+
+#[test]
+fn test_call_type_hash() {
+    let expected = selector!(
+        "\"Call\"(\"To\":\"ContractAddress\",\"Selector\":\"selector\",\"Calldata\":\"felt*\")"
+    );
+    assert_eq!(CALL_TYPE_HASH, expected);
+}
+
+#[test]
+fn test_call_struct_hash_generation() {
+    start_cheat_chain_id_global('SN_TEST');
+
+    let to = RECIPIENT();
+    let selector = selector!("selector");
+    let calldata = array![1, 2, 3, 4, 5].span();
+    let call = Call { to, selector, calldata };
+
+    let hash = call.hash_struct();
+    // This hash was computed using starknet js sdk from the following values:
+    //
+    // - to: 'RECIPIENT'
+    // - selector: 'selector'
+    // - calldata: [1, 2, 3, 4, 5]
+    // - revision: '1'
+    let expected_hash = 0x4ae967778b464ac19d41ebe243aae57e06fab783c88e60612d48cd671d3b78d;
+    assert_eq!(hash, expected_hash);
+}
+
+#[test]
+fn test_outside_execution_struct_hash_generation() {
+    start_cheat_chain_id_global('SN_TEST');
+
+    // Call
+    let to = RECIPIENT();
+    let selector = selector!("selector");
+    let calldata = array![1, 2, 3, 4, 5].span();
+    let call = Call { to, selector, calldata };
+
+    // OutsideExecution
+    let caller = OWNER();
+    let nonce = 5;
+    let execute_after = 10;
+    let execute_before = 20;
+    let calls = array![call].span();
+    let outside_execution = OutsideExecution {
+        caller, nonce, execute_after, execute_before, calls
+    };
+
+    let hash = outside_execution.hash_struct();
+    // This hash was computed using starknet js sdk from the following values:
+    //
+    // OutsideExecution:
+    // - caller: 'OWNER'
+    // - nonce: 5
+    // - execute_after: 10
+    // - execute_before: 20
+    // - calls: [Call]
+    // - revision: '1'
+    //
+    // Call:
+    // - to: 'RECIPIENT'
+    // - selector: 'selector'
+    // - calldata: [1, 2, 3, 4, 5]
+    let expected_hash = 0x6eaa7a55ee940f04bf2a784c616ecc43864042fa2c61ad8243512e20eb368d;
+    assert_eq!(hash, expected_hash);
+}
diff --git a/packages/account/src/tests/extensions/test_src9.cairo b/packages/account/src/tests/extensions/test_src9.cairo
new file mode 100644
index 000000000..74f098489
--- /dev/null
+++ b/packages/account/src/tests/extensions/test_src9.cairo
@@ -0,0 +1,272 @@
+use crate::extensions::SRC9Component::{InternalImpl, OutsideExecutionV2Impl, SNIP12MetadataImpl};
+use crate::extensions::SRC9Component;
+use crate::extensions::src9::interface::{ISRC9_V2Dispatcher, ISRC9_V2DispatcherTrait};
+use crate::extensions::src9::interface::{OutsideExecution, ISRC9_V2_ID};
+use crate::extensions::src9::snip12_utils::OutsideExecutionStructHash;
+use crate::tests::mocks::src9_mocks::SRC9AccountMock;
+use openzeppelin_introspection::interface::{ISRC5, ISRC5_ID};
+use openzeppelin_testing as utils;
+use openzeppelin_testing::constants::{RECIPIENT, OWNER, OTHER, FELT_VALUE};
+use openzeppelin_utils::cryptography::snip12::OffchainMessageHash;
+use snforge_std::signature::KeyPairTrait;
+use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl};
+use snforge_std::{
+    start_cheat_caller_address, cheat_caller_address, start_cheat_block_timestamp_global
+};
+use snforge_std::{test_address, load, CheatSpan};
+use starknet::account::Call;
+use starknet::storage::StorageMapWriteAccess;
+use starknet::{ContractAddress, contract_address_const};
+
+//
+// Setup
+//
+
+type ComponentState = SRC9Component::ComponentState<SRC9AccountMock::ContractState>;
+
+fn CONTRACT_STATE() -> SRC9AccountMock::ContractState {
+    SRC9AccountMock::contract_state_for_testing()
+}
+
+fn COMPONENT_STATE() -> ComponentState {
+    SRC9Component::component_state_for_testing()
+}
+
+fn setup() -> ComponentState {
+    let mut state = COMPONENT_STATE();
+    state.initializer();
+    state
+}
+
+fn setup_account(public_key: felt252) -> ContractAddress {
+    let mut calldata = array![public_key];
+    utils::declare_and_deploy("SRC9AccountMock", calldata)
+}
+
+//
+// initializer
+//
+
+#[test]
+fn test_initializer() {
+    let mut state = COMPONENT_STATE();
+    let mock_state = CONTRACT_STATE();
+
+    state.initializer();
+
+    let supports_isrc5 = mock_state.supports_interface(ISRC5_ID);
+    assert!(supports_isrc5);
+
+    let supports_ioutside_execution_v2 = mock_state.supports_interface(ISRC9_V2_ID);
+    assert!(supports_ioutside_execution_v2);
+}
+
+//
+// execute_from_outside_v2
+//
+
+#[test]
+fn test_execute_from_outside_v2_any_caller() {
+    let key_pair = KeyPairTrait::generate();
+    let account = setup_account(key_pair.public_key);
+    let outside_execution = setup_outside_execution(account, false);
+
+    let msg_hash = outside_execution.get_message_hash(account);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    // Use the dispatcher to simulate the appropriate context
+    let dispatcher = ISRC9_V2Dispatcher { contract_address: account };
+    dispatcher.execute_from_outside_v2(outside_execution, array![r, s].span());
+
+    assert_value(account, FELT_VALUE);
+}
+
+#[test]
+fn test_execute_from_outside_v2_specific_caller() {
+    let key_pair = KeyPairTrait::generate();
+    let account = setup_account(key_pair.public_key);
+    let mut outside_execution = setup_outside_execution(account, false);
+    outside_execution.caller = OWNER();
+
+    let msg_hash = outside_execution.get_message_hash(account);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    cheat_caller_address(account, OWNER(), CheatSpan::TargetCalls(1));
+
+    // Use the dispatcher to simulate the appropriate context
+    let dispatcher = ISRC9_V2Dispatcher { contract_address: account };
+    dispatcher.execute_from_outside_v2(outside_execution, array![r, s].span());
+
+    assert_value(account, FELT_VALUE);
+}
+
+#[test]
+fn test_execute_from_outside_v2_uses_nonce() {
+    let key_pair = KeyPairTrait::generate();
+    let account = setup_account(key_pair.public_key);
+    let outside_execution = setup_outside_execution(account, false);
+
+    // Use the dispatcher to simulate the appropriate context
+    let dispatcher = ISRC9_V2Dispatcher { contract_address: account };
+
+    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);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    dispatcher.execute_from_outside_v2(outside_execution, array![r, s].span());
+
+    assert_value(account, 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 mut state = setup();
+    let mut outside_execution = setup_outside_execution(RECIPIENT(), false);
+    outside_execution.caller = OWNER();
+
+    start_cheat_caller_address(test_address(), OTHER());
+
+    state.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 mut state = setup();
+    let outside_execution = setup_outside_execution(RECIPIENT(), false);
+
+    start_cheat_block_timestamp_global(25);
+
+    state.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 mut state = setup();
+    let outside_execution = setup_outside_execution(RECIPIENT(), false);
+
+    start_cheat_block_timestamp_global(20);
+
+    state.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 mut state = setup();
+    let outside_execution = setup_outside_execution(RECIPIENT(), false);
+
+    start_cheat_block_timestamp_global(5);
+
+    state.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 mut state = setup();
+    let outside_execution = setup_outside_execution(RECIPIENT(), false);
+
+    start_cheat_block_timestamp_global(10);
+
+    state.execute_from_outside_v2(outside_execution, array![].span());
+}
+
+#[test]
+#[should_panic(expected: ('SRC9: duplicated nonce',))]
+fn test_execute_from_outside_v2_invalid_nonce() {
+    let mut state = setup();
+    let outside_execution = setup_outside_execution(RECIPIENT(), false);
+
+    state.SRC9_nonces.write(outside_execution.nonce, true);
+
+    state.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 = KeyPairTrait::generate();
+    let account = setup_account(key_pair.public_key);
+    let outside_execution = setup_outside_execution(account, false);
+
+    let msg_hash = outside_execution.get_message_hash(account);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+    let invalid_signature = array![r, s + 1].span();
+
+    // Use the dispatcher to simulate the appropriate context
+    let dispatcher = ISRC9_V2Dispatcher { contract_address: account };
+    dispatcher.execute_from_outside_v2(outside_execution, invalid_signature);
+}
+
+#[test]
+#[should_panic(expected: "Some error")]
+fn test_execute_from_outside_v2_panics_when_inner_call_panic() {
+    let key_pair = KeyPairTrait::generate();
+    let account = setup_account(key_pair.public_key);
+    let outside_execution = setup_outside_execution(account, true);
+
+    let msg_hash = outside_execution.get_message_hash(account);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    // Use the dispatcher to simulate the appropriate context
+    let dispatcher = ISRC9_V2Dispatcher { contract_address: account };
+    dispatcher.execute_from_outside_v2(outside_execution, array![r, s].span());
+}
+
+//
+// Getters
+//
+
+#[test]
+fn test_is_valid_outside_execution_nonce_valid() {
+    let state = setup();
+
+    let nonce = 5;
+    let is_valid_nonce = state.is_valid_outside_execution_nonce(nonce);
+    assert!(is_valid_nonce);
+}
+
+#[test]
+fn test_is_valid_outside_execution_nonce_invalid() {
+    let mut state = setup();
+
+    let nonce = 5;
+    state.SRC9_nonces.write(nonce, true);
+    let is_invalid_nonce = !state.is_valid_outside_execution_nonce(nonce);
+    assert!(is_invalid_nonce);
+}
+
+//
+// Helpers
+//
+
+fn setup_outside_execution(target: ContractAddress, panic: bool) -> OutsideExecution {
+    let call = Call {
+        to: target,
+        selector: selector!("set_value"),
+        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!("value"), 1).at(0);
+    assert_eq!(value, expected_value);
+}
diff --git a/packages/account/src/tests/mocks.cairo b/packages/account/src/tests/mocks.cairo
index 5fd0e4c04..3d873c28b 100644
--- a/packages/account/src/tests/mocks.cairo
+++ b/packages/account/src/tests/mocks.cairo
@@ -2,3 +2,4 @@ pub(crate) mod account_mocks;
 pub(crate) mod eth_account_mocks;
 pub(crate) mod non_implementing_mock;
 pub(crate) mod simple_mock;
+pub(crate) mod src9_mocks;
diff --git a/packages/account/src/tests/mocks/src9_mocks.cairo b/packages/account/src/tests/mocks/src9_mocks.cairo
new file mode 100644
index 000000000..0f41818f2
--- /dev/null
+++ b/packages/account/src/tests/mocks/src9_mocks.cairo
@@ -0,0 +1,69 @@
+#[starknet::contract(account)]
+pub(crate) mod SRC9AccountMock {
+    use crate::AccountComponent;
+    use crate::extensions::SRC9Component;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::storage::StoragePointerWriteAccess;
+
+    component!(path: AccountComponent, storage: account, event: AccountEvent);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+    component!(path: SRC9Component, storage: src9, event: SRC9Event);
+
+    // Account
+    #[abi(embed_v0)]
+    impl SRC6Impl = AccountComponent::SRC6Impl<ContractState>;
+    #[abi(embed_v0)]
+    impl PublicKeyImpl = AccountComponent::PublicKeyImpl<ContractState>;
+    impl AccountInternalImpl = AccountComponent::InternalImpl<ContractState>;
+
+    // SRC9
+    #[abi(embed_v0)]
+    impl OutsideExecutionV2Impl =
+        SRC9Component::OutsideExecutionV2Impl<ContractState>;
+    impl OutsideExecutionInternalImpl = SRC9Component::InternalImpl<ContractState>;
+
+    // SCR5
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    #[storage]
+    pub struct Storage {
+        pub value: felt252,
+        #[substorage(v0)]
+        pub account: AccountComponent::Storage,
+        #[substorage(v0)]
+        pub src9: SRC9Component::Storage,
+        #[substorage(v0)]
+        pub src5: SRC5Component::Storage
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        AccountEvent: AccountComponent::Event,
+        #[flat]
+        SRC9Event: SRC9Component::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event
+    }
+
+    #[constructor]
+    fn constructor(ref self: ContractState, public_key: felt252) {
+        self.account.initializer(public_key);
+        self.src9.initializer();
+    }
+
+    #[abi(per_item)]
+    #[generate_trait]
+    impl ExternalImpl of ExternalTrait {
+        #[external(v0)]
+        fn set_value(ref self: ContractState, value: felt252, panic: bool) {
+            self.account.assert_only_self();
+            if panic {
+                panic!("Some error");
+            }
+            self.value.write(value);
+        }
+    }
+}
diff --git a/packages/presets/src/tests/test_vesting.cairo b/packages/presets/src/tests/test_vesting.cairo
index d3e17d7b6..657c87df7 100644
--- a/packages/presets/src/tests/test_vesting.cairo
+++ b/packages/presets/src/tests/test_vesting.cairo
@@ -1,8 +1,6 @@
 use openzeppelin_access::ownable::interface::{IOwnableDispatcher, IOwnableDispatcherTrait};
 use openzeppelin_finance::vesting::VestingComponent::InternalImpl;
-use openzeppelin_finance::vesting::VestingComponent;
 use openzeppelin_finance::vesting::interface::{IVestingDispatcher, IVestingDispatcherTrait};
-use openzeppelin_presets::vesting::VestingWallet;
 use openzeppelin_test_common::erc20::deploy_erc20;
 use openzeppelin_test_common::vesting::VestingSpyHelpers;
 use openzeppelin_testing as utils;