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

Issue:
Governance Contracts
The solidity OpenZeppelin repo has a module for [on-chain governance](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/governance). It might be useful to have a similar standard on starknet to allow projects to easily add governance to their projects.

I would love to work on defining the standard for starknet and implement it if this is of interest.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c8efd650..1c9cf83c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - SRC9 (Outside Execution) integration to account presets (#1201)
+- `SNIP12HashSpanImpl` to `openzeppelin_utils::cryptography::snip12` (#1180)
+- GovernorComponent with the following extensions: (#1180)
+  - GovernorCoreExecutionComponent
+  - GovernorCountingSimpleComponent
+  - GovernorSettingsComponent
+  - GovernorTimelockExecutionComponent
+  - GovernorVotesQuorumFractionComponent
+  - GovernorVotesComponent
 - `is_tx_version_valid` utility function to `openzeppelin_account::utils` (#1224)
 
 ### Changed
diff --git a/packages/governance/Scarb.toml b/packages/governance/Scarb.toml
index d32d87a15..d9afb52cf 100644
--- a/packages/governance/Scarb.toml
+++ b/packages/governance/Scarb.toml
@@ -46,6 +46,8 @@ casm = false
 name = "openzeppelin_governance_unittest"
 build-external-contracts = [
     "openzeppelin_test_common::mocks::account::SnakeAccountMock",
+    "openzeppelin_test_common::mocks::governor::GovernorMock",
+    "openzeppelin_test_common::mocks::governor::GovernorTimelockedMock",
     "openzeppelin_test_common::mocks::timelock::TimelockControllerMock",
     "openzeppelin_test_common::mocks::timelock::MockContract",
     "openzeppelin_test_common::mocks::timelock::TimelockAttackerMock",
diff --git a/packages/governance/src/governor.cairo b/packages/governance/src/governor.cairo
new file mode 100644
index 000000000..4d2442162
--- /dev/null
+++ b/packages/governance/src/governor.cairo
@@ -0,0 +1,8 @@
+pub mod extensions;
+pub mod governor;
+pub mod interface;
+pub mod proposal_core;
+pub mod vote;
+
+pub use governor::{GovernorComponent, DefaultConfig};
+pub use proposal_core::ProposalCore;
diff --git a/packages/governance/src/governor/extensions.cairo b/packages/governance/src/governor/extensions.cairo
new file mode 100644
index 000000000..c3364c162
--- /dev/null
+++ b/packages/governance/src/governor/extensions.cairo
@@ -0,0 +1,14 @@
+pub mod governor_core_execution;
+pub mod governor_counting_simple;
+pub mod governor_settings;
+pub mod governor_timelock_execution;
+pub mod governor_votes;
+pub mod governor_votes_quorum_fraction;
+pub mod interface;
+
+pub use governor_core_execution::GovernorCoreExecutionComponent;
+pub use governor_counting_simple::GovernorCountingSimpleComponent;
+pub use governor_settings::GovernorSettingsComponent;
+pub use governor_timelock_execution::GovernorTimelockExecutionComponent;
+pub use governor_votes::GovernorVotesComponent;
+pub use governor_votes_quorum_fraction::GovernorVotesQuorumFractionComponent;
diff --git a/packages/governance/src/governor/extensions/governor_core_execution.cairo b/packages/governance/src/governor/extensions/governor_core_execution.cairo
new file mode 100644
index 000000000..98aeaeeae
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_core_execution.cairo
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_core_execution.cairo)
+
+/// # GovernorCoreExecution Component
+///
+/// Extension of GovernorComponent providing an execution mechanism directly through
+/// the Governor itself. For a timelocked execution mechanism, see
+/// GovernorTimelockExecutionComponent.
+#[starknet::component]
+pub mod GovernorCoreExecutionComponent {
+    use crate::governor::GovernorComponent::{
+        InternalExtendedTrait, ComponentState as GovernorComponentState
+    };
+    use crate::governor::GovernorComponent;
+    use crate::governor::interface::ProposalState;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::account::Call;
+    use starknet::{ContractAddress, SyscallResultTrait};
+
+    #[storage]
+    pub struct Storage {}
+
+    //
+    // Extensions
+    //
+
+    pub impl GovernorExecution<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorSettingsTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorCoreExecution: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorExecutionTrait<TContractState> {
+        /// See `GovernorComponent::GovernorExecutionTrait::state`.
+        fn state(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> ProposalState {
+            self._state(proposal_id)
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::executor`.
+        fn executor(self: @GovernorComponentState<TContractState>) -> ContractAddress {
+            starknet::get_contract_address()
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::execute_operations`.
+        fn execute_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        ) {
+            for call in calls {
+                let Call { to, selector, calldata } = *call;
+                starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall();
+            };
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::queue_operations`.
+        fn queue_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        ) -> u64 {
+            0
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::proposal_needs_queuing`.
+        fn proposal_needs_queuing(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> bool {
+            false
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::cancel_operations`.
+        fn cancel_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            description_hash: felt252
+        ) {
+            self._cancel(proposal_id, description_hash);
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/governor_counting_simple.cairo b/packages/governance/src/governor/extensions/governor_counting_simple.cairo
new file mode 100644
index 000000000..c69745cdd
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_counting_simple.cairo
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_counting_simple.cairo)
+
+/// # GovernorCountingSimple Component
+///
+/// Extension of GovernorComponent for simple vote counting with three options.
+#[starknet::component]
+pub mod GovernorCountingSimpleComponent {
+    use crate::governor::GovernorComponent::{
+        InternalTrait, ComponentState as GovernorComponentState
+    };
+    use crate::governor::GovernorComponent;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::ContractAddress;
+    use starknet::storage::{Map, StoragePathEntry, StorageMapReadAccess, StorageMapWriteAccess};
+    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+
+    type ProposalId = felt252;
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_proposals_votes: Map<ProposalId, ProposalVote>,
+    }
+
+    /// Supported vote types.
+    #[derive(Drop, PartialEq, Debug)]
+    pub enum VoteType {
+        Against,
+        For,
+        Abstain
+    }
+
+    impl U8TryIntoVoteType of TryInto<u8, VoteType> {
+        fn try_into(self: u8) -> Option<VoteType> {
+            match self {
+                0 => Option::Some(VoteType::Against),
+                1 => Option::Some(VoteType::For),
+                2 => Option::Some(VoteType::Abstain),
+                _ => Option::None,
+            }
+        }
+    }
+
+    impl VoteTypeIntoU8 of Into<VoteType, u8> {
+        fn into(self: VoteType) -> u8 {
+            match self {
+                VoteType::Against => 0,
+                VoteType::For => 1,
+                VoteType::Abstain => 2
+            }
+        }
+    }
+
+    #[starknet::storage_node]
+    pub struct ProposalVote {
+        pub against_votes: u256,
+        pub for_votes: u256,
+        pub abstain_votes: u256,
+        pub has_voted: Map<ContractAddress, bool>
+    }
+
+    pub mod Errors {
+        pub const ALREADY_CAST_VOTE: felt252 = 'Already cast vote';
+        pub const INVALID_VOTE_TYPE: felt252 = 'Invalid vote type';
+    }
+
+    //
+    // Extensions
+    //
+
+    pub impl GovernorCounting<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +GovernorComponent::GovernorQuorumTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorCountingSimple: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorCountingTrait<TContractState> {
+        /// See `GovernorComponent::GovernorCountingTrait::counting_mode`.
+        fn counting_mode(self: @GovernorComponentState<TContractState>) -> ByteArray {
+            return "support=bravo&quorum=for,abstain";
+        }
+
+        /// See `GovernorComponent::GovernorCountingTrait::count_vote`.
+        ///
+        /// In this module, the support follows the `VoteType` enum (from Governor Bravo).
+        fn count_vote(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            account: ContractAddress,
+            support: u8,
+            total_weight: u256,
+            params: Span<felt252>
+        ) -> u256 {
+            let mut contract = self.get_contract_mut();
+            let mut this_component = GovernorCountingSimple::get_component_mut(ref contract);
+
+            let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id);
+            assert(!proposal_votes.has_voted.read(account), Errors::ALREADY_CAST_VOTE);
+
+            proposal_votes.has_voted.write(account, true);
+
+            let support: VoteType = support.try_into().expect(Errors::INVALID_VOTE_TYPE);
+            match support {
+                VoteType::Against => {
+                    let current_votes = proposal_votes.against_votes.read();
+                    proposal_votes.against_votes.write(current_votes + total_weight);
+                },
+                VoteType::For => {
+                    let current_votes = proposal_votes.for_votes.read();
+                    proposal_votes.for_votes.write(current_votes + total_weight);
+                },
+                VoteType::Abstain => {
+                    let current_votes = proposal_votes.abstain_votes.read();
+                    proposal_votes.abstain_votes.write(current_votes + total_weight);
+                }
+            }
+            total_weight
+        }
+
+        /// See `GovernorComponent::GovernorCountingTrait::has_voted`.
+        fn has_voted(
+            self: @GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            account: ContractAddress
+        ) -> bool {
+            let contract = self.get_contract();
+            let this_component = GovernorCountingSimple::get_component(contract);
+            let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id);
+
+            proposal_votes.has_voted.read(account)
+        }
+
+        /// See `GovernorComponent::GovernorCountingTrait::quorum_reached`.
+        fn quorum_reached(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> bool {
+            let contract = self.get_contract();
+            let this_component = GovernorCountingSimple::get_component(contract);
+
+            let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id);
+            let snapshot = self._proposal_snapshot(proposal_id);
+
+            self.quorum(snapshot) <= proposal_votes.for_votes.read()
+                + proposal_votes.abstain_votes.read()
+        }
+
+        /// See `GovernorComponent::GovernorCountingTrait::vote_succeeded`.
+        ///
+        /// In this module, the `for_votes` must be strictly over the `against_votes`.
+        fn vote_succeeded(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> bool {
+            let contract = self.get_contract();
+            let this_component = GovernorCountingSimple::get_component(contract);
+            let proposal_votes = this_component.Governor_proposals_votes.entry(proposal_id);
+
+            proposal_votes.for_votes.read() > proposal_votes.against_votes.read()
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/governor_settings.cairo b/packages/governance/src/governor/extensions/governor_settings.cairo
new file mode 100644
index 000000000..9ad903326
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_settings.cairo
@@ -0,0 +1,251 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_settings.cairo)
+
+/// # GovernorSettings Component
+///
+/// Extension of GovernorComponent for settings updatable through governance.
+#[starknet::component]
+pub mod GovernorSettingsComponent {
+    use crate::governor::GovernorComponent::{
+        InternalExtendedTrait, ComponentState as GovernorComponentState
+    };
+    use crate::governor::GovernorComponent;
+    use crate::governor::extensions::interface::IGovernorSettingsAdmin;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_voting_delay: u64,
+        pub Governor_voting_period: u64,
+        pub Governor_proposal_threshold: u256,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    pub enum Event {
+        VotingDelayUpdated: VotingDelayUpdated,
+        VotingPeriodUpdated: VotingPeriodUpdated,
+        ProposalThresholdUpdated: ProposalThresholdUpdated
+    }
+
+    /// Emitted when `Governor_voting_delay` is updated.
+    #[derive(Drop, starknet::Event)]
+    pub struct VotingDelayUpdated {
+        pub old_voting_delay: u64,
+        pub new_voting_delay: u64
+    }
+
+    /// Emitted when `Governor_voting_period` is updated.
+    #[derive(Drop, starknet::Event)]
+    pub struct VotingPeriodUpdated {
+        pub old_voting_period: u64,
+        pub new_voting_period: u64
+    }
+
+    /// Emitted when `Governor_proposal_threshold` is updated.
+    #[derive(Drop, starknet::Event)]
+    pub struct ProposalThresholdUpdated {
+        pub old_proposal_threshold: u256,
+        pub new_proposal_threshold: u256
+    }
+
+    pub mod Errors {
+        pub const INVALID_VOTING_PERIOD: felt252 = 'Invalid voting period';
+    }
+
+    //
+    // Extensions
+    //
+
+    pub impl GovernorSettings<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorSettings: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorSettingsTrait<TContractState> {
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`.
+        fn voting_delay(self: @GovernorComponentState<TContractState>) -> u64 {
+            let contract = self.get_contract();
+            let this_component = GovernorSettings::get_component(contract);
+
+            this_component.Governor_voting_delay.read()
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_period`.
+        fn voting_period(self: @GovernorComponentState<TContractState>) -> u64 {
+            let contract = self.get_contract();
+            let this_component = GovernorSettings::get_component(contract);
+
+            this_component.Governor_voting_period.read()
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`.
+        fn proposal_threshold(self: @GovernorComponentState<TContractState>) -> u256 {
+            let contract = self.get_contract();
+            let this_component = GovernorSettings::get_component(contract);
+
+            this_component.Governor_proposal_threshold.read()
+        }
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(GovernorSettingsAdminImpl)]
+    impl GovernorSettingsAdmin<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::HasComponent<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorExecutionTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of IGovernorSettingsAdmin<ComponentState<TContractState>> {
+        /// Sets the voting delay.
+        ///
+        /// Requirements:
+        ///
+        /// - Caller must be the governance executor.
+        ///
+        /// NOTE: This function does not emit an event if the new voting delay is the same as the
+        /// old one.
+        ///
+        /// May emit a `VotingDelayUpdated` event.
+        fn set_voting_delay(ref self: ComponentState<TContractState>, new_voting_delay: u64) {
+            self.assert_only_governance();
+            self._set_voting_delay(new_voting_delay);
+        }
+
+        /// Sets the voting period.
+        ///
+        /// NOTE: This function does not emit an event if the new voting period is the same as the
+        /// old one.
+        ///
+        /// Requirements:
+        ///
+        /// - Caller must be the governance executor.
+        /// - `new_voting_period` must be greater than 0.
+        ///
+        /// May emit a `VotingPeriodUpdated` event.
+        fn set_voting_period(ref self: ComponentState<TContractState>, new_voting_period: u64) {
+            self.assert_only_governance();
+            self._set_voting_period(new_voting_period);
+        }
+
+        /// Sets the proposal threshold.
+        ///
+        /// NOTE: This function does not emit an event if the new proposal threshold is the same as
+        /// the old one.
+        ///
+        /// Requirements:
+        ///
+        /// - Caller must be the governance executor.
+        ///
+        /// May emit a `ProposalThresholdUpdated` event.
+        fn set_proposal_threshold(
+            ref self: ComponentState<TContractState>, new_proposal_threshold: u256
+        ) {
+            self.assert_only_governance();
+            self._set_proposal_threshold(new_proposal_threshold);
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorExecutionTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl Governor: GovernorComponent::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the component by setting the default values.
+        ///
+        /// Requirements:
+        ///
+        /// - `new_voting_period` must be greater than 0.
+        ///
+        /// Emits a `VotingDelayUpdated`, `VotingPeriodUpdated`, and `ProposalThresholdUpdated`
+        /// event.
+        fn initializer(
+            ref self: ComponentState<TContractState>,
+            new_voting_delay: u64,
+            new_voting_period: u64,
+            new_proposal_threshold: u256
+        ) {
+            self._set_voting_delay(new_voting_delay);
+            self._set_voting_period(new_voting_period);
+            self._set_proposal_threshold(new_proposal_threshold);
+        }
+
+        /// Wrapper for `Governor::assert_only_governance`.
+        fn assert_only_governance(self: @ComponentState<TContractState>) {
+            let governor_component = get_dep_component!(self, Governor);
+            governor_component.assert_only_governance();
+        }
+
+        /// Internal function to update the voting delay.
+        ///
+        /// NOTE: This function does not emit an event if the new voting delay is the same as the
+        /// old one.
+        ///
+        /// May emit a `VotingDelayUpdated` event.
+        fn _set_voting_delay(ref self: ComponentState<TContractState>, new_voting_delay: u64) {
+            let old_voting_delay = self.Governor_voting_delay.read();
+            if old_voting_delay != new_voting_delay {
+                self.emit(VotingDelayUpdated { old_voting_delay, new_voting_delay });
+                self.Governor_voting_delay.write(new_voting_delay);
+            }
+        }
+
+        /// Internal function to update the voting period.
+        ///
+        /// Requirements:
+        ///
+        /// - `new_voting_period` must be greater than 0.
+        ///
+        /// NOTE: This function does not emit an event if the new voting period is the same as the
+        /// old one.
+        ///
+        /// May emit a `VotingPeriodUpdated` event.
+        fn _set_voting_period(ref self: ComponentState<TContractState>, new_voting_period: u64) {
+            assert(new_voting_period > 0, Errors::INVALID_VOTING_PERIOD);
+
+            let old_voting_period = self.Governor_voting_period.read();
+            if old_voting_period != new_voting_period {
+                self.emit(VotingPeriodUpdated { old_voting_period, new_voting_period });
+                self.Governor_voting_period.write(new_voting_period);
+            }
+        }
+
+        /// Internal function to update the proposal threshold.
+        ///
+        /// NOTE: This function does not emit an event if the new proposal threshold is the same as
+        /// the old one.
+        ///
+        /// May emit a `ProposalThresholdUpdated` event.
+        fn _set_proposal_threshold(
+            ref self: ComponentState<TContractState>, new_proposal_threshold: u256
+        ) {
+            let old_proposal_threshold = self.Governor_proposal_threshold.read();
+            if old_proposal_threshold != new_proposal_threshold {
+                let event = ProposalThresholdUpdated {
+                    old_proposal_threshold, new_proposal_threshold
+                };
+                self.emit(event);
+                self.Governor_proposal_threshold.write(new_proposal_threshold);
+            }
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/governor_timelock_execution.cairo b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo
new file mode 100644
index 000000000..435733e0a
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_timelock_execution.cairo
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_core_execution.cairo)
+
+/// # GovernorTimelockExecution Component
+///
+/// Extension of GovernorComponent that binds the execution process to an instance of a contract
+/// implementing TimelockControllerComponent. This adds a delay, enforced by the TimelockController
+/// to all successful proposal (in addition to the voting duration).
+///
+/// NOTE: The Governor needs the PROPOSER, EXECUTOR, and CANCELLER roles to work properly.
+///
+/// Using this model means the proposal will be operated by the TimelockController and not by the
+/// Governor. Thus, the assets and permissions must be attached to the TimelockController. Any asset
+/// sent to the Governor will be inaccessible from a proposal, unless executed via
+/// `Governor::relay`.
+///
+/// WARNING: Setting up the TimelockController to have additional proposers or cancellers besides
+/// the governor is very risky, as it grants them the ability to: 1) execute operations as the
+/// timelock, and thus possibly performing operations or accessing funds that are expected to only
+/// be accessible through a vote, and 2) block governance proposals that have been approved by the
+/// voters, effectively executing a Denial of Service attack.
+#[starknet::component]
+pub mod GovernorTimelockExecutionComponent {
+    use core::num::traits::Zero;
+    use crate::governor::GovernorComponent::{
+        InternalExtendedTrait, ComponentState as GovernorComponentState
+    };
+    use crate::governor::GovernorComponent;
+    use crate::governor::extensions::interface::ITimelocked;
+    use crate::governor::interface::ProposalState;
+    use crate::timelock::interface::{OperationState, ITimelockDispatcher, ITimelockDispatcherTrait};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::ContractAddress;
+    use starknet::account::Call;
+    use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
+    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+
+    type ProposalId = felt252;
+    type TimelockProposalId = felt252;
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_timelock_controller: ContractAddress,
+        pub Governor_timelock_ids: Map<ProposalId, TimelockProposalId>
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    pub enum Event {
+        TimelockUpdated: TimelockUpdated,
+    }
+
+    /// Emitted when the timelock controller used for proposal execution is modified.
+    #[derive(Drop, starknet::Event)]
+    pub struct TimelockUpdated {
+        pub old_timelock: ContractAddress,
+        pub new_timelock: ContractAddress
+    }
+
+    pub mod Errors {
+        pub const INVALID_TIMELOCK_CONTROLLER: felt252 = 'Invalid timelock controller';
+    }
+
+    //
+    // Extensions
+    //
+
+    /// NOTE: Some of these function can reenter through the external calls to the timelock, but we
+    /// assume the timelock is trusted and well behaved (according to TimelockController) and this
+    /// will not happen.
+    pub impl GovernorExecution<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +GovernorComponent::GovernorSettingsTrait<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorCoreExecution: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorExecutionTrait<TContractState> {
+        /// See `GovernorComponent::GovernorExecutionTrait::state`.
+        fn state(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> ProposalState {
+            let current_state = self._state(proposal_id);
+
+            if current_state != ProposalState::Queued {
+                return current_state;
+            }
+
+            let contract = self.get_contract();
+            let this_component = GovernorCoreExecution::get_component(contract);
+
+            let queue_id = this_component.Governor_timelock_ids.read(proposal_id);
+            let timelock_dispatcher = this_component.get_timelock_dispatcher();
+            let operation_state = timelock_dispatcher.get_operation_state(queue_id);
+
+            let is_timelock_operation_pending = operation_state == OperationState::Waiting
+                || operation_state == OperationState::Ready;
+            let is_timelock_operation_done = operation_state == OperationState::Done;
+
+            if is_timelock_operation_pending {
+                ProposalState::Queued
+            } else if is_timelock_operation_done {
+                // This can happen if the proposal is executed directly on the timelock.
+                ProposalState::Executed
+            } else {
+                // This can happen if the proposal is canceled directly on the timelock.
+                ProposalState::Canceled
+            }
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::executor`.
+        ///
+        /// In this module, the executor is the timelock controller.
+        fn executor(self: @GovernorComponentState<TContractState>) -> ContractAddress {
+            let contract = self.get_contract();
+            let this_component = GovernorCoreExecution::get_component(contract);
+
+            this_component.timelock()
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::execute_operations`.
+        ///
+        /// Runs the already queued proposal through the timelock.
+        fn execute_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        ) {
+            let mut contract = self.get_contract_mut();
+            let mut this_component = GovernorCoreExecution::get_component_mut(ref contract);
+
+            let timelock_dispatcher = this_component.get_timelock_dispatcher();
+
+            timelock_dispatcher
+                .execute_batch(calls, 0, this_component.timelock_salt(description_hash));
+
+            // Cleanup
+            this_component.Governor_timelock_ids.write(proposal_id, 0);
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::queue_operations`.
+        ///
+        /// Queue a proposal to the timelock.
+        fn queue_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        ) -> u64 {
+            let mut contract = self.get_contract_mut();
+            let mut this_component = GovernorCoreExecution::get_component_mut(ref contract);
+
+            let timelock_dispatcher = this_component.get_timelock_dispatcher();
+
+            let delay = timelock_dispatcher.get_min_delay();
+            let salt = this_component.timelock_salt(description_hash);
+
+            let queue_id = timelock_dispatcher.hash_operation_batch(calls, 0, salt);
+            this_component.Governor_timelock_ids.write(proposal_id, queue_id);
+
+            timelock_dispatcher.schedule_batch(calls, 0, salt, delay);
+
+            starknet::get_block_timestamp() + delay
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::proposal_needs_queuing`.
+        fn proposal_needs_queuing(
+            self: @GovernorComponentState<TContractState>, proposal_id: felt252
+        ) -> bool {
+            true
+        }
+
+        /// See `GovernorComponent::GovernorExecutionTrait::cancel_operations`.
+        ///
+        /// Cancels the timelocked proposal if it has already been queued.
+        fn cancel_operations(
+            ref self: GovernorComponentState<TContractState>,
+            proposal_id: felt252,
+            description_hash: felt252
+        ) {
+            self._cancel(proposal_id, description_hash);
+
+            let mut contract = self.get_contract_mut();
+            let mut this_component = GovernorCoreExecution::get_component_mut(ref contract);
+
+            let timelock_id = this_component.Governor_timelock_ids.read(proposal_id);
+            if timelock_id.is_non_zero() {
+                let timelock_dispatcher = this_component.get_timelock_dispatcher();
+
+                timelock_dispatcher.cancel(timelock_id);
+                this_component.Governor_timelock_ids.write(proposal_id, 0);
+            }
+        }
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(TimelockedImpl)]
+    impl Timelocked<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::GovernorSettingsTrait<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        +GovernorComponent::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of ITimelocked<ComponentState<TContractState>> {
+        /// Returns the token that voting power is sourced from.
+        fn timelock(self: @ComponentState<TContractState>) -> ContractAddress {
+            self.Governor_timelock_controller.read()
+        }
+
+        /// Returns the timelock proposal id for a given proposal id.
+        fn get_timelock_id(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> TimelockProposalId {
+            self.Governor_timelock_ids.read(proposal_id)
+        }
+
+        /// Updates the associated timelock.
+        ///
+        /// Requirements:
+        ///
+        /// - Caller must be the governance.
+        ///
+        /// Emits a `TimelockUpdated` event.
+        fn update_timelock(
+            ref self: ComponentState<TContractState>, new_timelock: ContractAddress
+        ) {
+            self.assert_only_governance();
+            self._update_timelock(new_timelock);
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::GovernorSettingsTrait<TContractState>,
+        +GovernorComponent::GovernorCountingTrait<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl Governor: GovernorComponent::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the component by setting the timelock contract address.
+        ///
+        /// Requirements:
+        ///
+        /// - `timelock_controller` must not be zero.
+        fn initializer(
+            ref self: ComponentState<TContractState>, timelock_controller: ContractAddress
+        ) {
+            assert(timelock_controller.is_non_zero(), Errors::INVALID_TIMELOCK_CONTROLLER);
+            self._update_timelock(timelock_controller);
+        }
+
+        /// Wrapper for `Governor::assert_only_governance`.
+        fn assert_only_governance(self: @ComponentState<TContractState>) {
+            let governor_component = get_dep_component!(self, Governor);
+            governor_component.assert_only_governance();
+        }
+
+        /// Computes the `TimelockController` operation salt.
+        ///
+        /// It is computed with the governor address itself to avoid collisions across
+        /// governor instances using the same timelock.
+        fn timelock_salt(
+            self: @ComponentState<TContractState>, description_hash: felt252
+        ) -> felt252 {
+            let description_hash: u256 = description_hash.into();
+            let this: felt252 = starknet::get_contract_address().into();
+
+            // Unwrap is safe since the u256 value came from a felt252.
+            (this.into() ^ description_hash).try_into().unwrap()
+        }
+
+        /// Returns the timelock contract address wrapped in a ITimelockDispatcher.
+        fn get_timelock_dispatcher(self: @ComponentState<TContractState>) -> ITimelockDispatcher {
+            let timelock_controller = self.timelock();
+            ITimelockDispatcher { contract_address: timelock_controller }
+        }
+
+        /// Updates the timelock contract address.
+        ///
+        /// Emits a `TimelockUpdated` event.
+        fn _update_timelock(
+            ref self: ComponentState<TContractState>, new_timelock: ContractAddress
+        ) {
+            let old_timelock = self.Governor_timelock_controller.read();
+            self.emit(TimelockUpdated { old_timelock, new_timelock });
+            self.Governor_timelock_controller.write(new_timelock);
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/governor_votes.cairo b/packages/governance/src/governor/extensions/governor_votes.cairo
new file mode 100644
index 000000000..d997bda35
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_votes.cairo
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_votes.cairo)
+
+/// # GovernorVotes Component
+///
+/// Extension of GovernorComponent for voting weight extraction from a token with the Votes
+/// extension.
+#[starknet::component]
+pub mod GovernorVotesComponent {
+    use core::num::traits::Zero;
+    use crate::governor::GovernorComponent::ComponentState as GovernorComponentState;
+    use crate::governor::GovernorComponent;
+    use crate::governor::extensions::interface::IVotesToken;
+    use crate::votes::interface::{IVotesDispatcher, IVotesDispatcherTrait};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use starknet::ContractAddress;
+    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_token: ContractAddress
+    }
+
+    pub mod Errors {
+        pub const INVALID_TOKEN: felt252 = 'Invalid votes token';
+    }
+
+    //
+    // Extensions
+    //
+
+    pub impl GovernorVotes<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorVotes: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorVotesTrait<TContractState> {
+        /// See `GovernorComponent::GovernorVotesTrait::clock`.
+        fn clock(self: @GovernorComponentState<TContractState>) -> u64 {
+            // VotesComponent uses the block timestamp for tracking checkpoints.
+            // That must be updated in order to allow for more flexible clock modes.
+            starknet::get_block_timestamp()
+        }
+
+        /// See `GovernorComponent::GovernorVotesTrait::CLOCK_MODE`.
+        fn clock_mode(self: @GovernorComponentState<TContractState>) -> ByteArray {
+            "mode=timestamp&from=starknet::SN_MAIN"
+        }
+
+        /// See `GovernorComponent::GovernorVotesTrait::get_votes`.
+        fn get_votes(
+            self: @GovernorComponentState<TContractState>,
+            account: ContractAddress,
+            timepoint: u64,
+            params: Span<felt252>
+        ) -> u256 {
+            let contract = self.get_contract();
+            let this_component = GovernorVotes::get_component(contract);
+
+            let token = this_component.Governor_token.read();
+            let votes_dispatcher = IVotesDispatcher { contract_address: token };
+
+            votes_dispatcher.get_past_votes(account, timepoint)
+        }
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(VotesTokenImpl)]
+    impl VotesToken<
+        TContractState, +HasComponent<TContractState>, +Drop<TContractState>
+    > of IVotesToken<ComponentState<TContractState>> {
+        /// Returns the token that voting power is sourced from.
+        fn token(self: @ComponentState<TContractState>) -> ContractAddress {
+            self.Governor_token.read()
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::HasComponent<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the component by setting the votes token.
+        ///
+        /// Requirements:
+        ///
+        /// - `votes_token` must not be zero.
+        fn initializer(ref self: ComponentState<TContractState>, votes_token: ContractAddress) {
+            assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN);
+            self.Governor_token.write(votes_token);
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo
new file mode 100644
index 000000000..2c91ecdcb
--- /dev/null
+++ b/packages/governance/src/governor/extensions/governor_votes_quorum_fraction.cairo
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0
+// (governance/governor/extensions/governor_votes_quorum_fraction.cairo)
+
+/// # GovernorVotesQuorumFraction Component
+///
+/// Extension of GovernorComponent for voting weight extraction from a token with the Votes
+/// extension and a quorum expressed as a fraction of the total supply.
+#[starknet::component]
+pub mod GovernorVotesQuorumFractionComponent {
+    use core::num::traits::Zero;
+    use crate::governor::GovernorComponent::ComponentState as GovernorComponentState;
+    use crate::governor::GovernorComponent;
+    use crate::governor::extensions::interface::IQuorumFraction;
+    use crate::votes::interface::{IVotesDispatcher, IVotesDispatcherTrait};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::structs::checkpoint::{Trace, TraceTrait};
+    use starknet::ContractAddress;
+    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_token: ContractAddress,
+        pub Governor_quorum_numerator_history: Trace,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    pub enum Event {
+        QuorumNumeratorUpdated: QuorumNumeratorUpdated
+    }
+
+    /// Emitted when the quorum numerator is updated.
+    #[derive(Drop, starknet::Event)]
+    pub struct QuorumNumeratorUpdated {
+        pub old_quorum_numerator: u256,
+        pub new_quorum_numerator: u256
+    }
+
+    pub mod Errors {
+        pub const INVALID_QUORUM_FRACTION: felt252 = 'Invalid quorum fraction';
+        pub const INVALID_TOKEN: felt252 = 'Invalid votes token';
+    }
+
+    //
+    // Extensions
+    //
+
+    pub impl GovernorQuorum<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorVotesQuorumFraction: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorQuorumTrait<TContractState> {
+        /// See `GovernorComponent::GovernorQuorumTrait::quorum`.
+        fn quorum(self: @GovernorComponentState<TContractState>, timepoint: u64) -> u256 {
+            let contract = self.get_contract();
+            let this_component = GovernorVotesQuorumFraction::get_component(contract);
+
+            let token = this_component.Governor_token.read();
+            let votes_dispatcher = IVotesDispatcher { contract_address: token };
+
+            let past_total_supply = votes_dispatcher.get_past_total_supply(timepoint);
+            let quorum_numerator = this_component.quorum_numerator(timepoint);
+            let quorum_denominator = this_component.quorum_denominator();
+
+            past_total_supply * quorum_numerator / quorum_denominator
+        }
+    }
+
+    pub impl GovernorVotes<
+        TContractState,
+        +GovernorComponent::HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        impl GovernorVotesQuorumFraction: HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of GovernorComponent::GovernorVotesTrait<TContractState> {
+        /// See `GovernorComponent::GovernorVotesTrait::clock`.
+        fn clock(self: @GovernorComponentState<TContractState>) -> u64 {
+            // VotesComponent uses the block timestamp for tracking checkpoints.
+            // That must be updated in order to allow for more flexible clock modes.
+            starknet::get_block_timestamp()
+        }
+
+        /// See `GovernorComponent::GovernorVotesTrait::CLOCK_MODE`.
+        fn clock_mode(self: @GovernorComponentState<TContractState>) -> ByteArray {
+            "mode=timestamp&from=starknet::SN_MAIN"
+        }
+
+        /// See `GovernorComponent::GovernorVotesTrait::get_votes`.
+        fn get_votes(
+            self: @GovernorComponentState<TContractState>,
+            account: ContractAddress,
+            timepoint: u64,
+            params: Span<felt252>
+        ) -> u256 {
+            let contract = self.get_contract();
+            let this_component = GovernorVotesQuorumFraction::get_component(contract);
+
+            let token = this_component.Governor_token.read();
+            let votes_dispatcher = IVotesDispatcher { contract_address: token };
+
+            votes_dispatcher.get_past_votes(account, timepoint)
+        }
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(QuorumFractionImpl)]
+    impl QuorumFraction<
+        TContractState, +HasComponent<TContractState>, +Drop<TContractState>
+    > of IQuorumFraction<ComponentState<TContractState>> {
+        /// Returns the token that voting power is sourced from.
+        fn token(self: @ComponentState<TContractState>) -> ContractAddress {
+            self.Governor_token.read()
+        }
+
+        /// Returns the current quorum numerator.
+        fn current_quorum_numerator(self: @ComponentState<TContractState>) -> u256 {
+            self.Governor_quorum_numerator_history.deref().latest()
+        }
+
+        /// Returns the quorum numerator at a specific timepoint.
+        fn quorum_numerator(self: @ComponentState<TContractState>, timepoint: u64) -> u256 {
+            // Optimistic search: check the latest checkpoint.
+            // The initializer call ensures that there is at least one checkpoint in the history.
+            //
+            // NOTE: This optimization is especially helpful when the supply is not updated often.
+            let history = self.Governor_quorum_numerator_history.deref();
+            let (_, key, value) = history.latest_checkpoint();
+
+            if key <= timepoint {
+                return value;
+            }
+
+            // Fallback to the binary search
+            history.upper_lookup(timepoint)
+        }
+
+        /// Returns the quorum denominator.
+        fn quorum_denominator(self: @ComponentState<TContractState>) -> u256 {
+            1000
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        +GovernorComponent::GovernorVotesTrait<TContractState>,
+        impl Governor: GovernorComponent::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the component by setting the votes token and the
+        /// initial quorum numerator value.
+        ///
+        /// Requirements:
+        ///
+        /// - `votes_token` must not be zero.
+        /// - `quorum_numerator` must be less than `quorum_denominator`.
+        ///
+        /// Emits a `QuorumNumeratorUpdated` event.
+        fn initializer(
+            ref self: ComponentState<TContractState>,
+            votes_token: ContractAddress,
+            quorum_numerator: u256
+        ) {
+            assert(votes_token.is_non_zero(), Errors::INVALID_TOKEN);
+
+            self.Governor_token.write(votes_token);
+            self.update_quorum_numerator(quorum_numerator);
+        }
+
+        /// Updates the quorum numerator.
+        ///
+        /// NOTE: This function does not emit an event if the new quorum numerator is the same as
+        /// the old one.
+        ///
+        /// Requirements:
+        ///
+        /// - `new_quorum_numerator` must be less than `quorum_denominator`.
+        ///
+        /// May emit a `QuorumNumeratorUpdated` event.
+        fn update_quorum_numerator(
+            ref self: ComponentState<TContractState>, new_quorum_numerator: u256
+        ) {
+            let denominator = self.quorum_denominator();
+
+            assert(new_quorum_numerator <= denominator, Errors::INVALID_QUORUM_FRACTION);
+
+            let old_quorum_numerator = self.current_quorum_numerator();
+
+            if old_quorum_numerator != new_quorum_numerator {
+                let governor_component = get_dep_component!(@self, Governor);
+
+                let clock = governor_component.clock();
+
+                self.Governor_quorum_numerator_history.deref().push(clock, new_quorum_numerator);
+
+                self.emit(QuorumNumeratorUpdated { old_quorum_numerator, new_quorum_numerator });
+            }
+        }
+    }
+}
diff --git a/packages/governance/src/governor/extensions/interface.cairo b/packages/governance/src/governor/extensions/interface.cairo
new file mode 100644
index 000000000..a8343a81e
--- /dev/null
+++ b/packages/governance/src/governor/extensions/interface.cairo
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/extensions/interface.cairo)
+
+use starknet::ContractAddress;
+
+#[starknet::interface]
+pub trait IQuorumFraction<TState> {
+    /// Returns the token that voting power is sourced from.
+    fn token(self: @TState) -> ContractAddress;
+
+    /// Returns the current quorum numerator.
+    fn current_quorum_numerator(self: @TState) -> u256;
+
+    /// Returns the quorum numerator at a specific timepoint.
+    fn quorum_numerator(self: @TState, timepoint: u64) -> u256;
+
+    /// Returns the quorum denominator.
+    fn quorum_denominator(self: @TState) -> u256;
+}
+
+#[starknet::interface]
+pub trait IVotesToken<TState> {
+    /// Returns the token that voting power is sourced from.
+    fn token(self: @TState) -> ContractAddress;
+}
+
+#[starknet::interface]
+pub trait ITimelocked<TState> {
+    /// Returns address of the associated timelock.
+    fn timelock(self: @TState) -> ContractAddress;
+
+    /// Returns the timelock proposal id for a given proposal id.
+    fn get_timelock_id(self: @TState, proposal_id: felt252) -> felt252;
+
+    /// Updates the associated timelock.
+    fn update_timelock(ref self: TState, new_timelock: ContractAddress);
+}
+
+#[starknet::interface]
+pub trait IGovernorSettingsAdmin<TState> {
+    /// Sets the voting delay.
+    fn set_voting_delay(ref self: TState, new_voting_delay: u64);
+
+    /// Sets the voting period.
+    fn set_voting_period(ref self: TState, new_voting_period: u64);
+
+    /// Sets the proposal threshold.
+    fn set_proposal_threshold(ref self: TState, new_proposal_threshold: u256);
+}
diff --git a/packages/governance/src/governor/governor.cairo b/packages/governance/src/governor/governor.cairo
new file mode 100644
index 000000000..9a750bbc2
--- /dev/null
+++ b/packages/governance/src/governor/governor.cairo
@@ -0,0 +1,1044 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/governor.cairo)
+
+/// # Governor Component
+///
+/// Core of the governance system.
+#[starknet::component]
+pub mod GovernorComponent {
+    use core::hash::{HashStateTrait, HashStateExTrait};
+    use core::num::traits::Zero;
+    use core::pedersen::PedersenTrait;
+    use crate::governor::ProposalCore;
+    use crate::governor::interface::{ProposalState, IGovernor, IGOVERNOR_ID};
+    use crate::governor::vote::{Vote, VoteWithReasonAndParams};
+    use crate::utils::call_impls::{HashCallImpl, HashCallsImpl};
+    use openzeppelin_account::interface::{ISRC6Dispatcher, ISRC6DispatcherTrait};
+    use openzeppelin_introspection::src5::SRC5Component::InternalImpl as SRC5InternalImpl;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::bytearray::ByteArrayExtTrait;
+    use openzeppelin_utils::cryptography::snip12::{OffchainMessageHash, SNIP12Metadata};
+    use starknet::account::Call;
+    use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess};
+    use starknet::{ContractAddress, SyscallResultTrait};
+
+    type ProposalId = felt252;
+
+    #[storage]
+    pub struct Storage {
+        pub Governor_proposals: Map<ProposalId, ProposalCore>,
+        pub Governor_nonces: Map<ContractAddress, felt252>
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    pub enum Event {
+        ProposalCreated: ProposalCreated,
+        ProposalQueued: ProposalQueued,
+        ProposalExecuted: ProposalExecuted,
+        ProposalCanceled: ProposalCanceled,
+        VoteCast: VoteCast,
+        VoteCastWithParams: VoteCastWithParams
+    }
+
+    /// Emitted when `call` is scheduled as part of operation `id`.
+    #[derive(Drop, starknet::Event)]
+    pub struct ProposalCreated {
+        #[key]
+        pub proposal_id: felt252,
+        #[key]
+        pub proposer: ContractAddress,
+        pub calls: Span<Call>,
+        pub signatures: Span<Span<felt252>>,
+        pub vote_start: u64,
+        pub vote_end: u64,
+        pub description: ByteArray
+    }
+
+    /// Emitted when a proposal is queued.
+    #[derive(Drop, starknet::Event)]
+    pub struct ProposalQueued {
+        #[key]
+        pub proposal_id: felt252,
+        pub eta_seconds: u64
+    }
+
+    /// Emitted when a proposal is executed.
+    #[derive(Drop, starknet::Event)]
+    pub struct ProposalExecuted {
+        #[key]
+        pub proposal_id: felt252
+    }
+
+    /// Emitted when a proposal is canceled.
+    #[derive(Drop, starknet::Event)]
+    pub struct ProposalCanceled {
+        #[key]
+        pub proposal_id: felt252
+    }
+
+    /// Emitted when a vote is cast without params.
+    #[derive(Drop, starknet::Event)]
+    pub struct VoteCast {
+        #[key]
+        pub voter: ContractAddress,
+        pub proposal_id: felt252,
+        pub support: u8,
+        pub weight: u256,
+        pub reason: ByteArray
+    }
+
+    /// Emitted when a vote is cast with params.
+    ///
+    /// NOTE: `support` values should be seen as buckets. Their interpretation depends on the voting
+    /// module used. `params` are additional encoded parameters. Their interpretation also
+    /// depends on the voting module used.
+    #[derive(Drop, starknet::Event)]
+    pub struct VoteCastWithParams {
+        #[key]
+        pub voter: ContractAddress,
+        pub proposal_id: felt252,
+        pub support: u8,
+        pub weight: u256,
+        pub reason: ByteArray,
+        pub params: Span<felt252>
+    }
+
+    pub mod Errors {
+        pub const EXECUTOR_ONLY: felt252 = 'Executor only';
+        pub const PROPOSER_ONLY: felt252 = 'Proposer only';
+        pub const NONEXISTENT_PROPOSAL: felt252 = 'Nonexistent proposal';
+        pub const EXISTENT_PROPOSAL: felt252 = 'Existent proposal';
+        pub const RESTRICTED_PROPOSER: felt252 = 'Restricted proposer';
+        pub const INSUFFICIENT_PROPOSER_VOTES: felt252 = 'Insufficient votes';
+        pub const UNEXPECTED_PROPOSAL_STATE: felt252 = 'Unexpected proposal state';
+        pub const QUEUE_NOT_IMPLEMENTED: felt252 = 'Queue not implemented';
+        pub const INVALID_SIGNATURE: felt252 = 'Invalid signature';
+    }
+
+    /// Constants expected to be defined at the contract level used to configure the component
+    /// behaviour.
+    ///
+    /// - `DEFAULT_PARAMS`: Default additional encoded parameters used by cast_vote
+    /// methods that don't include them.
+    pub trait ImmutableConfig {
+        /// Defined as a function since constant Span<felt252> is not supported.
+        fn DEFAULT_PARAMS() -> Span<felt252>;
+    }
+
+    //
+    // Extensions traits
+    //
+
+    pub trait GovernorSettingsTrait<TContractState> {
+        /// See `interface::IGovernor::voting_delay`.
+        fn voting_delay(self: @ComponentState<TContractState>) -> u64;
+
+        /// See `interface::IGovernor::voting_period`.
+        fn voting_period(self: @ComponentState<TContractState>) -> u64;
+
+        /// See `interface::IGovernor::proposal_threshold`.
+        fn proposal_threshold(self: @ComponentState<TContractState>) -> u256;
+    }
+
+    pub trait GovernorQuorumTrait<TContractState> {
+        /// See `interface::IGovernor::quorum`.
+        fn quorum(self: @ComponentState<TContractState>, timepoint: u64) -> u256;
+    }
+
+    pub trait GovernorCountingTrait<TContractState> {
+        /// See `interface::IGovernor::COUNTING_MODE`.
+        fn counting_mode(self: @ComponentState<TContractState>) -> ByteArray;
+
+        /// Register a vote for `proposal_id` by `account` with a given `support`,
+        /// voting `weight` and voting `params`.
+        ///
+        /// NOTE: Support is generic and can represent various things depending on the voting system
+        /// used.
+        fn count_vote(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            account: ContractAddress,
+            support: u8,
+            total_weight: u256,
+            params: Span<felt252>
+        ) -> u256;
+
+        /// See `interface::IGovernor::has_voted`.
+        fn has_voted(
+            self: @ComponentState<TContractState>, proposal_id: felt252, account: ContractAddress
+        ) -> bool;
+
+        /// Returns whether amount of votes already cast passes the threshold limit.
+        fn quorum_reached(self: @ComponentState<TContractState>, proposal_id: felt252) -> bool;
+
+        /// Returns whether the proposal is successful or not.
+        fn vote_succeeded(self: @ComponentState<TContractState>, proposal_id: felt252) -> bool;
+    }
+
+    pub trait GovernorVotesTrait<TContractState> {
+        /// See `interface::IERC6372::clock`.
+        fn clock(self: @ComponentState<TContractState>) -> u64;
+
+        /// See `interface::IERC6372::CLOCK_MODE`.
+        fn clock_mode(self: @ComponentState<TContractState>) -> ByteArray;
+
+        /// See `interface::IGovernor::get_votes`.
+        fn get_votes(
+            self: @ComponentState<TContractState>,
+            account: ContractAddress,
+            timepoint: u64,
+            params: Span<felt252>
+        ) -> u256;
+    }
+
+    pub trait GovernorExecutionTrait<TContractState> {
+        /// See `interface::IGovernor::state`.
+        fn state(self: @ComponentState<TContractState>, proposal_id: felt252) -> ProposalState;
+
+        /// Address through which the governor executes action.
+        /// Should be used to specify whether the module execute actions through another contract
+        /// such as a timelock.
+        ///
+        /// NOTE: MUST be the governor itself, or an instance of TimelockController with the
+        /// governor as the only proposer, canceller, and executor.
+        ///
+        /// WARNING: When the executor is not the governor itself (i.e. a timelock), it can call
+        /// functions that are restricted with the `assert_only_governance` guard, and also
+        /// potentially execute transactions on behalf of the governor. Because of this, this module
+        /// is designed to work with the TimelockController as the unique potential external
+        /// executor.
+        fn executor(self: @ComponentState<TContractState>) -> ContractAddress;
+
+        /// Execution mechanism. Can be used to modify the way execution is
+        /// performed (for example adding a vault/timelock).
+        fn execute_operations(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        );
+
+        /// Queuing mechanism. Can be used to modify the way queuing is
+        /// performed (for example adding a vault/timelock).
+        ///
+        /// Requirements:
+        ///
+        /// - Must return a timestamp that describes the expected ETA for execution. If the returned
+        /// value is 0, the core will consider queueing did not succeed, and the public `queue`
+        /// function will revert.
+        fn queue_operations(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            calls: Span<Call>,
+            description_hash: felt252
+        ) -> u64;
+
+        /// See `interface::IGovernor::proposal_needs_queuing`.
+        fn proposal_needs_queuing(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> bool;
+
+        /// Cancel mechanism. Can be used to modify the way canceling is
+        /// performed (for example adding a vault/timelock).
+        fn cancel_operations(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            description_hash: felt252
+        );
+    }
+
+    //
+    // External
+    //
+
+    #[embeddable_as(GovernorImpl)]
+    impl Governor<
+        TContractState,
+        +HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        +GovernorVotesTrait<TContractState>,
+        impl GovernorQuorum: GovernorQuorumTrait<TContractState>,
+        impl GovernorCounting: GovernorCountingTrait<TContractState>,
+        impl GovernorExecution: GovernorExecutionTrait<TContractState>,
+        impl GovernorSettings: GovernorSettingsTrait<TContractState>,
+        impl Metadata: SNIP12Metadata,
+        impl Immutable: ImmutableConfig,
+        +Drop<TContractState>,
+    > of IGovernor<ComponentState<TContractState>> {
+        /// Name of the governor instance (used in building the SNIP-12 domain separator).
+        fn name(self: @ComponentState<TContractState>) -> felt252 {
+            Metadata::name()
+        }
+
+        /// Version of the governor instance (used in building SNIP-12 domain separator).
+        fn version(self: @ComponentState<TContractState>) -> felt252 {
+            Metadata::version()
+        }
+
+        /// A description of the possible `support` values for `cast_vote` and the way these votes
+        /// are counted, meant to be consumed by UIs to show correct vote options and interpret the
+        /// results.
+        /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect,
+        /// for example `support=bravo&quorum=for,abstain`.
+        ///
+        /// There are 2 standard keys: `support` and `quorum`.
+        ///
+        /// - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in
+        /// `GovernorBravo`.
+        /// - `quorum=bravo` means that only For votes are counted towards quorum.
+        /// - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum.
+        ///
+        /// If a counting module makes use of encoded `params`, it should  include this under a
+        /// `params`
+        /// key with a unique name that describes the behavior. For example:
+        ///
+        /// - `params=fractional` might refer to a scheme where votes are divided fractionally
+        /// between for/against/abstain.
+        /// - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote.
+        ///
+        /// NOTE: The string can be decoded by the standard
+        /// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`]
+        /// JavaScript class.
+        fn COUNTING_MODE(self: @ComponentState<TContractState>) -> ByteArray {
+            self.counting_mode()
+        }
+
+        /// Hashing function used to (re)build the proposal id from the proposal details.
+        fn hash_proposal(
+            self: @ComponentState<TContractState>, calls: Span<Call>, description_hash: felt252
+        ) -> felt252 {
+            self._hash_proposal(calls, description_hash)
+        }
+
+        /// Returns the state of a proposal, given its id.
+        fn state(self: @ComponentState<TContractState>, proposal_id: felt252) -> ProposalState {
+            GovernorExecution::state(self, proposal_id)
+        }
+
+        /// The number of votes required in order for a voter to become a proposer.
+        fn proposal_threshold(self: @ComponentState<TContractState>) -> u256 {
+            self._proposal_threshold()
+        }
+
+        /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot
+        /// is performed at the end of this block. Hence, voting for this proposal starts at the
+        /// beginning of the following block.
+        fn proposal_snapshot(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            self._proposal_snapshot(proposal_id)
+        }
+
+        /// Timepoint at which votes close. If using block number, votes close at the end of this
+        /// block, so it is possible to cast a vote during this block.
+        fn proposal_deadline(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            self._proposal_deadline(proposal_id)
+        }
+
+        /// The account that created a proposal.
+        fn proposal_proposer(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> ContractAddress {
+            self._proposal_proposer(proposal_id)
+        }
+
+        /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot`
+        /// and `proposal_deadline`, this doesn't use the governor clock, and instead relies on the
+        /// executor's clock which may be different. In most cases this will be a timestamp.
+        fn proposal_eta(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            self._proposal_eta(proposal_id)
+        }
+
+        /// Whether a proposal needs to be queued before execution.
+        fn proposal_needs_queuing(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> bool {
+            GovernorExecution::proposal_needs_queuing(self, proposal_id)
+        }
+
+        /// Delay between the proposal is created and the vote starts. The unit this duration is
+        /// expressed in depends on the clock (see ERC-6372) this contract uses.
+        ///
+        /// This can be increased to leave time for users to buy voting power, or delegate it,
+        /// before the voting of a proposal starts.
+        fn voting_delay(self: @ComponentState<TContractState>) -> u64 {
+            GovernorSettings::voting_delay(self)
+        }
+
+        /// Delay between the vote start and vote end. The unit this duration is expressed in
+        /// depends on the clock (see ERC-6372) this contract uses.
+        ///
+        /// NOTE: The `voting_delay` can delay the start of the vote. This must be considered when
+        /// setting the voting duration compared to the voting delay.
+        ///
+        /// NOTE: This value is stored when the proposal is submitted so that possible changes to
+        /// the value do not affect proposals that have already been submitted.
+        fn voting_period(self: @ComponentState<TContractState>) -> u64 {
+            GovernorSettings::voting_period(self)
+        }
+
+        /// Minimum number of casted votes required for a proposal to be successful.
+        ///
+        /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting votes.
+        /// This allows the quorum to scale depending on values such as the total supply of a token
+        /// at this timepoint.
+        fn quorum(self: @ComponentState<TContractState>, timepoint: u64) -> u256 {
+            GovernorQuorum::quorum(self, timepoint)
+        }
+
+        /// Voting power of an `account` at a specific `timepoint`.
+        ///
+        /// NOTE: this can be implemented in a number of ways, for example by reading the delegated
+        /// balance from one (or multiple) `ERC20Votes` tokens.
+        fn get_votes(
+            self: @ComponentState<TContractState>, account: ContractAddress, timepoint: u64
+        ) -> u256 {
+            self._get_votes(account, timepoint, Immutable::DEFAULT_PARAMS())
+        }
+
+        /// Voting power of an `account` at a specific `timepoint` given additional encoded
+        /// parameters.
+        fn get_votes_with_params(
+            self: @ComponentState<TContractState>,
+            account: ContractAddress,
+            timepoint: u64,
+            params: Span<felt252>
+        ) -> u256 {
+            self._get_votes(account, timepoint, params)
+        }
+
+        /// Returns whether `account` has cast a vote on `proposal_id`.
+        fn has_voted(
+            self: @ComponentState<TContractState>, proposal_id: felt252, account: ContractAddress
+        ) -> bool {
+            GovernorCounting::has_voted(self, proposal_id, account)
+        }
+
+        /// Creates a new proposal. Voting starts after the delay specified by `voting_delay` and
+        /// lasts for a duration specified by `voting_period`. Returns the id of the proposal.
+        ///
+        /// This function has opt-in frontrunning protection, described in
+        /// `is_valid_description_for_proposer`.
+        ///
+        /// NOTE: The state of the Governor and targets may change between the proposal creation
+        /// and its execution. This may be the result of third party actions on the targeted
+        /// contracts, or other governor proposals. For example, the balance of this contract could
+        /// be updated or its access control permissions may be modified, possibly compromising the
+        /// proposal's ability to execute successfully (e.g. the governor doesn't have enough value
+        /// to cover a proposal with multiple transfers).
+        ///
+        /// Requirements:
+        ///
+        /// - The proposer must be authorized to submit the proposal.
+        /// - The proposer must have enough votes to submit the proposal if `proposal_threshold` is
+        /// greater than zero.
+        /// - The proposal must not already exist.
+        ///
+        /// Emits a `ProposalCreated` event.
+        fn propose(
+            ref self: ComponentState<TContractState>, calls: Span<Call>, description: ByteArray
+        ) -> felt252 {
+            let proposer = starknet::get_caller_address();
+
+            // Check description for restricted proposer
+            assert(
+                self.is_valid_description_for_proposer(proposer, @description),
+                Errors::RESTRICTED_PROPOSER
+            );
+
+            // Check proposal threshold
+            let vote_threshold = self._proposal_threshold();
+            if vote_threshold > 0 {
+                let votes = self
+                    ._get_votes(proposer, self.clock() - 1, Immutable::DEFAULT_PARAMS());
+                assert(votes >= vote_threshold, Errors::INSUFFICIENT_PROPOSER_VOTES);
+            }
+
+            self._propose(calls, @description, proposer)
+        }
+
+        /// Queues a proposal. Some governors require this step to be performed before execution can
+        /// happen. If queuing is not necessary, this function may revert.
+        /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the
+        /// deadline to be reached.
+        ///
+        /// Returns the id of the proposal.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be in the `Succeeded` state.
+        /// - The queue operation must return a non-zero ETA.
+        ///
+        /// Emits a `ProposalQueued` event.
+        fn queue(
+            ref self: ComponentState<TContractState>, calls: Span<Call>, description_hash: felt252
+        ) -> felt252 {
+            let proposal_id = self._hash_proposal(calls, description_hash);
+            self.validate_state(proposal_id, array![ProposalState::Succeeded].span());
+
+            let eta_seconds = self.queue_operations(proposal_id, calls, description_hash);
+            assert(eta_seconds > 0, Errors::QUEUE_NOT_IMPLEMENTED);
+
+            let mut proposal = self.Governor_proposals.read(proposal_id);
+            proposal.eta_seconds = eta_seconds;
+            self.Governor_proposals.write(proposal_id, proposal);
+
+            self.emit(ProposalQueued { proposal_id, eta_seconds });
+
+            proposal_id
+        }
+
+        /// Executes a successful proposal. This requires the quorum to be reached, the vote to be
+        /// successful, and the deadline to be reached. Depending on the governor it might also be
+        /// required that the proposal was queued and that some delay passed.
+        ///
+        /// NOTE: Some modules can modify the requirements for execution, for example by adding an
+        /// additional timelock (See `timelock_controller`).
+        ///
+        /// Returns the id of the proposal.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be in the `Succeeded` or `Queued` state.
+        ///
+        /// Emits a `ProposalExecuted` event.
+        fn execute(
+            ref self: ComponentState<TContractState>, calls: Span<Call>, description_hash: felt252
+        ) -> felt252 {
+            let proposal_id = self._hash_proposal(calls, description_hash);
+            self
+                .validate_state(
+                    proposal_id, array![ProposalState::Succeeded, ProposalState::Queued].span()
+                );
+
+            // Mark proposal as executed to avoid reentrancy
+            let mut proposal = self.Governor_proposals.read(proposal_id);
+            proposal.executed = true;
+            self.Governor_proposals.write(proposal_id, proposal);
+
+            self.execute_operations(proposal_id, calls, description_hash);
+
+            self.emit(ProposalExecuted { proposal_id });
+
+            proposal_id
+        }
+
+        /// Cancels a proposal. A proposal is cancellable by the proposer, but only while it is
+        /// Pending state, i.e. before the vote starts.
+        ///
+        /// Returns the id of the proposal.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be in the `Pending` state.
+        ///
+        /// Emits a `ProposalCanceled` event.
+        fn cancel(
+            ref self: ComponentState<TContractState>, calls: Span<Call>, description_hash: felt252
+        ) -> felt252 {
+            let proposal_id = self._hash_proposal(calls, description_hash);
+            self.validate_state(proposal_id, array![ProposalState::Pending].span());
+
+            assert(
+                starknet::get_caller_address() == self.proposal_proposer(proposal_id),
+                Errors::PROPOSER_ONLY
+            );
+            self.cancel_operations(proposal_id, description_hash);
+
+            self.emit(ProposalCanceled { proposal_id });
+
+            proposal_id
+        }
+
+        /// Cast a vote.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be active.
+        ///
+        /// Emits a `VoteCast` event.
+        fn cast_vote(
+            ref self: ComponentState<TContractState>, proposal_id: felt252, support: u8
+        ) -> u256 {
+            let voter = starknet::get_caller_address();
+            self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS())
+        }
+
+        /// Cast a vote with a `reason`.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be active.
+        ///
+        /// Emits a `VoteCast` event.
+        fn cast_vote_with_reason(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            support: u8,
+            reason: ByteArray
+        ) -> u256 {
+            let voter = starknet::get_caller_address();
+            self._cast_vote(proposal_id, voter, support, reason, Immutable::DEFAULT_PARAMS())
+        }
+
+        /// Cast a vote with a `reason` and additional serialized `params`.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be active.
+        ///
+        /// Emits either:
+        /// - `VoteCast` event if no params are provided.
+        /// - `VoteCastWithParams` event otherwise.
+        fn cast_vote_with_reason_and_params(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            support: u8,
+            reason: ByteArray,
+            params: Span<felt252>
+        ) -> u256 {
+            let voter = starknet::get_caller_address();
+            self._cast_vote(proposal_id, voter, support, reason, params)
+        }
+
+        /// Cast a vote using the `voter`'s signature.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be active.
+        /// - The nonce in the signed message must match the account's current nonce.
+        /// - `voter` must implement `SRC6::is_valid_signature`.
+        /// - `signature` should be valid for the message hash.
+        ///
+        /// Emits a `VoteCast` event.
+        fn cast_vote_by_sig(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            support: u8,
+            voter: ContractAddress,
+            signature: Span<felt252>
+        ) -> u256 {
+            // 1. Get and increase current nonce
+            let nonce = self.use_nonce(voter);
+
+            // 2. Build hash for calling `is_valid_signature`
+            let verifying_contract = starknet::get_contract_address();
+            let vote = Vote { verifying_contract, nonce, proposal_id, support, voter };
+            let hash = vote.get_message_hash(voter);
+
+            let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter }
+                .is_valid_signature(hash, signature.into());
+
+            // 3. 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);
+
+            // 4. Cast vote
+            self._cast_vote(proposal_id, voter, support, "", Immutable::DEFAULT_PARAMS())
+        }
+
+        /// Cast a vote with a `reason` and additional serialized `params` using the `voter`'s
+        /// signature.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must be active.
+        /// - The nonce in the signed message must match the account's current nonce.
+        /// - `voter` must implement `SRC6::is_valid_signature`.
+        /// - `signature` should be valid for the message hash.
+        ///
+        /// Emits either:
+        /// - `VoteCast` event if no params are provided.
+        /// - `VoteCastWithParams` event otherwise.
+        fn cast_vote_with_reason_and_params_by_sig(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            support: u8,
+            voter: ContractAddress,
+            reason: ByteArray,
+            params: Span<felt252>,
+            signature: Span<felt252>
+        ) -> u256 {
+            // 1. Get and increase current nonce
+            let nonce = self.use_nonce(voter);
+
+            // 2. Build hash for calling `is_valid_signature`
+            let verifying_contract = starknet::get_contract_address();
+            let reason_hash = reason.hash();
+            let vote = VoteWithReasonAndParams {
+                verifying_contract, nonce, proposal_id, support, voter, reason_hash, params
+            };
+            let hash = vote.get_message_hash(voter);
+
+            let is_valid_signature_felt = ISRC6Dispatcher { contract_address: voter }
+                .is_valid_signature(hash, signature.into());
+
+            // 3. 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);
+
+            // 4. Cast vote
+            self._cast_vote(proposal_id, voter, support, reason, params)
+        }
+
+        /// Returns the next unused nonce for an address.
+        fn nonces(self: @ComponentState<TContractState>, voter: ContractAddress) -> felt252 {
+            self.Governor_nonces.read(voter)
+        }
+
+        /// Relays a transaction or function call to an arbitrary target.
+        ///
+        /// In cases where the governance executor is some contract other than the governor itself,
+        /// like when using a timelock, this function can be invoked in a governance proposal to
+        /// recover tokens that was sent to the governor contract by mistake.
+        ///
+        /// NOTE: If the executor is simply the governor itself, use of `relay` is redundant.
+        fn relay(ref self: ComponentState<TContractState>, call: Call) {
+            self.assert_only_governance();
+
+            let Call { to, selector, calldata } = call;
+            starknet::syscalls::call_contract_syscall(to, selector, calldata).unwrap_syscall();
+        }
+    }
+
+    //
+    // Internal
+    //
+
+    #[generate_trait]
+    pub impl InternalImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        impl SRC5: SRC5Component::HasComponent<TContractState>,
+        +Drop<TContractState>
+    > of InternalTrait<TContractState> {
+        /// Initializes the contract by registering the supported interface id.
+        fn initializer(ref self: ComponentState<TContractState>) {
+            let mut src5_component = get_dep_component_mut!(ref self, SRC5);
+            src5_component.register_interface(IGOVERNOR_ID);
+        }
+
+        /// Returns the proposal object given its id.
+        fn get_proposal(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> ProposalCore {
+            self.Governor_proposals.read(proposal_id)
+        }
+
+        /// Checks if the proposer is authorized to submit a proposal with the given description.
+        ///
+        /// If the proposal description ends with `#proposer=0x???`, where `0x???` is an address
+        /// written as a hex string (case insensitive), then the submission of this proposal will
+        /// only be authorized to said address.
+        ///
+        /// This is used for frontrunning protection. By adding this pattern at the end of their
+        /// proposal, one can ensure that no other address can submit the same proposal. An attacker
+        /// would have to either remove or change that part, which would result in a different
+        /// proposal id.
+        ///
+        /// NOTE: In Starknet, the Sequencer ensures the order of transactions, but frontrunning
+        /// can still be achieved by nodes, and potentially other actors in the future with
+        /// sequencer decentralization.
+        ///
+        /// If the description does not match this pattern, it is unrestricted and anyone can submit
+        /// it. This includes:
+        /// - If the `0x???` part is not a valid hex string.
+        /// - If the `0x???` part is a valid hex string, but does not contain exactly 64 hex digits.
+        /// - If it ends with the expected suffix followed by newlines or other whitespace.
+        /// - If it ends with some other similar suffix, e.g. `#other=abc`.
+        /// - If it does not end with any such suffix.
+        fn is_valid_description_for_proposer(
+            self: @ComponentState<TContractState>,
+            proposer: ContractAddress,
+            description: @ByteArray
+        ) -> bool {
+            let length = description.len();
+
+            // Length is too short to contain a valid proposer suffix
+            if description.len() < 76 {
+                return true;
+            }
+
+            // Extract what would be the `#proposer=` marker beginning the suffix
+            let marker = description.read_n_bytes(length - 76, 10);
+
+            // If the marker is not found, there is no proposer suffix to check
+            if marker != "#proposer=" {
+                return true;
+            }
+
+            let expected_address = description.read_n_bytes(length - 64, 64);
+
+            proposer.to_byte_array(16, 64) == expected_address
+        }
+
+        /// Returns a hash of the proposal using the Pedersen hashing algorithm.
+        fn _hash_proposal(
+            self: @ComponentState<TContractState>, calls: Span<Call>, description_hash: felt252
+        ) -> felt252 {
+            PedersenTrait::new(0).update_with(calls).update_with(description_hash).finalize()
+        }
+
+        /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot
+        /// is performed at the end of this block. Hence, voting for this proposal starts at the
+        /// beginning of the following block.
+        fn _proposal_snapshot(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            self.Governor_proposals.read(proposal_id).vote_start
+        }
+
+        /// Timepoint at which votes close. If using block number, votes close at the end of this
+        /// block, so it is possible to cast a vote during this block.
+        fn _proposal_deadline(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            let proposal = self.Governor_proposals.read(proposal_id);
+            proposal.vote_start + proposal.vote_duration
+        }
+
+        /// The account that created a proposal.
+        fn _proposal_proposer(
+            self: @ComponentState<TContractState>, proposal_id: felt252
+        ) -> ContractAddress {
+            self.Governor_proposals.read(proposal_id).proposer
+        }
+
+        /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot`
+        /// and `proposal_deadline`, this doesn't use the governor clock, and instead relies on the
+        /// executor's clock which may be different. In most cases this will be a timestamp.
+        fn _proposal_eta(self: @ComponentState<TContractState>, proposal_id: felt252) -> u64 {
+            self.Governor_proposals.read(proposal_id).eta_seconds
+        }
+    }
+
+    #[generate_trait]
+    pub impl InternalExtendedImpl<
+        TContractState,
+        +HasComponent<TContractState>,
+        +SRC5Component::HasComponent<TContractState>,
+        +GovernorCountingTrait<TContractState>,
+        +GovernorExecutionTrait<TContractState>,
+        impl GovernorSettings: GovernorSettingsTrait<TContractState>,
+        impl GovernorVotes: GovernorVotesTrait<TContractState>,
+        +Drop<TContractState>
+    > of InternalExtendedTrait<TContractState> {
+        /// Asserts that the caller is the governance executor.
+        ///
+        /// WARNING: When the executor is not the governor itself (i.e. a timelock), it can call
+        /// functions that are restricted with this modifier, and also potentially execute
+        /// transactions on behalf of the governor. Because of this, this module is designed to work
+        /// with the TimelockController as the unique potential external executor. The timelock
+        /// MUST have the governor as the only proposer, canceller, and executor.
+        fn assert_only_governance(self: @ComponentState<TContractState>) {
+            let executor = self.executor();
+            assert(executor == starknet::get_caller_address(), Errors::EXECUTOR_ONLY);
+        }
+
+        /// Validates that the proposal is in one of the expected states.
+        fn validate_state(
+            self: @ComponentState<TContractState>,
+            proposal_id: felt252,
+            allowed_states: Span<ProposalState>
+        ) {
+            let current_state = self.state(proposal_id);
+            let mut found = false;
+            for state in allowed_states {
+                if current_state == *state {
+                    found = true;
+                    break;
+                }
+            };
+            assert(found, Errors::UNEXPECTED_PROPOSAL_STATE);
+        }
+
+        /// Consumes a nonce, returns the current value, and increments nonce.
+        fn use_nonce(ref self: ComponentState<TContractState>, voter: ContractAddress) -> felt252 {
+            // For each account, the nonce has an initial value of 0, can only be incremented by
+            // one, and cannot be decremented or reset. This guarantees that the nonce never
+            // overflows.
+            let nonce = self.Governor_nonces.read(voter);
+            self.Governor_nonces.write(voter, nonce + 1);
+            nonce
+        }
+
+        /// Internal wrapper for `GovernorVotesTrait::get_votes`.
+        fn _get_votes(
+            self: @ComponentState<TContractState>,
+            account: ContractAddress,
+            timepoint: u64,
+            params: Span<felt252>
+        ) -> u256 {
+            GovernorVotes::get_votes(self, account, timepoint, params)
+        }
+
+        /// Internal wrapper for `GovernorProposeTrait::proposal_threshold`.
+        fn _proposal_threshold(self: @ComponentState<TContractState>) -> u256 {
+            GovernorSettings::proposal_threshold(self)
+        }
+
+        /// Returns the state of a proposal, given its id.
+        fn _state(self: @ComponentState<TContractState>, proposal_id: felt252) -> ProposalState {
+            let proposal = self.get_proposal(proposal_id);
+
+            if proposal.executed {
+                return ProposalState::Executed;
+            }
+            if proposal.canceled {
+                return ProposalState::Canceled;
+            }
+
+            let snapshot = self._proposal_snapshot(proposal_id);
+            assert(snapshot.is_non_zero(), Errors::NONEXISTENT_PROPOSAL);
+
+            let current_timepoint = self.clock();
+            if current_timepoint < snapshot {
+                return ProposalState::Pending;
+            }
+
+            let deadline = self._proposal_deadline(proposal_id);
+
+            if current_timepoint <= deadline {
+                return ProposalState::Active;
+            } else if !self.quorum_reached(proposal_id) || !self.vote_succeeded(proposal_id) {
+                return ProposalState::Defeated;
+            } else if self._proposal_eta(proposal_id).is_zero() {
+                return ProposalState::Succeeded;
+            } else {
+                return ProposalState::Queued;
+            }
+        }
+
+        /// Internal propose mechanism. Returns the proposal id.
+        ///
+        /// Requirements:
+        ///
+        /// - The proposal must not already exist.
+        ///
+        /// Emits a `ProposalCreated` event.
+        fn _propose(
+            ref self: ComponentState<TContractState>,
+            calls: Span<Call>,
+            description: @ByteArray,
+            proposer: ContractAddress
+        ) -> felt252 {
+            let proposal_id = self._hash_proposal(calls, description.hash());
+
+            assert(
+                self.Governor_proposals.read(proposal_id).vote_start == 0, Errors::EXISTENT_PROPOSAL
+            );
+
+            let snapshot = self.clock() + self.voting_delay();
+            let duration = self.voting_period();
+
+            let proposal = ProposalCore {
+                proposer,
+                vote_start: snapshot,
+                vote_duration: duration,
+                executed: false,
+                canceled: false,
+                eta_seconds: 0
+            };
+
+            self.Governor_proposals.write(proposal_id, proposal);
+            self
+                .emit(
+                    ProposalCreated {
+                        proposal_id,
+                        proposer,
+                        calls,
+                        signatures: array![].span(),
+                        vote_start: snapshot,
+                        vote_end: snapshot + duration,
+                        description: description.clone()
+                    }
+                );
+
+            proposal_id
+        }
+
+        /// Internal cancel mechanism with minimal restrictions.
+        /// A proposal can be cancelled in any state other than Canceled or Executed.
+        ///
+        /// NOTE: Once cancelled a proposal can't be re-submitted.
+        fn _cancel(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            description_hash: felt252
+        ) {
+            let valid_states = array![
+                ProposalState::Pending,
+                ProposalState::Active,
+                ProposalState::Defeated,
+                ProposalState::Succeeded,
+                ProposalState::Queued
+            ];
+            self.validate_state(proposal_id, valid_states.span());
+
+            let mut proposal = self.Governor_proposals.read(proposal_id);
+            proposal.canceled = true;
+            self.Governor_proposals.write(proposal_id, proposal);
+        }
+
+        /// Internal wrapper for `GovernorCountingTrait::count_vote`.
+        fn _count_vote(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            account: ContractAddress,
+            support: u8,
+            total_weight: u256,
+            params: Span<felt252>
+        ) -> u256 {
+            self.count_vote(proposal_id, account, support, total_weight, params)
+        }
+
+        /// Internal vote casting mechanism.
+        ///
+        /// Checks that the vote is pending, that it has not been cast yet, retrieve
+        /// voting weight using `get_votes` and call the `_count_vote` internal function.
+        ///
+        /// Emits either:
+        /// - `VoteCast` event if no params are provided.
+        /// - `VoteCastWithParams` event otherwise.
+        fn _cast_vote(
+            ref self: ComponentState<TContractState>,
+            proposal_id: felt252,
+            voter: ContractAddress,
+            support: u8,
+            reason: ByteArray,
+            params: Span<felt252>
+        ) -> u256 {
+            self.validate_state(proposal_id, array![ProposalState::Active].span());
+
+            let snapshot = self._proposal_snapshot(proposal_id);
+            let total_weight = self._get_votes(voter, snapshot, params);
+            let voted_weight = self._count_vote(proposal_id, voter, support, total_weight, params);
+
+            if params.len().is_zero() {
+                self.emit(VoteCast { voter, proposal_id, support, weight: voted_weight, reason });
+            } else {
+                self
+                    .emit(
+                        VoteCastWithParams {
+                            voter, proposal_id, support, weight: voted_weight, reason, params
+                        }
+                    );
+            }
+
+            // TODO: add tally hook when the PreventLateQuorum extension gets added
+
+            voted_weight
+        }
+    }
+}
+
+/// Implementation of the default Governor ImmutableConfig.
+///
+/// See
+/// https://github.com/starknet-io/SNIPs/blob/963848f0752bde75c7087c2446d83b7da8118b25/SNIPS/snip-107.md#defaultconfig-implementation
+///
+/// The `DEFAULT_PARAMS` is set to an empty span of felts.
+pub impl DefaultConfig of GovernorComponent::ImmutableConfig {
+    fn DEFAULT_PARAMS() -> Span<felt252> {
+        array![].span()
+    }
+}
diff --git a/packages/governance/src/governor/interface.cairo b/packages/governance/src/governor/interface.cairo
new file mode 100644
index 000000000..317309876
--- /dev/null
+++ b/packages/governance/src/governor/interface.cairo
@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/interface.cairo)
+
+use starknet::ContractAddress;
+use starknet::account::Call;
+
+pub const IGOVERNOR_ID: felt252 = 0x1f; // TODO: Update this value.
+
+/// Interface for a contract that implements the ERC-6372 standard.
+#[starknet::interface]
+pub trait IERC6372<TState> {
+    /// Clock used for flagging checkpoints.
+    /// Can be overridden to implement timestamp based checkpoints (and voting).
+    fn clock(self: @TState) -> u64;
+
+    /// Description of the clock.
+    /// See https://eips.ethereum.org/EIPS/eip-6372#clock_mode
+    fn CLOCK_MODE(self: @TState) -> ByteArray;
+}
+
+#[derive(Copy, PartialEq, Drop, Serde, Debug)]
+pub enum ProposalState {
+    Pending,
+    Active,
+    Canceled,
+    Defeated,
+    Succeeded,
+    Queued,
+    Executed
+}
+
+#[starknet::interface]
+pub trait IGovernor<TState> {
+    /// Name of the governor instance (used in building the SNIP-12 domain separator).
+    fn name(self: @TState) -> felt252;
+
+    /// Version of the governor instance (used in building SNIP-12 domain separator).
+    fn version(self: @TState) -> felt252;
+
+    /// A description of the possible `support` values for `cast_vote` and the way these votes are
+    /// counted, meant to be consumed by UIs to show correct vote options and interpret the results.
+    /// The string is a URL-encoded sequence of key-value pairs that each describe one aspect, for
+    /// example `support=bravo&quorum=for,abstain`.
+    ///
+    /// There are 2 standard keys: `support` and `quorum`.
+    ///
+    /// - `support=bravo` refers to the vote options 0 = Against, 1 = For, 2 = Abstain, as in
+    /// `GovernorBravo`.
+    /// - `quorum=bravo` means that only For votes are counted towards quorum.
+    /// - `quorum=for,abstain` means that both For and Abstain votes are counted towards quorum.
+    ///
+    /// If a counting module makes use of encoded `params`, it should  include this under a `params`
+    /// key with a unique name that describes the behavior. For example:
+    ///
+    /// - `params=fractional` might refer to a scheme where votes are divided fractionally between
+    /// for/against/abstain.
+    /// - `params=erc721` might refer to a scheme where specific NFTs are delegated to vote.
+    ///
+    /// NOTE: The string can be decoded by the standard
+    /// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams[`URLSearchParams`]
+    /// JavaScript class.
+    fn COUNTING_MODE(self: @TState) -> ByteArray;
+
+    /// Hashing function used to (re)build the proposal id from the proposal details.
+    fn hash_proposal(self: @TState, calls: Span<Call>, description_hash: felt252) -> felt252;
+
+    /// Returns the state of a proposal, given its id.
+    fn state(self: @TState, proposal_id: felt252) -> ProposalState;
+
+    /// The number of votes required in order for a voter to become a proposer.
+    fn proposal_threshold(self: @TState) -> u256;
+
+    /// Timepoint used to retrieve user's votes and quorum. If using block number, the snapshot is
+    /// performed at the end of this block. Hence, voting for this proposal starts at the beginning
+    /// of the following block.
+    fn proposal_snapshot(self: @TState, proposal_id: felt252) -> u64;
+
+    /// Timepoint at which votes close. If using block number, votes close at the end of this block,
+    /// so it is possible to cast a vote during this block.
+    fn proposal_deadline(self: @TState, proposal_id: felt252) -> u64;
+
+    /// The account that created a proposal.
+    fn proposal_proposer(self: @TState, proposal_id: felt252) -> ContractAddress;
+
+    /// The time when a queued proposal becomes executable ("ETA"). Unlike `proposal_snapshot` and
+    /// `proposal_deadline`, this doesn't use the governor clock, and instead relies on the
+    /// executor's clock which may be different. In most cases this will be a timestamp.
+    fn proposal_eta(self: @TState, proposal_id: felt252) -> u64;
+
+    /// Whether a proposal needs to be queued before execution.
+    fn proposal_needs_queuing(self: @TState, proposal_id: felt252) -> bool;
+
+    /// Delay between the proposal is created and the vote starts. The unit this duration is
+    /// expressed in depends on the clock (see ERC-6372) this contract uses.
+    ///
+    /// This can be increased to leave time for users to buy voting power, or delegate it, before
+    /// the voting of a proposal starts.
+    fn voting_delay(self: @TState) -> u64;
+
+    /// Delay between the vote start and vote end. The unit this duration is expressed in depends on
+    /// the clock (see ERC-6372) this contract uses.
+    ///
+    /// NOTE: The `voting_delay` can delay the start of the vote. This must be considered when
+    /// setting the voting duration compared to the voting delay.
+    ///
+    /// NOTE: This value is stored when the proposal is submitted so that possible changes to the
+    /// value do not affect proposals that have already been submitted.
+    fn voting_period(self: @TState) -> u64;
+
+    /// Minimum number of cast voted required for a proposal to be successful.
+    ///
+    /// NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This
+    /// allows the quorum to scale depending on values such as the total supply of a token at this
+    /// timepoint.
+    fn quorum(self: @TState, timepoint: u64) -> u256;
+
+    /// Voting power of an `account` at a specific `timepoint`.
+    ///
+    /// NOTE: this can be implemented in a number of ways, for example by reading the delegated
+    /// balance from one (or multiple) `ERC20Votes` tokens.
+    fn get_votes(self: @TState, account: ContractAddress, timepoint: u64) -> u256;
+
+    /// Voting power of an `account` at a specific `timepoint` given additional encoded parameters.
+    fn get_votes_with_params(
+        self: @TState, account: ContractAddress, timepoint: u64, params: Span<felt252>
+    ) -> u256;
+
+    /// Returns whether `account` has cast a vote on `proposal_id`.
+    fn has_voted(self: @TState, proposal_id: felt252, account: ContractAddress) -> bool;
+
+    /// Creates a new proposal. Vote start after a delay specified by `voting_delay` and
+    /// lasts for a duration specified by `voting_period`.
+    ///
+    /// NOTE: The state of the Governor and targets may change between the proposal creation and
+    /// its execution. This may be the result of third party actions on the targeted contracts, or
+    /// other governor proposals. For example, the balance of this contract could be updated or its
+    /// access control permissions may be modified, possibly compromising the proposal's ability to
+    /// execute successfully (e.g. the governor doesn't have enough value to cover a proposal with
+    /// multiple transfers).
+    ///
+    /// Returns the id of the proposal.
+    fn propose(ref self: TState, calls: Span<Call>, description: ByteArray) -> felt252;
+
+    /// Queue a proposal. Some governors require this step to be performed before execution can
+    /// happen. If queuing is not necessary, this function may revert.
+    /// Queuing a proposal requires the quorum to be reached, the vote to be successful, and the
+    /// deadline to be reached.
+    ///
+    /// Returns the id of the proposal.
+    fn queue(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
+
+    /// Execute a successful proposal. This requires the quorum to be reached, the vote to be
+    /// successful, and the deadline to be reached. Depending on the governor it might also be
+    /// required that the proposal was queued and that some delay passed.
+    ///
+    /// NOTE: Some modules can modify the requirements for execution, for example by adding an
+    /// additional timelock (See `timelock_controller`).
+    ///
+    /// Returns the id of the proposal.
+    fn execute(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
+
+    /// Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending
+    /// state, i.e. before the vote starts.
+    ///
+    /// Returns the id of the proposal.
+    fn cancel(ref self: TState, calls: Span<Call>, description_hash: felt252) -> felt252;
+
+    /// Cast a vote.
+    fn cast_vote(ref self: TState, proposal_id: felt252, support: u8) -> u256;
+
+    /// Cast a vote with a `reason`.
+    fn cast_vote_with_reason(
+        ref self: TState, proposal_id: felt252, support: u8, reason: ByteArray
+    ) -> u256;
+
+    /// Cast a vote with a `reason` and additional serialized `params`.
+    fn cast_vote_with_reason_and_params(
+        ref self: TState,
+        proposal_id: felt252,
+        support: u8,
+        reason: ByteArray,
+        params: Span<felt252>
+    ) -> u256;
+
+    /// Cast a vote using the `voter`'s signature.
+    fn cast_vote_by_sig(
+        ref self: TState,
+        proposal_id: felt252,
+        support: u8,
+        voter: ContractAddress,
+        signature: Span<felt252>
+    ) -> u256;
+
+    /// Cast a vote with a `reason` and additional serialized `params` using the `voter`'s
+    /// signature.
+    fn cast_vote_with_reason_and_params_by_sig(
+        ref self: TState,
+        proposal_id: felt252,
+        support: u8,
+        voter: ContractAddress,
+        reason: ByteArray,
+        params: Span<felt252>,
+        signature: Span<felt252>
+    ) -> u256;
+
+    /// Returns the next unused nonce for an address.
+    fn nonces(self: @TState, voter: ContractAddress) -> felt252;
+
+    /// Relays a transaction or function call to an arbitrary target.
+    ///
+    /// In cases where the governance executor is some contract other than the governor itself, like
+    /// when using a timelock, this function can be invoked in a governance proposal to recover
+    /// tokens that were sent to the governor contract by mistake.
+    ///
+    /// NOTE: If the executor is simply the governor itself, use of `relay` is redundant.
+    fn relay(ref self: TState, call: Call);
+}
diff --git a/packages/governance/src/governor/proposal_core.cairo b/packages/governance/src/governor/proposal_core.cairo
new file mode 100644
index 000000000..748ce0e91
--- /dev/null
+++ b/packages/governance/src/governor/proposal_core.cairo
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/proposal_core.cairo)
+
+use core::traits::DivRem;
+use starknet::ContractAddress;
+use starknet::storage_access::StorePacking;
+
+/// Proposal state.
+#[derive(Copy, Drop, Serde, PartialEq, Debug)]
+pub struct ProposalCore {
+    pub proposer: ContractAddress,
+    pub vote_start: u64,
+    pub vote_duration: u64,
+    pub executed: bool,
+    pub canceled: bool,
+    pub eta_seconds: u64
+}
+
+const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000;
+const _2_POW_120: felt252 = 0x1000000000000000000000000000000;
+const _2_POW_56: felt252 = 0x100000000000000;
+const _2_POW_55: felt252 = 0x80000000000000;
+const _2_POW_54: felt252 = 0x40000000000000;
+
+/// Packs a ProposalCore into a (felt252, felt252).
+///
+/// The packing is done as follows:
+///
+/// 1. The first felt of the tuple contains `proposer` serialized.
+/// 2. The second felt of the tuple contains `vote_start`, `vote_duration`, `eta_seconds`,
+/// `executed`, and `canceled` organized as:
+///   - `vote_start` is stored at range [4,67] bits (0-indexed), taking the most significant usable
+/// bits.
+///   - `vote_duration` is stored at range [68, 131], following `vote_start`.
+///   - `eta_seconds` is stored at range [132, 195], following `vote_duration`.
+///   - `executed` is stored at range [133, 133], following `eta_seconds`.
+///   - `canceled` is stored at range [134, 134], following `executed`.
+///
+/// NOTE: In the second felt252, the first four bits are skipped to avoid representation errors due
+/// to `felt252` max value being a bit less than a 252 bits number max value
+/// (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/).
+impl ProposalCoreStorePacking of StorePacking<ProposalCore, (felt252, felt252)> {
+    fn pack(value: ProposalCore) -> (felt252, felt252) {
+        let proposal = value;
+
+        // shift-left to reach the corresponding positions
+        let vote_start = proposal.vote_start.into() * _2_POW_184;
+        let vote_duration = proposal.vote_duration.into() * _2_POW_120;
+        let eta_seconds = proposal.eta_seconds.into() * _2_POW_56;
+        let executed = proposal.executed.into() * _2_POW_55;
+        let canceled = proposal.canceled.into() * _2_POW_54;
+
+        let second_felt = vote_start + vote_duration + eta_seconds + executed + canceled;
+
+        (proposal.proposer.into(), second_felt)
+    }
+
+    fn unpack(value: (felt252, felt252)) -> ProposalCore {
+        let (proposer, second_felt) = value;
+        let _2_POW_64: NonZero<u256> = 0x10000000000000000;
+
+        // shift-right to extract the corresponding values
+        let val: u256 = second_felt.into() / _2_POW_54.into();
+
+        let (val, canceled) = DivRem::div_rem(val, 2);
+        let (val, executed) = DivRem::div_rem(val, 2);
+        let (val, eta_seconds) = DivRem::div_rem(val, _2_POW_64);
+        let (val, vote_duration) = DivRem::div_rem(val, _2_POW_64);
+        let (_, vote_start) = DivRem::div_rem(val, _2_POW_64);
+
+        ProposalCore {
+            proposer: proposer.try_into().unwrap(),
+            vote_start: vote_start.try_into().unwrap(),
+            vote_duration: vote_duration.try_into().unwrap(),
+            executed: executed > 0,
+            canceled: canceled > 0,
+            eta_seconds: eta_seconds.try_into().unwrap()
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::num::traits::Bounded;
+    use openzeppelin_testing::constants::ALICE;
+    use super::{ProposalCore, ProposalCoreStorePacking};
+
+    #[test]
+    fn test_pack_and_unpack() {
+        let proposal = ProposalCore {
+            proposer: ALICE(),
+            vote_start: 100,
+            vote_duration: 200,
+            executed: false,
+            canceled: true,
+            eta_seconds: 300
+        };
+        let packed = ProposalCoreStorePacking::pack(proposal);
+        let unpacked = ProposalCoreStorePacking::unpack(packed);
+        assert_eq!(proposal, unpacked);
+    }
+
+    #[test]
+    fn test_pack_and_unpack_big_values() {
+        let proposal = ProposalCore {
+            proposer: ALICE(),
+            vote_start: Bounded::MAX,
+            vote_duration: Bounded::MAX,
+            executed: true,
+            canceled: true,
+            eta_seconds: Bounded::MAX
+        };
+        let packed = ProposalCoreStorePacking::pack(proposal);
+        let unpacked = ProposalCoreStorePacking::unpack(packed);
+        assert_eq!(proposal, unpacked);
+    }
+}
diff --git a/packages/governance/src/governor/vote.cairo b/packages/governance/src/governor/vote.cairo
new file mode 100644
index 000000000..75e98b4b4
--- /dev/null
+++ b/packages/governance/src/governor/vote.cairo
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (governance/governor/vote.cairo)
+
+use core::hash::{HashStateTrait, HashStateExTrait};
+use core::poseidon::PoseidonTrait;
+use openzeppelin_utils::cryptography::snip12::{StructHash, SNIP12HashSpanImpl};
+use starknet::ContractAddress;
+
+// sn_keccak(
+//      "\"Vote\"(\"verifying_contract\":\"ContractAddress\",
+//      \"nonce\":\"felt\",
+//      \"proposal_id\":\"felt\",
+//      \"support\":\"u128\",
+//      \"voter\":\"ContractAddress\")"
+// )
+//
+// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation.
+pub const VOTE_TYPE_HASH: felt252 =
+    0x19a625949c5c367200d9ca91e845fe9c5f3c7f04735d97d91f3a6cb4cb30b81;
+
+#[derive(Copy, Drop, Hash)]
+pub struct Vote {
+    pub verifying_contract: ContractAddress,
+    pub nonce: felt252,
+    pub proposal_id: felt252,
+    pub support: u8,
+    pub voter: ContractAddress,
+}
+
+impl VoteStructHashImpl of StructHash<Vote> {
+    fn hash_struct(self: @Vote) -> felt252 {
+        let hash_state = PoseidonTrait::new();
+        hash_state.update_with(VOTE_TYPE_HASH).update_with(*self).finalize()
+    }
+}
+
+// sn_keccak(
+//      "\"VoteWithReasonAndParams\"(\"verifying_contract\":\"ContractAddress\",
+//      \"nonce\":\"felt\",
+//      \"proposal_id\":\"felt\",
+//      \"support\":\"u128\",
+//      \"voter\":\"ContractAddress\",
+//      \"reason_hash\":\"felt\",
+//      \"params\":\"felt*\")"
+// )
+//
+// Since there's no u8 type in SNIP-12, we use u128 for `support` in the type hash generation.
+pub const VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH: felt252 =
+    0x1f4ccab7220d6a3c0c1cbc1008bfcb3f6fbdb361dd14fd017fabd229e0cf94b;
+
+#[derive(Copy, Drop)]
+pub struct VoteWithReasonAndParams {
+    pub verifying_contract: ContractAddress,
+    pub nonce: felt252,
+    pub proposal_id: felt252,
+    pub support: u8,
+    pub voter: ContractAddress,
+    pub reason_hash: felt252,
+    pub params: Span<felt252>,
+}
+
+impl VoteWithReasonAndParamsStructHashImpl of StructHash<VoteWithReasonAndParams> {
+    fn hash_struct(self: @VoteWithReasonAndParams) -> felt252 {
+        let hash_state = PoseidonTrait::new();
+        hash_state
+            .update_with(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH)
+            .update_with(*self.verifying_contract)
+            .update_with(*self.nonce)
+            .update_with(*self.proposal_id)
+            .update_with(*self.support)
+            .update_with(*self.voter)
+            .update_with(*self.reason_hash)
+            .update_with(*self.params)
+            .finalize()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{VOTE_TYPE_HASH, VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH};
+
+    #[test]
+    fn test_vote_type_hash() {
+        let expected = selector!(
+            "\"Vote\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u128\",\"voter\":\"ContractAddress\")"
+        );
+        assert_eq!(VOTE_TYPE_HASH, expected);
+    }
+
+    #[test]
+    fn test_vote_with_reason_and_params_type_hash() {
+        let expected = selector!(
+            "\"VoteWithReasonAndParams\"(\"verifying_contract\":\"ContractAddress\",\"nonce\":\"felt\",\"proposal_id\":\"felt\",\"support\":\"u128\",\"voter\":\"ContractAddress\",\"reason_hash\":\"felt\",\"params\":\"felt*\")"
+        );
+        assert_eq!(VOTE_WITH_REASON_AND_PARAMS_TYPE_HASH, expected);
+    }
+}
diff --git a/packages/governance/src/lib.cairo b/packages/governance/src/lib.cairo
index 041e1b05e..a30d8952a 100644
--- a/packages/governance/src/lib.cairo
+++ b/packages/governance/src/lib.cairo
@@ -1,3 +1,4 @@
+pub mod governor;
 pub mod multisig;
 
 #[cfg(test)]
diff --git a/packages/governance/src/utils/call_impls.cairo b/packages/governance/src/utils/call_impls.cairo
index 36ba4df14..4079ffe06 100644
--- a/packages/governance/src/utils/call_impls.cairo
+++ b/packages/governance/src/utils/call_impls.cairo
@@ -19,10 +19,9 @@ pub impl HashCallImpl<S, +HashStateTrait<S>, +Drop<S>> of Hash<Call, S> {
 pub impl HashCallsImpl<S, +HashStateTrait<S>, +Drop<S>> of Hash<Span<Call>, S> {
     fn update_state(mut state: S, value: Span<Call>) -> S {
         state = state.update_with(value.len());
-        for call in value {
-            state = state.update_with(*call);
+        for elem in value {
+            state = state.update_with(*elem);
         };
-
         state
     }
 }
diff --git a/packages/utils/src/bytearray.cairo b/packages/utils/src/bytearray.cairo
new file mode 100644
index 000000000..f2f383943
--- /dev/null
+++ b/packages/utils/src/bytearray.cairo
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts for Cairo v0.19.0 (utils/bytearray.cairo)
+
+use core::byte_array::ByteArrayTrait;
+use core::hash::{HashStateTrait, HashStateExTrait};
+use core::pedersen::PedersenTrait;
+use core::to_byte_array::FormatAsByteArray;
+
+/// Reads n bytes from a byte array starting from a given index.
+///
+/// Requirements:
+///
+/// - `start + n` must be less than or equal to the length of the byte array.
+pub fn read_n_bytes(data: @ByteArray, start: u32, n: u32) -> ByteArray {
+    let end_index = start + n;
+    assert(end_index <= data.len(), 'ByteArray: out of bounds');
+
+    let mut result: ByteArray = Default::default();
+    for i in start..end_index {
+        result.append_byte(data[i]);
+    };
+    result
+}
+
+/// Converts a value to a byte array with a given base and padding.
+///
+/// Requirements:
+///
+/// - `base` cannot be zero.
+pub fn to_byte_array<T, +Into<T, felt252>, +Copy<T>>(
+    value: @T, base: u8, padding: u16
+) -> ByteArray {
+    let value: felt252 = (*value).into();
+    let base: felt252 = base.into();
+    let mut byte_array = value
+        .format_as_byte_array(base.try_into().expect('ByteArray: base cannot be 0'));
+
+    if padding.into() > byte_array.len() {
+        let mut padding = padding.into() - byte_array.len();
+        while padding > 0 {
+            byte_array = "0" + byte_array;
+            padding -= 1;
+        };
+    };
+    byte_array
+}
+
+/// Returns a unique hash given a ByteArray.
+///
+/// The hash is computed by serializing the data into a span of felts, and
+/// then hashing the span using the Pedersen hash algorithm.
+pub fn hash_byte_array(data: @ByteArray) -> felt252 {
+    let mut serialized = array![];
+
+    data.serialize(ref serialized);
+    let len = serialized.len();
+
+    let mut state = PedersenTrait::new(0);
+    for elem in serialized {
+        state = state.update_with(elem);
+    };
+    state = state.update_with(len);
+    state.finalize()
+}
+
+/// ByteArray extension trait.
+#[generate_trait]
+pub impl ByteArrayExtImpl of ByteArrayExtTrait {
+    // Reads n bytes from a byte array starting from a given index.
+    ///
+    /// Requirements:
+    ///
+    /// - `start + n` must be less than or equal to the length of the byte array.
+    fn read_n_bytes(self: @ByteArray, start: u32, n: u32) -> ByteArray {
+        read_n_bytes(self, start, n)
+    }
+
+    /// Converts a value to a byte array with a given base and padding.
+    ///
+    /// Requirements:
+    ///
+    /// - `base` cannot be zero.
+    fn to_byte_array<T, +Into<T, felt252>, +Copy<T>>(
+        self: @T, base: u8, padding: u16
+    ) -> ByteArray {
+        to_byte_array(self, base, padding)
+    }
+
+    /// Hashes a byte array using the Pedersen hash algorithm.
+    /// Encodes the byte array as a ´Span<felt252>´ by serializing ´data´.
+    fn hash(self: @ByteArray) -> felt252 {
+        hash_byte_array(self)
+    }
+}
diff --git a/packages/utils/src/cryptography/snip12.cairo b/packages/utils/src/cryptography/snip12.cairo
index f2f1df794..9135333f7 100644
--- a/packages/utils/src/cryptography/snip12.cairo
+++ b/packages/utils/src/cryptography/snip12.cairo
@@ -1,8 +1,8 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts for Cairo v0.19.0 (utils/cryptography/snip12.cairo)
 
-use core::hash::{HashStateTrait, HashStateExTrait};
-use core::poseidon::PoseidonTrait;
+use core::hash::{HashStateTrait, HashStateExTrait, Hash};
+use core::poseidon::{PoseidonTrait, HashState};
 use starknet::{ContractAddress, get_tx_info};
 
 // selector!(
@@ -77,3 +77,14 @@ pub(crate) impl OffchainMessageHashImpl<
         state.finalize()
     }
 }
+
+/// Hash trait implementation for a span of elements according to SNIP-12.
+pub impl SNIP12HashSpanImpl<T, +Copy<T>, +Hash<T, HashState>> of Hash<Span<T>, HashState> {
+    fn update_state(mut state: HashState, value: Span<T>) -> HashState {
+        let mut inner_state = PoseidonTrait::new();
+        for elem in value {
+            inner_state = inner_state.update_with(*elem);
+        };
+        state.update_with(inner_state.finalize())
+    }
+}
diff --git a/packages/utils/src/lib.cairo b/packages/utils/src/lib.cairo
index e1d443c29..02dcf735f 100644
--- a/packages/utils/src/lib.cairo
+++ b/packages/utils/src/lib.cairo
@@ -1,6 +1,4 @@
-// SPDX-License-Identifier: MIT
-// OpenZeppelin Contracts for Cairo v0.19.0 (utils/lib.cairo)
-
+pub mod bytearray;
 pub mod cryptography;
 pub mod deployments;
 pub mod interfaces;
diff --git a/packages/utils/src/structs.cairo b/packages/utils/src/structs.cairo
index 4c0eada6d..a42c00014 100644
--- a/packages/utils/src/structs.cairo
+++ b/packages/utils/src/structs.cairo
@@ -1,1 +1,3 @@
 pub mod checkpoint;
+
+pub use checkpoint::{Trace, Checkpoint};
diff --git a/packages/utils/src/structs/checkpoint.cairo b/packages/utils/src/structs/checkpoint.cairo
index c677f805f..46f2d39ae 100644
--- a/packages/utils/src/structs/checkpoint.cairo
+++ b/packages/utils/src/structs/checkpoint.cairo
@@ -7,8 +7,6 @@ use starknet::storage::{StoragePath, StorageAsPath, Vec, VecTrait, Mutable, Muta
 use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
 use starknet::storage_access::StorePacking;
 
-const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000;
-
 /// `Trace` struct, for checkpointing values as they change at different points in
 /// time, and later looking up past values by block timestamp.
 #[starknet::storage_node]
@@ -161,20 +159,26 @@ impl CheckpointImpl of CheckpointTrait {
     }
 }
 
+const _2_POW_184: felt252 = 0x10000000000000000000000000000000000000000000000;
+const _128_BITS_MASK: u256 = 0xffffffffffffffffffffffffffffffff;
+
 /// Packs a Checkpoint into a (felt252, felt252).
 ///
 /// The packing is done as follows:
+///
 /// - The first felt of the tuple contains `key` and `value.low`.
-/// - In this first felt, the first four bits are skipped to avoid representation errors due
-///   to `felt252` max value being a bit less than a 252 bits number max value
-///   (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/).
 /// - `key` is stored at range [4,67] bits (0-indexed), taking the most significant usable bits.
 /// - `value.low` is stored at range [124, 251], taking the less significant bits (at the end).
 /// - `value.high` is stored as the second tuple element.
+///
+/// NOTE: In this first felt, the first four bits are skipped to avoid representation errors due
+/// to `felt252` max value being a bit less than a 252 bits number max value
+/// (https://docs.starknet.io/documentation/architecture_and_concepts/Cryptography/p-value/).
 impl CheckpointStorePacking of StorePacking<Checkpoint, (felt252, felt252)> {
     fn pack(value: Checkpoint) -> (felt252, felt252) {
         let checkpoint = value;
-        // shift-left by 184 bits
+
+        // shift-left to reach the corresponding position
         let key = checkpoint.key.into() * _2_POW_184;
         let key_and_low = key + checkpoint.value.low.into();
 
@@ -183,11 +187,11 @@ impl CheckpointStorePacking of StorePacking<Checkpoint, (felt252, felt252)> {
 
     fn unpack(value: (felt252, felt252)) -> Checkpoint {
         let (key_and_low, high) = value;
-
         let key_and_low: u256 = key_and_low.into();
-        // shift-right by 184 bits
+
+        // shift-right and mask to extract the corresponding values
         let key: u256 = key_and_low / _2_POW_184.into();
-        let low = key_and_low & 0xffffffffffffffffffffffffffffffff;
+        let low = key_and_low & _128_BITS_MASK;
 
         Checkpoint {
             key: key.try_into().unwrap(),
diff --git a/sncast_scripts/Scarb.lock b/sncast_scripts/Scarb.lock
index fc192541d..58357c836 100644
--- a/sncast_scripts/Scarb.lock
+++ b/sncast_scripts/Scarb.lock
@@ -25,14 +25,6 @@ dependencies = [
  "openzeppelin_token",
 ]
 
-[[package]]
-name = "openzeppelin_governance"
-version = "0.19.0"
-dependencies = [
- "openzeppelin_access",
- "openzeppelin_introspection",
-]
-
 [[package]]
 name = "openzeppelin_introspection"
 version = "0.19.0"
@@ -47,6 +39,7 @@ dependencies = [
  "openzeppelin_introspection",
  "openzeppelin_token",
  "openzeppelin_upgrades",
+ "openzeppelin_utils",
 ]
 
 [[package]]
@@ -60,9 +53,10 @@ dependencies = [
 name = "openzeppelin_token"
 version = "0.19.0"
 dependencies = [
+ "openzeppelin_access",
  "openzeppelin_account",
- "openzeppelin_governance",
  "openzeppelin_introspection",
+ "openzeppelin_utils",
 ]
 
 [[package]]
@@ -90,15 +84,15 @@ checksum = "sha256:cfd7c73a6f9984880249babfa8664b69c5f7209c737b1081156a284061ccd
 
 [[package]]
 name = "snforge_scarb_plugin"
-version = "0.2.0"
+version = "0.32.0"
 source = "registry+https://scarbs.xyz/"
-checksum = "sha256:2e4ce3ebe3f49548bd26908391b5d78537a765d827df0d96c32aeb88941d0d67"
+checksum = "sha256:e5a0e80294b1f5f00955c614ee3fc94c843ff0d27935693c3598d0ac8d79250a"
 
 [[package]]
 name = "snforge_std"
-version = "0.30.0"
+version = "0.32.0"
 source = "registry+https://scarbs.xyz/"
-checksum = "sha256:2f3c4846881813ac0f5d1460981249c9f5e2a6831e752beedf9b70975495b4ec"
+checksum = "sha256:0e3cb45c6276334fd142a77212f0592d55744f1c022b7a63f20bcd79d0ce3927"
 dependencies = [
  "snforge_scarb_plugin",
 ]


Test patch:
diff --git a/packages/governance/src/tests.cairo b/packages/governance/src/tests.cairo
index 6a51994ef..b9ca1c3dc 100644
--- a/packages/governance/src/tests.cairo
+++ b/packages/governance/src/tests.cairo
@@ -1,3 +1,4 @@
+mod governor;
 mod test_multisig;
 mod test_timelock;
 mod test_utils;
diff --git a/packages/governance/src/tests/governor.cairo b/packages/governance/src/tests/governor.cairo
new file mode 100644
index 000000000..981591110
--- /dev/null
+++ b/packages/governance/src/tests/governor.cairo
@@ -0,0 +1,8 @@
+mod common;
+mod test_governor;
+mod test_governor_core_execution;
+mod test_governor_counting_simple;
+mod test_governor_settings;
+mod test_governor_timelock_execution;
+mod test_governor_votes;
+mod test_governor_votes_quorum_fraction;
diff --git a/packages/governance/src/tests/governor/common.cairo b/packages/governance/src/tests/governor/common.cairo
new file mode 100644
index 000000000..4ebb68ef0
--- /dev/null
+++ b/packages/governance/src/tests/governor/common.cairo
@@ -0,0 +1,246 @@
+use core::hash::{HashStateTrait, HashStateExTrait};
+use core::pedersen::PedersenTrait;
+use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl};
+use crate::governor::interface::{IGovernor, ProposalState};
+use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore};
+use crate::utils::call_impls::{HashCallImpl, HashCallsImpl};
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_test_common::mocks::governor::{GovernorMock, GovernorTimelockedMock};
+use openzeppelin_testing::constants::{ADMIN, OTHER};
+use openzeppelin_utils::bytearray::ByteArrayExtTrait;
+use snforge_std::{start_cheat_block_timestamp_global, start_mock_call};
+use starknet::ContractAddress;
+use starknet::account::Call;
+use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess};
+
+pub type ComponentState = GovernorComponent::ComponentState<GovernorMock::ContractState>;
+pub type ComponentStateTimelocked =
+    GovernorComponent::ComponentState<GovernorTimelockedMock::ContractState>;
+
+pub fn CONTRACT_STATE() -> GovernorMock::ContractState {
+    GovernorMock::contract_state_for_testing()
+}
+
+pub fn COMPONENT_STATE() -> ComponentState {
+    GovernorComponent::component_state_for_testing()
+}
+
+pub fn CONTRACT_STATE_TIMELOCKED() -> GovernorTimelockedMock::ContractState {
+    GovernorTimelockedMock::contract_state_for_testing()
+}
+
+pub fn COMPONENT_STATE_TIMELOCKED() -> ComponentStateTimelocked {
+    GovernorComponent::component_state_for_testing()
+}
+
+//
+// Helpers
+//
+
+pub fn hash_proposal(calls: Span<Call>, description_hash: felt252) -> felt252 {
+    PedersenTrait::new(0).update_with(calls).update_with(description_hash).finalize()
+}
+
+pub fn get_proposal_info() -> (felt252, ProposalCore) {
+    let calls = get_calls(OTHER(), false);
+    get_proposal_with_id(calls, @"proposal description")
+}
+
+pub fn get_proposal_with_id(calls: Span<Call>, description: @ByteArray) -> (felt252, ProposalCore) {
+    let timestamp = starknet::get_block_timestamp();
+    let vote_start = timestamp + GovernorMock::VOTING_DELAY;
+    let vote_duration = GovernorMock::VOTING_PERIOD;
+
+    let proposal_id = hash_proposal(calls, description.hash());
+    let proposal = ProposalCore {
+        proposer: ADMIN(),
+        vote_start,
+        vote_duration,
+        executed: false,
+        canceled: false,
+        eta_seconds: 0
+    };
+
+    (proposal_id, proposal)
+}
+
+pub fn get_calls(to: ContractAddress, mock_syscalls: bool) -> Span<Call> {
+    let call1 = Call { to, selector: selector!("test1"), calldata: array![].span() };
+    let call2 = Call { to, selector: selector!("test2"), calldata: array![].span() };
+
+    if mock_syscalls {
+        start_mock_call(to, selector!("test1"), 'test1');
+        start_mock_call(to, selector!("test2"), 'test2');
+    }
+
+    array![call1, call2].span()
+}
+
+pub fn get_state(
+    state: @ComponentState, id: felt252, external_state_version: bool
+) -> ProposalState {
+    if external_state_version {
+        state.state(id)
+    } else {
+        state._state(id)
+    }
+}
+
+pub fn get_mock_state(
+    mock_state: @GovernorMock::ContractState, id: felt252, external_state_version: bool
+) -> ProposalState {
+    if external_state_version {
+        mock_state.governor.state(id)
+    } else {
+        mock_state.governor._state(id)
+    }
+}
+
+pub fn set_executor(
+    ref mock_state: GovernorTimelockedMock::ContractState, executor: ContractAddress
+) {
+    mock_state.governor_timelock_execution.Governor_timelock_controller.write(executor);
+}
+
+//
+// Setup proposals
+//
+
+pub fn setup_pending_proposal(
+    ref state: ComponentState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let current_state = get_state(@state, id, external_state_version);
+    let expected = ProposalState::Pending;
+
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_active_proposal(
+    ref state: ComponentState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Active;
+
+    // Is active before deadline
+    start_cheat_block_timestamp_global(deadline - 1);
+    let current_state = get_state(@state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_queued_proposal(
+    ref mock_state: GovernorMock::ContractState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, mut proposal) = get_proposal_info();
+
+    proposal.eta_seconds = 1;
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+
+    // Quorum reached
+    start_cheat_block_timestamp_global(deadline + 1);
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote succeeded
+    proposal_votes.against_votes.write(quorum);
+
+    let expected = ProposalState::Queued;
+    let current_state = get_mock_state(@mock_state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_canceled_proposal(
+    ref state: ComponentState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    state._cancel(id, 0);
+
+    let expected = ProposalState::Canceled;
+    let current_state = get_state(@state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_defeated_proposal(
+    ref mock_state: GovernorMock::ContractState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+
+    // Quorum not reached
+    start_cheat_block_timestamp_global(deadline + 1);
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum - 1);
+
+    let expected = ProposalState::Defeated;
+    let current_state = get_mock_state(@mock_state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_succeeded_proposal(
+    ref mock_state: GovernorMock::ContractState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Succeeded;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum reached
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote succeeded
+    proposal_votes.against_votes.write(quorum);
+
+    let current_state = get_mock_state(@mock_state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
+
+pub fn setup_executed_proposal(
+    ref state: ComponentState, external_state_version: bool
+) -> (felt252, ProposalCore) {
+    let (id, mut proposal) = get_proposal_info();
+
+    proposal.executed = true;
+    state.Governor_proposals.write(id, proposal);
+
+    let current_state = get_state(@state, id, external_state_version);
+    let expected = ProposalState::Executed;
+
+    assert_eq!(current_state, expected);
+
+    (id, proposal)
+}
diff --git a/packages/governance/src/tests/governor/test_governor.cairo b/packages/governance/src/tests/governor/test_governor.cairo
new file mode 100644
index 000000000..2cdbfa40a
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor.cairo
@@ -0,0 +1,2113 @@
+use core::num::traits::{Bounded, Zero};
+use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl};
+use crate::governor::interface::{IGovernor, IGOVERNOR_ID, ProposalState};
+use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait};
+use crate::governor::vote::{Vote, VoteWithReasonAndParams};
+use crate::governor::{DefaultConfig, GovernorComponent, ProposalCore};
+use crate::tests::governor::common::{
+    hash_proposal, get_state, get_mock_state, get_proposal_info, get_calls
+};
+use crate::tests::governor::common::{
+    setup_pending_proposal, setup_active_proposal, setup_queued_proposal, setup_canceled_proposal,
+    setup_defeated_proposal, setup_succeeded_proposal, setup_executed_proposal
+};
+use crate::tests::governor::common::{COMPONENT_STATE, CONTRACT_STATE};
+use openzeppelin_introspection::src5::SRC5Component::SRC5Impl;
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_test_common::mocks::governor::GovernorMock;
+use openzeppelin_test_common::mocks::timelock::{
+    IMockContractDispatcher, IMockContractDispatcherTrait
+};
+use openzeppelin_testing as utils;
+use openzeppelin_testing::constants::{ADMIN, OTHER, ZERO, VOTES_TOKEN};
+use openzeppelin_testing::events::EventSpyExt;
+use openzeppelin_utils::bytearray::ByteArrayExtTrait;
+use openzeppelin_utils::cryptography::snip12::OffchainMessageHash;
+use snforge_std::signature::stark_curve::{StarkCurveKeyPairImpl, StarkCurveSignerImpl};
+use snforge_std::{
+    start_cheat_caller_address, start_cheat_block_timestamp_global, start_cheat_chain_id_global,
+    start_mock_call
+};
+use snforge_std::{EventSpy, spy_events, test_address};
+use starknet::account::Call;
+use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess};
+use starknet::{ContractAddress, contract_address_const};
+
+//
+// Dispatchers
+//
+
+fn deploy_governor() -> IGovernorDispatcher {
+    let mut calldata = array![VOTES_TOKEN().into()];
+
+    let address = utils::declare_and_deploy("GovernorMock", calldata);
+    IGovernorDispatcher { contract_address: address }
+}
+
+fn deploy_mock_target() -> IMockContractDispatcher {
+    let mut calldata = array![];
+
+    let address = utils::declare_and_deploy("MockContract", calldata);
+    IMockContractDispatcher { contract_address: address }
+}
+
+fn setup_dispatchers() -> (IGovernorDispatcher, IMockContractDispatcher) {
+    let governor = deploy_governor();
+    let target = deploy_mock_target();
+
+    (governor, target)
+}
+
+fn setup_account(public_key: felt252) -> ContractAddress {
+    let mut calldata = array![public_key];
+    utils::declare_and_deploy("SnakeAccountMock", calldata)
+}
+
+//
+// External
+//
+
+#[test]
+fn test_name() {
+    let state = @COMPONENT_STATE();
+    let name = state.name();
+    assert_eq!(name, 'DAPP_NAME');
+}
+
+#[test]
+fn test_version() {
+    let state = COMPONENT_STATE();
+    let version = state.version();
+    assert_eq!(version, 'DAPP_VERSION');
+}
+
+#[test]
+fn test_counting_mode() {
+    let state = COMPONENT_STATE();
+    let counting_mode = state.COUNTING_MODE();
+    assert_eq!(counting_mode, "support=bravo&quorum=for,abstain");
+}
+
+#[test]
+fn test_hash_proposal() {
+    let state = COMPONENT_STATE();
+    let calls = get_calls(ZERO(), false);
+    let description = @"proposal description";
+    let description_hash = description.hash();
+
+    let expected_hash = hash_proposal(calls, description_hash);
+    let hash = state.hash_proposal(calls, description_hash);
+
+    assert_eq!(hash, expected_hash);
+}
+
+//
+// state
+//
+
+#[test]
+fn test_state_executed() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_executed_proposal(ref state, true);
+}
+
+#[test]
+fn test_state_canceled() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_canceled_proposal(ref state, true);
+}
+
+#[test]
+#[should_panic(expected: 'Nonexistent proposal')]
+fn test_state_non_existent() {
+    let state = COMPONENT_STATE();
+
+    state._state(1);
+}
+
+#[test]
+fn test_state_pending() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_pending_proposal(ref state, true);
+}
+
+fn test_state_active_external_version(external_state_version: bool) {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Active;
+
+    // Is active before deadline
+    start_cheat_block_timestamp_global(deadline - 1);
+    let current_state = get_state(@state, id, external_state_version);
+    assert_eq!(current_state, expected);
+
+    // Is active in deadline
+    start_cheat_block_timestamp_global(deadline);
+    let current_state = get_state(@state, id, external_state_version);
+    assert_eq!(current_state, expected);
+}
+
+#[test]
+fn test_state_active() {
+    test_state_active_external_version(true);
+}
+
+fn test_state_defeated_quorum_not_reached_external_version(external_state_version: bool) {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum not reached
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum - 1);
+
+    let current_state = get_mock_state(@mock_state, id, external_state_version);
+    assert_eq!(current_state, expected);
+}
+
+#[test]
+fn test_state_defeated_quorum_not_reached() {
+    test_state_defeated_quorum_not_reached_external_version(true);
+}
+
+fn test_state_defeated_vote_not_succeeded_external_version(external_state_version: bool) {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum reached
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote not succeeded
+    proposal_votes.against_votes.write(quorum + 1);
+
+    let current_state = get_mock_state(@mock_state, id, external_state_version);
+    assert_eq!(current_state, expected);
+}
+
+#[test]
+fn test_state_defeated_vote_not_succeeded() {
+    test_state_defeated_vote_not_succeeded_external_version(true);
+}
+
+#[test]
+fn test_state_queued() {
+    let mut mock_state = CONTRACT_STATE();
+
+    // The function already asserts the state
+    setup_queued_proposal(ref mock_state, true);
+}
+
+#[test]
+fn test_state_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+
+    // The function already asserts the state
+    setup_succeeded_proposal(ref mock_state, true);
+}
+
+//
+// Proposal info
+//
+
+#[test]
+fn test_proposal_threshold() {
+    let state = COMPONENT_STATE();
+
+    let threshold = state.proposal_threshold();
+    let expected = GovernorMock::PROPOSAL_THRESHOLD;
+    assert_eq!(threshold, expected);
+}
+
+#[test]
+fn test_proposal_snapshot() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let snapshot = state.proposal_snapshot(id);
+    let expected = proposal.vote_start;
+    assert_eq!(snapshot, expected);
+}
+
+#[test]
+fn test_proposal_deadline() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let deadline = state.proposal_deadline(id);
+    let expected = proposal.vote_start + proposal.vote_duration;
+    assert_eq!(deadline, expected);
+}
+
+#[test]
+fn test_proposal_proposer() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let proposer = state.proposal_proposer(id);
+    let expected = proposal.proposer;
+    assert_eq!(proposer, expected);
+}
+
+#[test]
+fn test_proposal_eta() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let eta = state.proposal_eta(id);
+    let expected = proposal.eta_seconds;
+    assert_eq!(eta, expected);
+}
+
+#[test]
+fn test_proposal_needs_queuing() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let needs_queuing = state.proposal_needs_queuing(id);
+    assert_eq!(needs_queuing, false);
+}
+
+#[test]
+fn test_voting_delay() {
+    let state = COMPONENT_STATE();
+
+    let threshold = state.voting_delay();
+    let expected = GovernorMock::VOTING_DELAY;
+    assert_eq!(threshold, expected);
+}
+
+#[test]
+fn test_voting_period() {
+    let state = COMPONENT_STATE();
+
+    let threshold = state.voting_period();
+    let expected = GovernorMock::VOTING_PERIOD;
+    assert_eq!(threshold, expected);
+}
+
+#[test]
+fn test_quorum(timepoint: u64) {
+    let state = COMPONENT_STATE();
+
+    let threshold = state.quorum(timepoint);
+    let expected = if timepoint == Bounded::MAX {
+        Bounded::MAX
+    } else {
+        GovernorMock::QUORUM
+    };
+    assert_eq!(threshold, expected);
+}
+
+//
+// get_votes
+//
+
+#[test]
+fn test_get_votes() {
+    let state = COMPONENT_STATE();
+    let timepoint = 0;
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    let votes = state.get_votes(OTHER(), timepoint);
+    assert_eq!(votes, expected_weight);
+}
+
+#[test]
+fn test_get_votes_with_params() {
+    let state = COMPONENT_STATE();
+    let timepoint = 0;
+    let expected_weight = 100;
+    let params = array!['param'].span();
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    let votes = state.get_votes_with_params(OTHER(), timepoint, params);
+    assert_eq!(votes, expected_weight);
+}
+
+//
+// has_voted
+//
+
+#[test]
+fn test_has_voted() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+
+    let reason = "reason";
+    let params = array![].span();
+
+    // 1. Assert has not voted
+    let has_not_voted = !state.has_voted(id, OTHER());
+    assert!(has_not_voted);
+
+    // 2. Cast vote
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), 100_u256);
+    state._cast_vote(id, OTHER(), 0, reason, params);
+
+    // 3. Assert has voted
+    let has_voted = state.has_voted(id, OTHER());
+    assert!(has_voted);
+}
+
+//
+// propose
+//
+
+fn test_propose_external_version(external_state_version: bool) {
+    let mut state = COMPONENT_STATE();
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let calls = get_calls(OTHER(), false);
+    let proposer = ADMIN();
+    let address = ADMIN().to_byte_array(16, 64);
+    let mut description: ByteArray = "proposal description#proposer=0x";
+    description.append(@address);
+    let description_snap = @description;
+    let vote_start = starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY;
+    let vote_end = vote_start + GovernorMock::VOTING_PERIOD;
+
+    // 1. Check id
+    let id = if external_state_version {
+        state.propose(calls, description)
+    } else {
+        state._propose(calls, description_snap, proposer)
+    };
+    let expected_id = hash_proposal(calls, description_snap.hash());
+    assert_eq!(id, expected_id);
+
+    // 2. Check event
+    spy
+        .assert_only_event_proposal_created(
+            contract_address,
+            expected_id,
+            proposer,
+            calls,
+            array![].span(),
+            vote_start,
+            vote_end,
+            description_snap
+        );
+
+    // 3. Check proposal
+    let proposal = state.get_proposal(id);
+    let expected = ProposalCore {
+        proposer: ADMIN(),
+        vote_start: starknet::get_block_timestamp() + GovernorMock::VOTING_DELAY,
+        vote_duration: GovernorMock::VOTING_PERIOD,
+        executed: false,
+        canceled: false,
+        eta_seconds: 0
+    };
+
+    assert_eq!(proposal, expected);
+}
+
+#[test]
+fn test_propose() {
+    let votes = GovernorMock::PROPOSAL_THRESHOLD + 1;
+
+    start_cheat_block_timestamp_global(10);
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), votes);
+    start_cheat_caller_address(test_address(), ADMIN());
+
+    test_propose_external_version(true);
+}
+
+#[test]
+#[should_panic(expected: 'Existent proposal')]
+fn test_propose_existent_proposal() {
+    let mut state = COMPONENT_STATE();
+    let calls = get_calls(OTHER(), false);
+    let description = "proposal description";
+    let votes = GovernorMock::PROPOSAL_THRESHOLD + 1;
+
+    start_cheat_block_timestamp_global(10);
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), votes);
+    start_cheat_caller_address(test_address(), ADMIN());
+
+    state.propose(calls, description.clone());
+
+    // Propose again
+    state.propose(calls, description);
+}
+
+#[test]
+#[should_panic(expected: 'Insufficient votes')]
+fn test_propose_insufficient_proposer_votes() {
+    let mut state = COMPONENT_STATE();
+    let votes = GovernorMock::PROPOSAL_THRESHOLD - 1;
+
+    start_cheat_block_timestamp_global(10);
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), votes);
+
+    let calls = get_calls(ZERO(), false);
+    let description = "proposal description";
+
+    state.propose(calls, description);
+}
+
+#[test]
+#[should_panic(expected: 'Restricted proposer')]
+fn test_propose_restricted_proposer() {
+    let mut state = COMPONENT_STATE();
+
+    let calls = get_calls(ZERO(), false);
+    let description =
+        "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
+
+    state.propose(calls, description);
+}
+
+//
+// execute
+//
+
+#[test]
+fn test_execute() {
+    let (mut governor, target) = setup_dispatchers();
+    let new_number = 125;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("set_number"),
+        calldata: array![new_number].span()
+    };
+    let calls = array![call].span();
+    let description = "proposal description";
+
+    let number = target.get_number();
+    assert_eq!(number, 0);
+
+    // Mock the get_past_votes call
+    let quorum = GovernorMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 1. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let id = governor.propose(calls, description.clone());
+
+    // 2. Cast vote
+
+    // Fast forward the vote delay
+    current_time += GovernorMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // Cast vote
+    governor.cast_vote(id, 1);
+
+    // 3. Execute
+
+    // Fast forward the vote duration
+    current_time += (GovernorMock::VOTING_PERIOD + 1);
+    start_cheat_block_timestamp_global(current_time);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Succeeded);
+
+    let mut spy = spy_events();
+    governor.execute(calls, (@description).hash());
+
+    // 4. Assertions
+    let number = target.get_number();
+    assert_eq!(number, new_number);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Executed);
+
+    spy.assert_only_event_proposal_executed(governor.contract_address, id);
+}
+
+#[test]
+#[should_panic(expected: 'Expected failure')]
+fn test_execute_panics() {
+    let (mut governor, target) = setup_dispatchers();
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("failing_function"),
+        calldata: array![].span()
+    };
+    let calls = array![call].span();
+    let description = "proposal description";
+
+    // Mock the get_past_votes call
+    let quorum = GovernorMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 1. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let id = governor.propose(calls, description.clone());
+
+    // 2. Cast vote
+
+    // Fast forward the vote delay
+    current_time += GovernorMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // Cast vote
+    governor.cast_vote(id, 1);
+
+    // 3. Execute
+
+    // Fast forward the vote duration
+    current_time += (GovernorMock::VOTING_PERIOD + 1);
+    start_cheat_block_timestamp_global(current_time);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Succeeded);
+
+    governor.execute(calls, (@description).hash());
+}
+
+#[test]
+fn test_execute_correct_id() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_succeeded_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), true);
+    let description = @"proposal description";
+
+    let id = mock_state.governor.execute(calls, description.hash());
+    let expected_id = hash_proposal(calls, description.hash());
+    assert_eq!(id, expected_id);
+}
+
+#[test]
+fn test_execute_succeeded_passes() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_succeeded_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), true);
+    let description = @"proposal description";
+
+    mock_state.governor.execute(calls, description.hash());
+}
+
+#[test]
+fn test_execute_queued_passes() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_queued_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), true);
+    let description = @"proposal description";
+
+    mock_state.governor.execute(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_execute_pending() {
+    let mut state = COMPONENT_STATE();
+    setup_pending_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.execute(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_execute_active() {
+    let mut state = COMPONENT_STATE();
+    setup_active_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.execute(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_execute_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_defeated_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    mock_state.governor.execute(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_execute_canceled() {
+    let mut state = COMPONENT_STATE();
+    setup_canceled_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.execute(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_execute_executed() {
+    let mut state = COMPONENT_STATE();
+    setup_executed_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.execute(calls, description.hash());
+}
+
+//
+// cancel
+//
+
+#[test]
+fn test_cancel() {
+    let mut state = COMPONENT_STATE();
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+    let proposer = ADMIN();
+
+    // 1. Propose
+    let id = state._propose(calls, description, proposer);
+
+    let proposal_state = state.state(id);
+    assert_eq!(proposal_state, ProposalState::Pending);
+
+    // 2. Cancel
+    let mut spy = spy_events();
+
+    // Proposer must be the caller
+    start_cheat_caller_address(test_address(), ADMIN());
+
+    state.cancel(calls, description.hash());
+
+    // 3. Assertions
+    let proposal_state = state.state(id);
+    assert_eq!(proposal_state, ProposalState::Canceled);
+
+    spy.assert_only_event_proposal_canceled(test_address(), id);
+}
+
+#[test]
+fn test_cancel_correct_id() {
+    let mut state = COMPONENT_STATE();
+    setup_pending_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    // Proposer must be the caller
+    start_cheat_caller_address(test_address(), ADMIN());
+
+    let id = state.cancel(calls, description.hash());
+    let expected_id = hash_proposal(calls, description.hash());
+    assert_eq!(id, expected_id);
+}
+
+#[test]
+#[should_panic(expected: 'Proposer only')]
+fn test_cancel_invalid_caller() {
+    let mut state = COMPONENT_STATE();
+    setup_pending_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    // Proposer must be the caller
+    start_cheat_caller_address(test_address(), OTHER());
+
+    state.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_succeeded_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), true);
+    let description = @"proposal description";
+
+    mock_state.governor.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_queued_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), true);
+    let description = @"proposal description";
+
+    mock_state.governor.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_active() {
+    let mut state = COMPONENT_STATE();
+    setup_active_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    setup_defeated_proposal(ref mock_state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    mock_state.governor.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_canceled() {
+    let mut state = COMPONENT_STATE();
+    setup_canceled_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.cancel(calls, description.hash());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_executed() {
+    let mut state = COMPONENT_STATE();
+    setup_executed_proposal(ref state, false);
+
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+
+    state.cancel(calls, description.hash());
+}
+
+//
+// cast_vote
+//
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_pending() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref state, false);
+
+    state.cast_vote(id, 0);
+}
+
+#[test]
+fn test_cast_vote_active() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    start_cheat_caller_address(contract_address, OTHER());
+    let weight = state.cast_vote(id, 0);
+    assert_eq!(weight, expected_weight);
+
+    spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @"");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote(id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote(id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote(id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+
+    state.cast_vote(id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+
+    state.cast_vote(id, 0);
+}
+
+//
+// cast_vote_with_reason
+//
+
+#[test]
+fn test_cast_vote_with_reason_active() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let reason = "proposal reason";
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    start_cheat_caller_address(contract_address, OTHER());
+    let weight = state.cast_vote_with_reason(id, 0, reason.clone());
+    assert_eq!(weight, expected_weight);
+
+    spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @reason);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_pending() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref state, false);
+
+    state.cast_vote_with_reason(id, 0, "");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason(id, 0, "");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason(id, 0, "");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason(id, 0, "");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+
+    state.cast_vote_with_reason(id, 0, "");
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+
+    state.cast_vote_with_reason(id, 0, "");
+}
+
+//
+// cast_vote_with_reason_and_params
+//
+
+#[test]
+fn test_cast_vote_with_reason_and_params_active() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let params = array!['param1', 'param2'].span();
+    let reason = "proposal reason";
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    start_cheat_caller_address(contract_address, OTHER());
+    let weight = state.cast_vote_with_reason_and_params(id, 0, reason.clone(), params);
+    assert_eq!(weight, expected_weight);
+
+    spy
+        .assert_only_event_vote_cast_with_params(
+            contract_address, OTHER(), id, 0, expected_weight, @reason, params
+        );
+}
+
+#[test]
+fn test_cast_vote_with_reason_and_params_active_no_params() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let params = array![].span();
+    let reason = "proposal reason";
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    start_cheat_caller_address(contract_address, OTHER());
+    let weight = state.cast_vote_with_reason_and_params(id, 0, reason.clone(), params);
+    assert_eq!(weight, expected_weight);
+
+    spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @reason);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_and_params_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_and_params_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_and_params_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    mock_state.governor.cast_vote_with_reason_and_params(id, 0, "", array![].span());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_and_params_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+
+    state.cast_vote_with_reason_and_params(id, 0, "", array![].span());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cast_vote_with_reason_and_params_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+
+    state.cast_vote_with_reason_and_params(id, 0, "", array![].span());
+}
+
+//
+// cast_vote_by_sig
+//
+
+fn prepare_governor_and_signature(
+    nonce: felt252
+) -> (IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256) {
+    let mut governor = deploy_governor();
+    let calls = get_calls(OTHER(), false);
+    let description = "proposal description";
+
+    // Mock the get_past_votes call
+    let quorum = GovernorMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 1. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let proposal_id = governor.propose(calls, description.clone());
+
+    // 2. Fast forward the vote delay
+    current_time += GovernorMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // 3. Generate a key pair and set up an account
+    let key_pair = StarkCurveKeyPairImpl::generate();
+    let voter = setup_account(key_pair.public_key);
+
+    // 4. Set up signature parameters
+    let support = 1;
+    let verifying_contract = governor.contract_address;
+
+    // 5. Create and sign the vote message
+    let vote = Vote { verifying_contract, nonce, proposal_id, support, voter };
+    let msg_hash = vote.get_message_hash(voter);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    (governor, r, s, proposal_id, support, voter, quorum)
+}
+
+#[test]
+fn test_cast_vote_by_sig() {
+    let (governor, r, s, proposal_id, support, voter, quorum) = prepare_governor_and_signature(0);
+
+    // Set up event spy and cast vote
+    let mut spy = spy_events();
+    governor.cast_vote_by_sig(proposal_id, support, voter, array![r, s].span());
+
+    spy
+        .assert_only_event_vote_cast(
+            governor.contract_address, voter, proposal_id, support, quorum, @""
+        );
+}
+
+#[test]
+#[should_panic(expected: 'Invalid signature')]
+fn test_cast_vote_by_sig_invalid_signature() {
+    let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(0);
+
+    // Cast vote with invalid signature
+    governor.cast_vote_by_sig(proposal_id, support, voter, array![r + 1, s].span());
+}
+
+#[test]
+#[should_panic(expected: 'Invalid signature')]
+fn test_cast_vote_by_sig_invalid_msg_hash() {
+    // Use invalid nonce (not the account's current nonce)
+    let (governor, r, s, proposal_id, support, voter, _) = prepare_governor_and_signature(1);
+
+    // Cast vote with invalid msg hash
+    governor.cast_vote_by_sig(proposal_id, support, voter, array![r, s].span());
+}
+
+#[test]
+fn test_cast_vote_by_sig_hash_generation() {
+    start_cheat_chain_id_global('SN_TEST');
+
+    let verifying_contract = contract_address_const::<'VERIFIER'>();
+    let nonce = 0;
+    let proposal_id = 1;
+    let support = 1;
+    let voter = contract_address_const::<'VOTER'>();
+
+    let vote = Vote { verifying_contract, nonce, proposal_id, support, voter };
+    let hash = vote.get_message_hash(voter);
+
+    // This hash was computed using starknet js sdk from the following values:
+    // - name: 'DAPP_NAME'
+    // - version: 'DAPP_VERSION'
+    // - chainId: 'SN_TEST'
+    // - account: 'VOTER'
+    // - nonce: 0
+    // - verifying_contract: 'VERIFIER'
+    // - proposal_id: 1
+    // - support: 1
+    // - voter: 'VOTER'
+    // - revision: '1'
+    let expected_hash = 0x6541a00fa95d4796bded177fca3cee29d9697174edadebfa4b0b9784379f636;
+    assert_eq!(hash, expected_hash);
+}
+
+//
+// cast_vote_with_reason_and_params_by_sig
+//
+
+fn prepare_governor_and_signature_with_reason_and_params(
+    reason: @ByteArray, params: Span<felt252>, nonce: felt252
+) -> (IGovernorDispatcher, felt252, felt252, felt252, u8, ContractAddress, u256) {
+    let mut governor = deploy_governor();
+    let calls = get_calls(OTHER(), false);
+    let description = "proposal description";
+
+    // Mock the get_past_votes call
+    let quorum = GovernorMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 1. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let proposal_id = governor.propose(calls, description.clone());
+
+    // 2. Fast forward the vote delay
+    current_time += GovernorMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // 3. Generate a key pair and set up an account
+    let key_pair = StarkCurveKeyPairImpl::generate();
+    let voter = setup_account(key_pair.public_key);
+
+    // 4. Set up signature parameters
+    let support = 1;
+    let verifying_contract = governor.contract_address;
+    let reason_hash = reason.hash();
+
+    // 5. Create and sign the vote message
+    let vote = VoteWithReasonAndParams {
+        verifying_contract, nonce, proposal_id, support, voter, reason_hash, params
+    };
+    let msg_hash = vote.get_message_hash(voter);
+    let (r, s) = key_pair.sign(msg_hash).unwrap();
+
+    (governor, r, s, proposal_id, support, voter, quorum)
+}
+
+#[test]
+fn test_cast_vote_with_reason_and_params_by_sig() {
+    let reason = "proposal reason";
+    let params = array!['param'].span();
+
+    let (governor, r, s, proposal_id, support, voter, quorum) =
+        prepare_governor_and_signature_with_reason_and_params(
+        @reason, params, 0
+    );
+
+    // Set up event spy and cast vote
+    let mut spy = spy_events();
+    governor
+        .cast_vote_with_reason_and_params_by_sig(
+            proposal_id, support, voter, reason.clone(), params, array![r, s].span()
+        );
+
+    spy
+        .assert_only_event_vote_cast_with_params(
+            governor.contract_address, voter, proposal_id, support, quorum, @reason, params
+        );
+}
+
+#[test]
+fn test_cast_vote_with_reason_and_params_by_sig_empty_params() {
+    let reason = "proposal reason";
+    let params = array![].span();
+
+    let (governor, r, s, proposal_id, support, voter, quorum) =
+        prepare_governor_and_signature_with_reason_and_params(
+        @reason, params, 0
+    );
+
+    // Set up event spy and cast vote
+    let mut spy = spy_events();
+    governor
+        .cast_vote_with_reason_and_params_by_sig(
+            proposal_id, support, voter, reason.clone(), params, array![r, s].span()
+        );
+
+    spy
+        .assert_only_event_vote_cast(
+            governor.contract_address, voter, proposal_id, support, quorum, @reason
+        );
+}
+
+#[test]
+#[should_panic(expected: 'Invalid signature')]
+fn test_cast_vote_with_reason_and_params_by_sig_invalid_signature() {
+    let reason = "proposal reason";
+    let params = array!['param'].span();
+
+    let (governor, r, s, proposal_id, support, voter, _) =
+        prepare_governor_and_signature_with_reason_and_params(
+        @reason, params, 0
+    );
+
+    // Cast vote with invalid signature
+    governor
+        .cast_vote_with_reason_and_params_by_sig(
+            proposal_id, support, voter, reason.clone(), params, array![r + 1, s].span()
+        );
+}
+
+#[test]
+#[should_panic(expected: 'Invalid signature')]
+fn test_cast_vote_with_reason_and_params_by_sig_invalid_msg_hash() {
+    let reason = "proposal reason";
+    let params = array!['param'].span();
+
+    // Use invalid nonce (not the account's current nonce)
+    let invalid_nonce = 1;
+    let (governor, r, s, proposal_id, support, voter, _) =
+        prepare_governor_and_signature_with_reason_and_params(
+        @reason, params, invalid_nonce
+    );
+
+    // Cast vote with invalid msg hash
+    governor
+        .cast_vote_with_reason_and_params_by_sig(
+            proposal_id, support, voter, reason.clone(), params, array![r, s].span()
+        );
+}
+
+#[test]
+fn test_cast_vote_with_reason_and_params_by_sig_hash_generation() {
+    start_cheat_chain_id_global('SN_TEST');
+
+    let verifying_contract = contract_address_const::<'VERIFIER'>();
+    let nonce = 0;
+    let proposal_id = 1;
+    let support = 1;
+    let voter = contract_address_const::<'VOTER'>();
+    let reason_hash = 'hash';
+    let params = array!['param'].span();
+    let vote = VoteWithReasonAndParams {
+        verifying_contract, nonce, proposal_id, support, voter, reason_hash, params
+    };
+    let hash = vote.get_message_hash(voter);
+
+    // This hash was computed using starknet js sdk from the following values:
+    // - name: 'DAPP_NAME'
+    // - version: 'DAPP_VERSION'
+    // - chainId: 'SN_TEST'
+    // - account: 'VOTER'
+    // - nonce: 0
+    // - verifying_contract: 'VERIFIER'
+    // - proposal_id: 1
+    // - support: 1
+    // - voter: 'VOTER'
+    // - reason_hash: 'hash'
+    // - params: ['param']
+    // - revision: '1'
+    let expected_hash = 0x729b7bd36fcddae615f7e2d7c78270e7f820f0dec9faf7842e0187670d3e84a;
+    assert_eq!(hash, expected_hash);
+}
+
+//
+// nonces
+//
+
+#[test]
+fn test_nonces() {
+    let mut state = COMPONENT_STATE();
+    let nonce = state.nonces(OTHER());
+    assert_eq!(nonce, 0);
+
+    state.Governor_nonces.write(OTHER(), 1);
+    let nonce = state.nonces(OTHER());
+    assert_eq!(nonce, 1);
+}
+
+//
+// relay
+//
+
+#[test]
+fn test_relay() {
+    let (mut governor, target) = setup_dispatchers();
+    let new_number = 1;
+    let contract_address = governor.contract_address;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("set_number"),
+        calldata: array![new_number].span()
+    };
+
+    let number = target.get_number();
+    assert_eq!(number, 0);
+
+    start_cheat_caller_address(contract_address, contract_address);
+    governor.relay(call);
+
+    let number = target.get_number();
+    assert_eq!(number, new_number);
+}
+
+#[test]
+#[should_panic(expected: 'Expected failure')]
+fn test_relay_panics() {
+    let (mut governor, target) = setup_dispatchers();
+    let contract_address = governor.contract_address;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("failing_function"),
+        calldata: array![].span()
+    };
+
+    start_cheat_caller_address(contract_address, contract_address);
+    governor.relay(call);
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_relay_invalid_caller() {
+    let mut state = COMPONENT_STATE();
+    let call = Call { to: ADMIN(), selector: selector!("foo"), calldata: array![].span() };
+
+    start_cheat_caller_address(test_address(), OTHER());
+    state.relay(call);
+}
+
+//
+// Internal
+//
+
+#[test]
+fn test_initializer() {
+    let mut state = COMPONENT_STATE();
+    let contract_state = CONTRACT_STATE();
+
+    state.initializer();
+
+    assert!(contract_state.supports_interface(IGOVERNOR_ID));
+}
+
+//
+// get_proposal
+//
+
+#[test]
+fn test_get_empty_proposal() {
+    let mut state = COMPONENT_STATE();
+
+    let proposal = state.get_proposal(0);
+
+    assert_eq!(proposal.proposer, ZERO());
+    assert_eq!(proposal.vote_start, 0);
+    assert_eq!(proposal.vote_duration, 0);
+    assert_eq!(proposal.executed, false);
+    assert_eq!(proposal.canceled, false);
+    assert_eq!(proposal.eta_seconds, 0);
+}
+
+#[test]
+fn test_get_proposal() {
+    let mut state = COMPONENT_STATE();
+    let (_, expected_proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(1, expected_proposal);
+
+    let proposal = state.get_proposal(1);
+    assert_eq!(proposal, expected_proposal);
+}
+
+//
+// is_valid_description_for_proposer
+//
+
+#[test]
+fn test_is_valid_description_too_short() {
+    let state = COMPONENT_STATE();
+    let short_description: ByteArray =
+        "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
+    assert_eq!(short_description.len(), 75);
+
+    let is_valid = state.is_valid_description_for_proposer(ADMIN(), @short_description);
+    assert!(is_valid);
+}
+
+#[test]
+fn test_is_valid_description_wrong_suffix() {
+    let state = COMPONENT_STATE();
+    let description =
+        "?proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
+
+    let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description);
+    assert!(is_valid);
+}
+
+#[test]
+fn test_is_valid_description_wrong_proposer() {
+    let state = COMPONENT_STATE();
+    let description =
+        "#proposer=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d";
+
+    let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description);
+    assert!(!is_valid);
+}
+
+#[test]
+fn test_is_valid_description_valid_proposer() {
+    let state = COMPONENT_STATE();
+    let address = ADMIN().to_byte_array(16, 64);
+    let mut description: ByteArray = "#proposer=0x";
+
+    description.append(@address);
+
+    let is_valid = state.is_valid_description_for_proposer(ADMIN(), @description);
+    assert!(is_valid);
+}
+
+//
+// _hash_proposal
+//
+
+#[test]
+fn test__hash_proposal() {
+    let state = COMPONENT_STATE();
+    let calls = get_calls(ZERO(), false);
+    let description = @"proposal description";
+    let description_hash = description.hash();
+
+    let expected_hash = hash_proposal(calls, description_hash);
+    let hash = state._hash_proposal(calls, description_hash);
+
+    assert_eq!(hash, expected_hash);
+}
+
+//
+// Proposal info
+//
+
+#[test]
+fn test__proposal_threshold() {
+    let mut state = COMPONENT_STATE();
+
+    let threshold = state._proposal_threshold();
+    let expected = GovernorMock::PROPOSAL_THRESHOLD;
+    assert_eq!(threshold, expected);
+}
+
+#[test]
+fn test__proposal_snapshot() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let snapshot = state._proposal_snapshot(id);
+    let expected = proposal.vote_start;
+    assert_eq!(snapshot, expected);
+}
+
+#[test]
+fn test__proposal_deadline() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let deadline = state._proposal_deadline(id);
+    let expected = proposal.vote_start + proposal.vote_duration;
+    assert_eq!(deadline, expected);
+}
+
+#[test]
+fn test__proposal_proposer() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let proposer = state._proposal_proposer(id);
+    let expected = proposal.proposer;
+    assert_eq!(proposer, expected);
+}
+
+#[test]
+fn test__proposal_eta() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let eta = state._proposal_eta(id);
+    let expected = proposal.eta_seconds;
+    assert_eq!(eta, expected);
+}
+
+//
+// assert_only_governance
+//
+
+#[test]
+fn test_assert_only_governance() {
+    let mut state = COMPONENT_STATE();
+    let contract_address = test_address();
+
+    start_cheat_caller_address(contract_address, contract_address);
+
+    state.assert_only_governance();
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_assert_only_governance_not_executor() {
+    let mut state = COMPONENT_STATE();
+    let contract_address = test_address();
+
+    start_cheat_caller_address(contract_address, OTHER());
+
+    state.assert_only_governance();
+}
+
+//
+// validate_state
+//
+
+#[test]
+fn test_validate_state() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    // Current should be Pending
+    let current_state = state._state(id);
+    assert_eq!(current_state, ProposalState::Pending);
+
+    let valid_states = array![ProposalState::Pending];
+    state.validate_state(id, valid_states.span());
+
+    let valid_states = array![ProposalState::Pending, ProposalState::Active];
+    state.validate_state(id, valid_states.span());
+
+    let valid_states = array![
+        ProposalState::Executed, ProposalState::Active, ProposalState::Pending
+    ];
+    state.validate_state(id, valid_states.span());
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_validate_state_invalid() {
+    let mut state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    // Current should be Pending
+    let current_state = state._state(id);
+    assert_eq!(current_state, ProposalState::Pending);
+
+    let valid_states = array![ProposalState::Active].span();
+    state.validate_state(id, valid_states);
+}
+
+//
+// _get_votes
+//
+
+#[test]
+fn test__get_votes() {
+    let mut state = COMPONENT_STATE();
+    let timepoint = 0;
+    let expected_weight = 100;
+    let params = array!['param'].span();
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    let votes = state._get_votes(OTHER(), timepoint, params);
+    assert_eq!(votes, expected_weight);
+}
+
+//
+// _state
+//
+
+#[test]
+fn test__state_executed() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_executed_proposal(ref state, false);
+}
+
+#[test]
+fn test__state_canceled() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_canceled_proposal(ref state, false);
+}
+
+#[test]
+#[should_panic(expected: 'Nonexistent proposal')]
+fn test__state_non_existent() {
+    let state = COMPONENT_STATE();
+
+    state._state(1);
+}
+
+#[test]
+fn test__state_pending() {
+    let mut state = COMPONENT_STATE();
+
+    // The function already asserts the state
+    setup_pending_proposal(ref state, false);
+}
+
+#[test]
+fn test__state_active() {
+    test_state_active_external_version(false);
+}
+
+#[test]
+fn test__state_defeated_quorum_not_reached() {
+    test_state_defeated_quorum_not_reached_external_version(false);
+}
+
+#[test]
+fn test__state_defeated_vote_not_succeeded() {
+    test_state_defeated_vote_not_succeeded_external_version(false);
+}
+
+#[test]
+fn test__state_queued() {
+    let mut mock_state = CONTRACT_STATE();
+
+    // The function already asserts the state
+    setup_queued_proposal(ref mock_state, false);
+}
+
+#[test]
+fn test__state_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+
+    // The function already asserts the state
+    setup_succeeded_proposal(ref mock_state, false);
+}
+
+//
+// _propose
+//
+
+#[test]
+fn test__propose() {
+    test_propose_external_version(false);
+}
+
+#[test]
+#[should_panic(expected: 'Existent proposal')]
+fn test__propose_existent_proposal() {
+    let mut state = COMPONENT_STATE();
+    let calls = get_calls(OTHER(), false);
+    let description = @"proposal description";
+    let proposer = ADMIN();
+
+    let id = state._propose(calls, description, proposer);
+    let expected_id = hash_proposal(calls, description.hash());
+    assert_eq!(id, expected_id);
+
+    // Propose again
+    state._propose(calls, description, proposer);
+}
+
+//
+// _cancel
+//
+
+#[test]
+fn test__cancel_pending() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref state, false);
+
+    state._cancel(id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test__cancel_active() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+
+    state._cancel(id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test__cancel_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+
+    mock_state.governor._cancel(id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test__cancel_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    mock_state.governor._cancel(id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test__cancel_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    mock_state.governor._cancel(id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cancel_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+
+    // Cancel again
+    state._cancel(id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cancel_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+
+    state._cancel(id, 0);
+}
+
+//
+// _cast_vote
+//
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_pending() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref state, false);
+    let params = array![].span();
+
+    state._cast_vote(id, OTHER(), 0, "", params);
+}
+
+#[test]
+fn test__cast_vote_active_no_params() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let reason = "reason";
+    let params = array![].span();
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    let weight = state._cast_vote(id, OTHER(), 0, reason, params);
+    assert_eq!(weight, expected_weight);
+
+    spy.assert_only_event_vote_cast(contract_address, OTHER(), id, 0, expected_weight, @"reason");
+}
+
+#[test]
+fn test__cast_vote_active_with_params() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    let reason = "reason";
+    let params = array!['param'].span();
+    let expected_weight = 100;
+
+    // Mock the get_past_votes call
+    start_mock_call(Zero::zero(), selector!("get_past_votes"), expected_weight);
+
+    let weight = state._cast_vote(id, OTHER(), 0, reason, params);
+    assert_eq!(weight, expected_weight);
+
+    spy
+        .assert_event_vote_cast_with_params(
+            contract_address, OTHER(), id, 0, expected_weight, @"reason", params
+        );
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+    let params = array![].span();
+
+    mock_state.governor._cast_vote(id, OTHER(), 0, "", params);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+    let params = array![].span();
+
+    mock_state.governor._cast_vote(id, OTHER(), 0, "", params);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+    let params = array![].span();
+
+    mock_state.governor._cast_vote(id, OTHER(), 0, "", params);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+    let params = array![].span();
+
+    state._cast_vote(id, OTHER(), 0, "", params);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test__cast_vote_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+    let params = array![].span();
+
+    state._cast_vote(id, OTHER(), 0, "", params);
+}
+
+//
+// Event helpers
+//
+
+#[generate_trait]
+pub(crate) impl GovernorSpyHelpersImpl of GovernorSpyHelpers {
+    fn assert_event_proposal_created(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        proposal_id: felt252,
+        proposer: ContractAddress,
+        calls: Span<Call>,
+        signatures: Span<Span<felt252>>,
+        vote_start: u64,
+        vote_end: u64,
+        description: @ByteArray
+    ) {
+        let expected = GovernorComponent::Event::ProposalCreated(
+            GovernorComponent::ProposalCreated {
+                proposal_id,
+                proposer,
+                calls,
+                signatures,
+                vote_start,
+                vote_end,
+                description: description.clone()
+            }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_proposal_created(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        proposal_id: felt252,
+        proposer: ContractAddress,
+        calls: Span<Call>,
+        signatures: Span<Span<felt252>>,
+        vote_start: u64,
+        vote_end: u64,
+        description: @ByteArray
+    ) {
+        self
+            .assert_event_proposal_created(
+                contract,
+                proposal_id,
+                proposer,
+                calls,
+                signatures,
+                vote_start,
+                vote_end,
+                description
+            );
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_vote_cast(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        voter: ContractAddress,
+        proposal_id: felt252,
+        support: u8,
+        weight: u256,
+        reason: @ByteArray
+    ) {
+        let expected = GovernorComponent::Event::VoteCast(
+            GovernorComponent::VoteCast {
+                voter, proposal_id, support, weight, reason: reason.clone()
+            }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_vote_cast(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        voter: ContractAddress,
+        proposal_id: felt252,
+        support: u8,
+        weight: u256,
+        reason: @ByteArray
+    ) {
+        self.assert_event_vote_cast(contract, voter, proposal_id, support, weight, reason);
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_vote_cast_with_params(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        voter: ContractAddress,
+        proposal_id: felt252,
+        support: u8,
+        weight: u256,
+        reason: @ByteArray,
+        params: Span<felt252>
+    ) {
+        let expected = GovernorComponent::Event::VoteCastWithParams(
+            GovernorComponent::VoteCastWithParams {
+                voter, proposal_id, support, weight, reason: reason.clone(), params
+            }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_vote_cast_with_params(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        voter: ContractAddress,
+        proposal_id: felt252,
+        support: u8,
+        weight: u256,
+        reason: @ByteArray,
+        params: Span<felt252>
+    ) {
+        self
+            .assert_event_vote_cast_with_params(
+                contract, voter, proposal_id, support, weight, reason, params
+            );
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_proposal_queued(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252, eta_seconds: u64
+    ) {
+        let expected = GovernorComponent::Event::ProposalQueued(
+            GovernorComponent::ProposalQueued { proposal_id, eta_seconds }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_proposal_queued(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252, eta_seconds: u64
+    ) {
+        self.assert_event_proposal_queued(contract, proposal_id, eta_seconds);
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_proposal_executed(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252
+    ) {
+        let expected = GovernorComponent::Event::ProposalExecuted(
+            GovernorComponent::ProposalExecuted { proposal_id }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_proposal_executed(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252
+    ) {
+        self.assert_event_proposal_executed(contract, proposal_id);
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_proposal_canceled(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252
+    ) {
+        let expected = GovernorComponent::Event::ProposalCanceled(
+            GovernorComponent::ProposalCanceled { proposal_id }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_proposal_canceled(
+        ref self: EventSpy, contract: ContractAddress, proposal_id: felt252
+    ) {
+        self.assert_event_proposal_canceled(contract, proposal_id);
+        self.assert_no_events_left_from(contract);
+    }
+}
diff --git a/packages/governance/src/tests/governor/test_governor_core_execution.cairo b/packages/governance/src/tests/governor/test_governor_core_execution.cairo
new file mode 100644
index 000000000..6b305dc8c
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_core_execution.cairo
@@ -0,0 +1,283 @@
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::InternalImpl;
+use crate::governor::extensions::GovernorCoreExecutionComponent::GovernorExecution;
+use crate::governor::interface::{IGovernor, ProposalState};
+use crate::tests::governor::common::{
+    setup_pending_proposal, setup_active_proposal, setup_defeated_proposal, setup_queued_proposal,
+    setup_canceled_proposal, setup_succeeded_proposal, setup_executed_proposal
+};
+use crate::tests::governor::common::{get_proposal_info, get_calls, COMPONENT_STATE, CONTRACT_STATE};
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_testing::constants::OTHER;
+use snforge_std::start_cheat_block_timestamp_global;
+use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess};
+
+//
+// state
+//
+
+#[test]
+fn test_state_executed() {
+    let mut component_state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref component_state, false);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Executed);
+}
+
+#[test]
+fn test_state_canceled() {
+    let mut component_state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref component_state, false);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Canceled);
+}
+
+#[test]
+#[should_panic(expected: 'Nonexistent proposal')]
+fn test_state_non_existent() {
+    let component_state = COMPONENT_STATE();
+
+    GovernorExecution::state(@component_state, 1);
+}
+
+#[test]
+fn test_state_pending() {
+    let mut component_state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref component_state, false);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Pending);
+}
+
+#[test]
+fn test_state_active() {
+    let mut component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    component_state.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Active;
+
+    // Is active before deadline
+    start_cheat_block_timestamp_global(deadline - 1);
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+
+    // Is active in deadline
+    start_cheat_block_timestamp_global(deadline);
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_defeated_quorum_not_reached() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum not reached
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum - 1);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_defeated_vote_not_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum reached
+    let quorum = mock_state.governor.quorum(0);
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote not succeeded
+    proposal_votes.against_votes.write(quorum + 1);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Queued);
+}
+
+#[test]
+fn test_state_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Succeeded);
+}
+
+//
+// executor
+//
+
+#[test]
+fn test_executor() {
+    let component_state = COMPONENT_STATE();
+    let expected = starknet::get_contract_address();
+
+    assert_eq!(GovernorExecution::executor(@component_state), expected);
+}
+
+//
+// execute_operations
+//
+
+#[test]
+fn test_execute_operations(id: felt252) {
+    let mut component_state = COMPONENT_STATE();
+    let calls = get_calls(OTHER(), true);
+    let description_hash = 'hash';
+
+    GovernorExecution::execute_operations(ref component_state, id, calls, description_hash);
+}
+
+#[test]
+#[should_panic(expected: "Contract not deployed at address: 0x4f54484552")]
+fn test_execute_operations_panics() {
+    let mut component_state = COMPONENT_STATE();
+    let id = 0;
+    let calls = get_calls(OTHER(), false);
+    let description_hash = 'hash';
+
+    GovernorExecution::execute_operations(ref component_state, id, calls, description_hash);
+}
+
+//
+// queue_operations
+//
+
+#[test]
+fn test_queue_operations(id: felt252) {
+    let mut component_state = COMPONENT_STATE();
+    let calls = array![].span();
+    let description_hash = 'hash';
+
+    let eta = GovernorExecution::queue_operations(ref component_state, id, calls, description_hash);
+
+    assert_eq!(eta, 0);
+}
+
+//
+// proposal_needs_queuing
+//
+
+#[test]
+fn test_proposal_needs_queuing(id: felt252) {
+    let component_state = COMPONENT_STATE();
+
+    assert_eq!(GovernorExecution::proposal_needs_queuing(@component_state, id), false);
+}
+
+//
+// cancel_operations
+//
+
+#[test]
+fn test_cancel_operations_pending() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_pending_proposal(ref state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_active() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_active_proposal(ref state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_defeated_proposal(ref mock_state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_succeeded_proposal(ref mock_state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_queued() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_queued_proposal(ref mock_state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_operations_canceled() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_canceled_proposal(ref state, false);
+
+    // Cancel again
+    GovernorExecution::cancel_operations(ref state, id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_operations_executed() {
+    let mut state = COMPONENT_STATE();
+    let (id, _) = setup_executed_proposal(ref state, false);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+}
diff --git a/packages/governance/src/tests/governor/test_governor_counting_simple.cairo b/packages/governance/src/tests/governor/test_governor_counting_simple.cairo
new file mode 100644
index 000000000..13a12a7dd
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_counting_simple.cairo
@@ -0,0 +1,313 @@
+use core::num::traits::Bounded;
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::InternalImpl;
+use crate::governor::extensions::GovernorCountingSimpleComponent::{VoteType, GovernorCounting};
+use crate::governor::interface::IGovernor;
+use crate::tests::governor::common::{COMPONENT_STATE, CONTRACT_STATE};
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_testing::constants::OTHER;
+use starknet::storage::{StorageMapReadAccess, StorageMapWriteAccess};
+use starknet::storage::{StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess};
+
+//
+// try_into
+//
+
+#[test]
+fn test_try_into_u8_VoteType() {
+    let number = 0_u8;
+    let vote_type: VoteType = number.try_into().unwrap();
+    assert_eq!(vote_type, VoteType::Against);
+
+    let number = 1_u8;
+    let vote_type: VoteType = number.try_into().unwrap();
+    assert_eq!(vote_type, VoteType::For);
+
+    let number = 2_u8;
+    let vote_type: VoteType = number.try_into().unwrap();
+    assert_eq!(vote_type, VoteType::Abstain);
+
+    let number = 3_u8;
+    let result: Option<VoteType> = number.try_into();
+    assert_eq!(result, Option::None);
+}
+
+//
+// into
+//
+
+#[test]
+fn test_into_VoteType_u8() {
+    let vote_type = VoteType::Against;
+    let number: u8 = vote_type.into();
+    assert_eq!(number, 0);
+
+    let vote_type = VoteType::For;
+    let number: u8 = vote_type.into();
+    assert_eq!(number, 1);
+
+    let vote_type = VoteType::Abstain;
+    let number: u8 = vote_type.into();
+    assert_eq!(number, 2);
+}
+
+//
+// counting_mode
+//
+
+#[test]
+fn test_counting_mode() {
+    let state = COMPONENT_STATE();
+    assert_eq!(GovernorCounting::counting_mode(@state), "support=bravo&quorum=for,abstain");
+}
+
+//
+// count_vote
+//
+
+#[test]
+fn test_count_vote_against() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+    let support = 0;
+    let total_weight = 100;
+    let params = array![].span();
+
+    let proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+    assert_eq!(proposal_votes.has_voted.read(account), false);
+    assert_eq!(proposal_votes.against_votes.read(), 0);
+
+    let weight = GovernorCounting::count_vote(
+        ref state, proposal_id, account, support, total_weight, params
+    );
+    assert_eq!(weight, total_weight);
+
+    assert_eq!(proposal_votes.has_voted.read(account), true);
+    assert_eq!(proposal_votes.against_votes.read(), total_weight);
+}
+
+#[test]
+fn test_count_vote_for() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+    let support = 1;
+    let total_weight = 100;
+    let params = array![].span();
+
+    let proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+    assert_eq!(proposal_votes.has_voted.read(account), false);
+    assert_eq!(proposal_votes.for_votes.read(), 0);
+
+    let weight = GovernorCounting::count_vote(
+        ref state, proposal_id, account, support, total_weight, params
+    );
+    assert_eq!(weight, total_weight);
+
+    assert_eq!(proposal_votes.has_voted.read(account), true);
+    assert_eq!(proposal_votes.for_votes.read(), total_weight);
+}
+
+#[test]
+fn test_count_vote_abstain() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+    let support = 2;
+    let total_weight = 100;
+    let params = array![].span();
+
+    let proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+    assert_eq!(proposal_votes.has_voted.read(account), false);
+    assert_eq!(proposal_votes.abstain_votes.read(), 0);
+
+    let weight = GovernorCounting::count_vote(
+        ref state, proposal_id, account, support, total_weight, params
+    );
+    assert_eq!(weight, total_weight);
+
+    assert_eq!(proposal_votes.has_voted.read(account), true);
+    assert_eq!(proposal_votes.abstain_votes.read(), total_weight);
+}
+
+#[test]
+#[should_panic(expected: 'Already cast vote')]
+fn test_count_vote_already_voted() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+    let support = 2;
+    let total_weight = 100;
+    let params = array![].span();
+
+    let proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+    proposal_votes.has_voted.write(account, true);
+
+    GovernorCounting::count_vote(ref state, proposal_id, account, support, total_weight, params);
+}
+
+#[test]
+#[should_panic(expected: 'Invalid vote type')]
+fn test_count_vote_invalid_vote_type() {
+    let mut state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+    let support = 3;
+    let total_weight = 100;
+    let params = array![].span();
+
+    GovernorCounting::count_vote(ref state, proposal_id, account, support, total_weight, params);
+}
+
+//
+// has_voted
+//
+
+#[test]
+fn test_has_voted() {
+    let mut mock_state = CONTRACT_STATE();
+    let state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+    let account = OTHER();
+
+    assert_eq!(GovernorCounting::has_voted(@state, proposal_id, account), false);
+
+    let proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+    proposal_votes.has_voted.write(account, true);
+
+    assert_eq!(GovernorCounting::has_voted(@state, proposal_id, account), true);
+}
+
+//
+// quorum_reached
+//
+
+#[test]
+fn test_quorum_reached() {
+    let mut mock_state = CONTRACT_STATE();
+    let state = COMPONENT_STATE();
+
+    let timepoint = 0;
+    let proposal_id = 0;
+
+    let quorum = state.quorum(timepoint);
+    let for_votes = quorum - 10;
+    let abstain_votes = 10;
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+
+    let mut proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+
+    // 1. Set for_votes and abstain_votes sum equal to quorum
+    proposal_votes.for_votes.write(for_votes);
+    proposal_votes.abstain_votes.write(abstain_votes);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), true);
+
+    // 2. Set for_votes and abstain_votes sum greater than quorum
+    proposal_votes.for_votes.write(for_votes + 1);
+    proposal_votes.abstain_votes.write(abstain_votes + 1);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), true);
+
+    // 3. Set for_votes and abstain_votes sum less than quorum
+    proposal_votes.for_votes.write(for_votes - 1);
+    proposal_votes.abstain_votes.write(abstain_votes - 1);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+}
+
+#[test]
+fn test_quorum_reached_snapshot_used() {
+    let mut mock_state = CONTRACT_STATE();
+    let state = COMPONENT_STATE();
+
+    let timepoint = 0;
+    let proposal_id = 0;
+
+    let quorum = state.quorum(timepoint);
+    let for_votes = quorum + 10;
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+
+    let mut proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+
+    // 1. Set for_votes greater than quorum
+    proposal_votes.for_votes.write(for_votes);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), true);
+
+    // 2. Set proposal snapshot to a special timepoint
+    let mut proposal = mock_state.governor.Governor_proposals.read(proposal_id);
+    proposal.vote_start = Bounded::MAX;
+    mock_state.governor.Governor_proposals.write(proposal_id, proposal);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+}
+
+//
+// vote_succeeded
+//
+
+#[test]
+fn test_vote_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let state = COMPONENT_STATE();
+
+    let proposal_id = 0;
+
+    let mut proposal_votes = mock_state
+        .governor_counting_simple
+        .Governor_proposals_votes
+        .entry(proposal_id);
+
+    // 1. Set for_votes greater than against_votes
+    proposal_votes.for_votes.write(500);
+    proposal_votes.against_votes.write(499);
+
+    assert_eq!(GovernorCounting::vote_succeeded(@state, proposal_id), true);
+
+    // 2. Set for_votes less than against_votes
+    proposal_votes.for_votes.write(499);
+    proposal_votes.against_votes.write(500);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+
+    // 3. Set for_votes equal to against_votes
+    proposal_votes.for_votes.write(500);
+    proposal_votes.abstain_votes.write(500);
+
+    assert_eq!(GovernorCounting::quorum_reached(@state, proposal_id), false);
+}
diff --git a/packages/governance/src/tests/governor/test_governor_settings.cairo b/packages/governance/src/tests/governor/test_governor_settings.cairo
new file mode 100644
index 000000000..309f8ec13
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_settings.cairo
@@ -0,0 +1,461 @@
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::InternalImpl;
+use crate::governor::extensions::GovernorSettingsComponent::InternalImpl as GovernorSettingsInternalImpl;
+use crate::governor::extensions::GovernorSettingsComponent::{
+    GovernorSettings, GovernorSettingsAdminImpl
+};
+use crate::governor::extensions::GovernorSettingsComponent;
+use crate::tests::governor::common::set_executor;
+use crate::tests::governor::common::{
+    COMPONENT_STATE_TIMELOCKED as COMPONENT_STATE, CONTRACT_STATE_TIMELOCKED as CONTRACT_STATE
+};
+use openzeppelin_test_common::mocks::governor::GovernorTimelockedMock::SNIP12MetadataImpl;
+use openzeppelin_testing::constants::OTHER;
+use openzeppelin_testing::events::EventSpyExt;
+use snforge_std::start_cheat_caller_address;
+use snforge_std::{EventSpy, spy_events, test_address};
+use starknet::ContractAddress;
+use starknet::storage::StoragePointerWriteAccess;
+
+//
+// Extensions
+//
+
+#[test]
+fn test_voting_delay() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let expected = 15;
+
+    assert_eq!(GovernorSettings::voting_delay(component_state), 0);
+    mock_state.governor_settings.Governor_voting_delay.write(expected);
+    assert_eq!(GovernorSettings::voting_delay(component_state), expected);
+}
+
+#[test]
+fn test_voting_period() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let expected = 45;
+
+    assert_eq!(GovernorSettings::voting_period(component_state), 0);
+    mock_state.governor_settings.Governor_voting_period.write(expected);
+    assert_eq!(GovernorSettings::voting_period(component_state), expected);
+}
+
+#[test]
+fn test_proposal_threshold() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let expected = 100;
+
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), 0);
+    mock_state.governor_settings.Governor_proposal_threshold.write(expected);
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), expected);
+}
+
+//
+// External
+//
+
+//
+// set_voting_delay
+//
+
+#[test]
+fn test_set_voting_delay() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::voting_delay(component_state), 0);
+    mock_state.governor_settings.set_voting_delay(expected);
+    assert_eq!(GovernorSettings::voting_delay(component_state), expected);
+
+    spy.assert_only_event_voting_delay_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test_set_voting_delay_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_voting_delay.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings.set_voting_delay(expected);
+    assert_eq!(GovernorSettings::voting_delay(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_set_voting_delay_only_governance() {
+    let mut mock_state = CONTRACT_STATE();
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+
+    mock_state.governor_settings.set_voting_delay(expected);
+}
+
+//
+// set_voting_period
+//
+
+#[test]
+fn test_set_voting_period() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::voting_period(component_state), 0);
+    mock_state.governor_settings.set_voting_period(expected);
+    assert_eq!(GovernorSettings::voting_period(component_state), expected);
+
+    spy.assert_only_event_voting_period_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test_set_voting_period_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_voting_period.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings.set_voting_period(expected);
+    assert_eq!(GovernorSettings::voting_period(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_set_voting_period_only_governance() {
+    let mut mock_state = CONTRACT_STATE();
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+
+    mock_state.governor_settings.set_voting_period(expected);
+}
+
+//
+// set_proposal_threshold
+//
+
+#[test]
+fn test_set_proposal_threshold() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), 0);
+    mock_state.governor_settings.set_proposal_threshold(expected);
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), expected);
+
+    spy.assert_only_event_proposal_threshold_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test_set_proposal_threshold_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_proposal_threshold.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings.set_proposal_threshold(expected);
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_set_proposal_threshold_only_governance() {
+    let mut mock_state = CONTRACT_STATE();
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+
+    mock_state.governor_settings.set_proposal_threshold(expected);
+}
+
+//
+// Internal
+//
+
+//
+// initializer
+//
+
+#[test]
+fn test_initializer() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    mock_state.governor_settings.initializer(15, 45, 100);
+
+    assert_eq!(GovernorSettings::voting_delay(component_state), 15);
+    spy.assert_event_voting_delay_updated(test_address(), 0, 15);
+
+    assert_eq!(GovernorSettings::voting_period(component_state), 45);
+    spy.assert_event_voting_period_updated(test_address(), 0, 45);
+
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), 100);
+    spy.assert_only_event_proposal_threshold_updated(test_address(), 0, 100);
+}
+
+//
+// assert_only_governance
+//
+
+#[test]
+fn test_assert_only_governance() {
+    let mut mock_state = CONTRACT_STATE();
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings.assert_only_governance();
+}
+
+#[test]
+#[should_panic(expected: 'Executor only')]
+fn test_assert_only_governance_not_executor() {
+    let mut mock_state = CONTRACT_STATE();
+
+    set_executor(ref mock_state, OTHER());
+
+    mock_state.governor_settings.assert_only_governance();
+}
+
+
+//
+// _set_voting_delay
+//
+
+#[test]
+fn test__set_voting_delay() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::voting_delay(component_state), 0);
+    mock_state.governor_settings._set_voting_delay(expected);
+    assert_eq!(GovernorSettings::voting_delay(component_state), expected);
+
+    spy.assert_only_event_voting_delay_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test__set_voting_delay_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_voting_delay.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings._set_voting_delay(expected);
+    assert_eq!(GovernorSettings::voting_delay(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+//
+// _set_voting_period
+//
+
+#[test]
+fn test__set_voting_period() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::voting_period(component_state), 0);
+    mock_state.governor_settings._set_voting_period(expected);
+    assert_eq!(GovernorSettings::voting_period(component_state), expected);
+
+    spy.assert_only_event_voting_period_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test__set_voting_period_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_voting_period.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings._set_voting_period(expected);
+    assert_eq!(GovernorSettings::voting_period(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+//
+// _set_proposal_threshold
+//
+
+#[test]
+fn test__set_proposal_threshold() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let contract_address = test_address();
+    let mut spy = spy_events();
+
+    let expected = 15;
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(contract_address, OTHER());
+
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), 0);
+    mock_state.governor_settings._set_proposal_threshold(expected);
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), expected);
+
+    spy.assert_only_event_proposal_threshold_updated(contract_address, 0, expected);
+}
+
+#[test]
+fn test__set_proposal_threshold_no_change() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = @COMPONENT_STATE();
+    let mut spy = spy_events();
+
+    let expected = 15;
+    mock_state.governor_settings.Governor_proposal_threshold.write(expected);
+
+    set_executor(ref mock_state, OTHER());
+    start_cheat_caller_address(test_address(), OTHER());
+
+    mock_state.governor_settings._set_proposal_threshold(expected);
+    assert_eq!(GovernorSettings::proposal_threshold(component_state), expected);
+
+    spy.assert_no_events_left();
+}
+
+//
+// Event helpers
+//
+
+#[generate_trait]
+pub(crate) impl GovernorSettingsSpyHelpersImpl of GovernorSettingsSpyHelpers {
+    fn assert_event_voting_delay_updated(
+        ref self: EventSpy, contract: ContractAddress, old_voting_delay: u64, new_voting_delay: u64
+    ) {
+        let expected = GovernorSettingsComponent::Event::VotingDelayUpdated(
+            GovernorSettingsComponent::VotingDelayUpdated { old_voting_delay, new_voting_delay }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_voting_delay_updated(
+        ref self: EventSpy, contract: ContractAddress, old_voting_delay: u64, new_voting_delay: u64
+    ) {
+        self.assert_event_voting_delay_updated(contract, old_voting_delay, new_voting_delay);
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_voting_period_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_voting_period: u64,
+        new_voting_period: u64
+    ) {
+        let expected = GovernorSettingsComponent::Event::VotingPeriodUpdated(
+            GovernorSettingsComponent::VotingPeriodUpdated { old_voting_period, new_voting_period }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_voting_period_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_voting_period: u64,
+        new_voting_period: u64
+    ) {
+        self.assert_event_voting_period_updated(contract, old_voting_period, new_voting_period);
+        self.assert_no_events_left_from(contract);
+    }
+
+    fn assert_event_proposal_threshold_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_proposal_threshold: u256,
+        new_proposal_threshold: u256
+    ) {
+        let expected = GovernorSettingsComponent::Event::ProposalThresholdUpdated(
+            GovernorSettingsComponent::ProposalThresholdUpdated {
+                old_proposal_threshold, new_proposal_threshold
+            }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_proposal_threshold_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_proposal_threshold: u256,
+        new_proposal_threshold: u256
+    ) {
+        self
+            .assert_event_proposal_threshold_updated(
+                contract, old_proposal_threshold, new_proposal_threshold
+            );
+        self.assert_no_events_left_from(contract);
+    }
+}
diff --git a/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo b/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo
new file mode 100644
index 000000000..b954e5e3c
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_timelock_execution.cairo
@@ -0,0 +1,745 @@
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::{InternalImpl, InternalExtendedImpl};
+use crate::governor::extensions::GovernorTimelockExecutionComponent::GovernorExecution;
+use crate::governor::extensions::GovernorTimelockExecutionComponent;
+use crate::governor::extensions::interface::{ITimelockedDispatcher, ITimelockedDispatcherTrait};
+use crate::governor::interface::ProposalState;
+use crate::governor::interface::{IGovernorDispatcher, IGovernorDispatcherTrait};
+use crate::tests::governor::common::{
+    COMPONENT_STATE_TIMELOCKED as COMPONENT_STATE, CONTRACT_STATE_TIMELOCKED as CONTRACT_STATE
+};
+use crate::tests::governor::common::{set_executor, get_proposal_info, ComponentStateTimelocked};
+use crate::tests::governor::test_governor::GovernorSpyHelpersImpl;
+use crate::tests::test_timelock::TimelockSpyHelpersImpl;
+use crate::timelock::interface::{OperationState, ITimelockDispatcher};
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_test_common::mocks::governor::{
+    GovernorTimelockedMock, CancelOperationsDispatcher, CancelOperationsDispatcherTrait
+};
+use openzeppelin_test_common::mocks::timelock::{
+    IMockContractDispatcher, IMockContractDispatcherTrait
+};
+use openzeppelin_testing as utils;
+use openzeppelin_testing::constants::{TIMELOCK, VOTES_TOKEN, OTHER};
+use openzeppelin_testing::events::EventSpyExt;
+use openzeppelin_utils::bytearray::ByteArrayExtTrait;
+use openzeppelin_utils::serde::SerializedAppend;
+use snforge_std::{start_cheat_block_timestamp_global, start_cheat_caller_address};
+use snforge_std::{start_mock_call, spy_events, store, EventSpy};
+use starknet::ContractAddress;
+use starknet::account::Call;
+use starknet::storage::{StoragePathEntry, StoragePointerWriteAccess, StorageMapWriteAccess};
+
+const MIN_DELAY: u64 = 100;
+
+//
+// Dispatchers
+//
+
+fn deploy_governor(timelock: ContractAddress) -> IGovernorDispatcher {
+    let mut calldata = array![];
+    calldata.append_serde(VOTES_TOKEN());
+    calldata.append_serde(timelock);
+
+    let address = utils::declare_and_deploy("GovernorTimelockedMock", calldata);
+    IGovernorDispatcher { contract_address: address }
+}
+
+fn deploy_timelock(admin: ContractAddress) -> ITimelockDispatcher {
+    let proposers = array![admin].span();
+    let executors = array![admin].span();
+
+    let mut calldata = array![];
+    calldata.append_serde(MIN_DELAY);
+    calldata.append_serde(proposers);
+    calldata.append_serde(executors);
+    calldata.append_serde(admin);
+
+    let address = utils::declare_and_deploy("TimelockControllerMock", calldata);
+    ITimelockDispatcher { contract_address: address }
+}
+
+fn deploy_mock_target() -> IMockContractDispatcher {
+    let mut calldata = array![];
+
+    let address = utils::declare_and_deploy("MockContract", calldata);
+    IMockContractDispatcher { contract_address: address }
+}
+
+fn setup_dispatchers() -> (IGovernorDispatcher, ITimelockDispatcher, IMockContractDispatcher) {
+    let governor = deploy_governor(TIMELOCK());
+    let timelock = deploy_timelock(governor.contract_address);
+    let target = deploy_mock_target();
+
+    // Set the timelock controller
+    store(
+        governor.contract_address,
+        selector!("Governor_timelock_controller"),
+        array![timelock.contract_address.into()].span()
+    );
+
+    (governor, timelock, target)
+}
+
+//
+// state
+//
+
+#[test]
+fn test_state_executed() {
+    let mut component_state = COMPONENT_STATE();
+    let id = setup_executed_proposal(ref component_state);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Executed);
+}
+
+#[test]
+fn test_state_canceled() {
+    let mut component_state = COMPONENT_STATE();
+    let id = setup_canceled_proposal(ref component_state);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Canceled);
+}
+
+#[test]
+#[should_panic(expected: 'Nonexistent proposal')]
+fn test_state_non_existent() {
+    let component_state = COMPONENT_STATE();
+
+    GovernorExecution::state(@component_state, 1);
+}
+
+#[test]
+fn test_state_pending() {
+    let mut component_state = COMPONENT_STATE();
+    let id = setup_pending_proposal(ref component_state);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Pending);
+}
+
+#[test]
+fn test_state_active() {
+    let mut component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    component_state.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Active;
+
+    // Is active before deadline
+    start_cheat_block_timestamp_global(deadline - 1);
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+
+    // Is active in deadline
+    start_cheat_block_timestamp_global(deadline);
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_defeated_quorum_not_reached() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum not reached
+    let quorum = GovernorTimelockedMock::QUORUM;
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum - 1);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_defeated_vote_not_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Defeated;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum reached
+    let quorum = GovernorTimelockedMock::QUORUM;
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote not succeeded
+    proposal_votes.against_votes.write(quorum + 1);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, expected);
+}
+
+#[test]
+fn test_state_queued_timelock_waiting() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let id = setup_queued_proposal(ref mock_state);
+
+    // 1. Mock the timelock to return pending
+    set_executor(ref mock_state, TIMELOCK());
+    start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Waiting);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Queued);
+}
+
+#[test]
+fn test_state_queued_timelock_ready() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let id = setup_queued_proposal(ref mock_state);
+
+    // 1. Mock the timelock to return pending
+    set_executor(ref mock_state, TIMELOCK());
+    start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Ready);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Queued);
+}
+
+#[test]
+fn test_state_queued_timelock_done() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let id = setup_queued_proposal(ref mock_state);
+
+    // 1. Mock the timelock to return pending
+    set_executor(ref mock_state, TIMELOCK());
+    start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Done);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Executed);
+}
+
+#[test]
+fn test_state_queued_timelock_canceled() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let id = setup_queued_proposal(ref mock_state);
+
+    // 1. Mock the timelock to return pending
+    set_executor(ref mock_state, TIMELOCK());
+    start_mock_call(TIMELOCK(), selector!("get_operation_state"), OperationState::Unset);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Canceled);
+}
+
+#[test]
+fn test_state_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let id = setup_succeeded_proposal(ref mock_state);
+
+    let state = GovernorExecution::state(@component_state, id);
+    assert_eq!(state, ProposalState::Succeeded);
+}
+
+//
+// executor
+//
+
+#[test]
+fn test_executor() {
+    let mut mock_state = CONTRACT_STATE();
+    let component_state = COMPONENT_STATE();
+    let expected = TIMELOCK();
+
+    set_executor(ref mock_state, expected);
+
+    assert_eq!(GovernorExecution::executor(@component_state), expected);
+}
+
+//
+// execute_operations
+//
+
+#[test]
+fn test_execute_operations() {
+    let (mut governor, timelock, target) = setup_dispatchers();
+    let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address };
+
+    let new_number = 125;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("set_number"),
+        calldata: array![new_number].span()
+    };
+    let calls = array![call].span();
+    let description = "proposal description";
+
+    let number = target.get_number();
+    assert_eq!(number, 0);
+
+    // 1. Mock the get_past_votes call
+    let quorum = GovernorTimelockedMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 2. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let id = governor.propose(calls, description.clone());
+
+    // 3. Cast vote
+
+    // Fast forward the vote delay
+    current_time += GovernorTimelockedMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // Cast vote
+    governor.cast_vote(id, 1);
+
+    // 4. Queue
+
+    // Fast forward the vote duration
+    current_time += (GovernorTimelockedMock::VOTING_PERIOD + 1);
+    start_cheat_block_timestamp_global(current_time);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Succeeded);
+
+    governor.queue(calls, (@description).hash());
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Queued);
+
+    let target_id = timelocked.get_timelock_id(id);
+
+    // 5. Execute
+    // Fast forward the timelock delay
+    current_time += MIN_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    let mut spy = spy_events();
+    governor.execute(calls, (@description).hash());
+
+    // 6. Assertions
+    let number = target.get_number();
+    assert_eq!(number, new_number);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Executed);
+
+    spy.assert_events_call_executed_batch(timelock.contract_address, target_id, calls);
+
+    let target_id = timelocked.get_timelock_id(id);
+    assert_eq!(target_id, 0);
+}
+
+//
+// queue_operations
+//
+
+#[test]
+fn test_queue_operations() {
+    let (mut governor, timelock, target) = setup_dispatchers();
+    let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address };
+
+    let new_number = 125;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("set_number"),
+        calldata: array![new_number].span()
+    };
+    let calls = array![call].span();
+    let description = "proposal description";
+
+    let number = target.get_number();
+    assert_eq!(number, 0);
+
+    // 1. Mock the get_past_votes call
+    let quorum = GovernorTimelockedMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 2. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let id = governor.propose(calls, description.clone());
+
+    // 3. Cast vote
+
+    // Fast forward the vote delay
+    current_time += GovernorTimelockedMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // Cast vote
+    governor.cast_vote(id, 1);
+
+    // 4. Queue
+
+    // Fast forward the vote duration
+    current_time += (GovernorTimelockedMock::VOTING_PERIOD + 1);
+    start_cheat_block_timestamp_global(current_time);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Succeeded);
+
+    let mut spy = spy_events();
+
+    governor.queue(calls, (@description).hash());
+
+    let target_id = timelocked.get_timelock_id(id);
+    let salt = timelock_salt(governor.contract_address, (@description).hash());
+    spy.assert_event_call_scheduled(timelock.contract_address, target_id, 0, call, 0, MIN_DELAY);
+    spy.assert_event_call_salt(timelock.contract_address, target_id, salt);
+    spy.assert_only_event_proposal_queued(governor.contract_address, id, current_time + MIN_DELAY);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Queued);
+}
+
+//
+// proposal_needs_queuing
+//
+
+#[test]
+fn test_proposal_needs_queuing(id: felt252) {
+    let component_state = COMPONENT_STATE();
+
+    assert_eq!(GovernorExecution::proposal_needs_queuing(@component_state, id), true);
+}
+
+//
+// cancel_operations
+//
+
+#[test]
+fn test_cancel_operations_queued() {
+    let (mut governor, timelock, target) = setup_dispatchers();
+    let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address };
+
+    let new_number = 125;
+
+    let call = Call {
+        to: target.contract_address,
+        selector: selector!("set_number"),
+        calldata: array![new_number].span()
+    };
+    let calls = array![call].span();
+    let description = "proposal description";
+
+    let number = target.get_number();
+    assert_eq!(number, 0);
+
+    // 1. Mock the get_past_votes call
+    let quorum = GovernorTimelockedMock::QUORUM;
+    start_mock_call(VOTES_TOKEN(), selector!("get_past_votes"), quorum);
+
+    // 2. Propose
+    let mut current_time = 10;
+    start_cheat_block_timestamp_global(current_time);
+    let id = governor.propose(calls, description.clone());
+
+    // 3. Cast vote
+
+    // Fast forward the vote delay
+    current_time += GovernorTimelockedMock::VOTING_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    // Cast vote
+    governor.cast_vote(id, 1);
+
+    // 4. Queue
+
+    // Fast forward the vote duration
+    current_time += (GovernorTimelockedMock::VOTING_PERIOD + 1);
+    start_cheat_block_timestamp_global(current_time);
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Succeeded);
+
+    governor.queue(calls, (@description).hash());
+
+    let state = governor.state(id);
+    assert_eq!(state, ProposalState::Queued);
+
+    let target_id = timelocked.get_timelock_id(id);
+
+    // 5. Cancel
+    // Fast forward the timelock delay
+    current_time += MIN_DELAY;
+    start_cheat_block_timestamp_global(current_time);
+
+    let mut spy = spy_events();
+    let dispatcher = CancelOperationsDispatcher { contract_address: governor.contract_address };
+    dispatcher.cancel_operations(id, (@description).hash());
+
+    spy.assert_event_call_cancelled(timelock.contract_address, target_id);
+
+    let target_id = timelocked.get_timelock_id(id);
+    assert_eq!(target_id, 0);
+}
+
+#[test]
+fn test_cancel_operations_pending() {
+    let mut state = COMPONENT_STATE();
+    let id = setup_pending_proposal(ref state);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_active() {
+    let mut state = COMPONENT_STATE();
+    let id = setup_active_proposal(ref state);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = state.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_defeated() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+    let id = setup_defeated_proposal(ref mock_state);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+fn test_cancel_operations_succeeded() {
+    let mut mock_state = CONTRACT_STATE();
+    let mut state = COMPONENT_STATE();
+    let id = setup_succeeded_proposal(ref mock_state);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+
+    let canceled_proposal = mock_state.governor.get_proposal(id);
+    assert_eq!(canceled_proposal.canceled, true);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_operations_canceled() {
+    let mut state = COMPONENT_STATE();
+    let id = setup_canceled_proposal(ref state);
+
+    // Cancel again
+    GovernorExecution::cancel_operations(ref state, id, 0);
+}
+
+#[test]
+#[should_panic(expected: 'Unexpected proposal state')]
+fn test_cancel_operations_executed() {
+    let mut state = COMPONENT_STATE();
+    let id = setup_executed_proposal(ref state);
+
+    GovernorExecution::cancel_operations(ref state, id, 0);
+}
+
+//
+// update_timelock
+//
+
+#[test]
+fn test_update_timelock() {
+    let mut governor = deploy_governor(TIMELOCK());
+    let timelocked = ITimelockedDispatcher { contract_address: governor.contract_address };
+
+    start_cheat_caller_address(governor.contract_address, TIMELOCK());
+
+    let mut spy = spy_events();
+    timelocked.update_timelock(OTHER());
+
+    assert_eq!(timelocked.timelock(), OTHER());
+
+    spy.assert_only_event_timelock_updated(governor.contract_address, TIMELOCK(), OTHER());
+}
+
+//
+// Helpers
+//
+
+fn timelock_salt(contract_address: ContractAddress, description_hash: felt252) -> felt252 {
+    let description_hash: u256 = description_hash.into();
+    let contract_address: felt252 = contract_address.into();
+
+    (contract_address.into() ^ description_hash).try_into().unwrap()
+}
+
+//
+// Setup proposals
+//
+
+pub fn setup_pending_proposal(ref state: ComponentStateTimelocked) -> felt252 {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let current_state = state._state(id);
+    let expected = ProposalState::Pending;
+
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_active_proposal(ref state: ComponentStateTimelocked) -> felt252 {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Active;
+
+    // Is active before deadline
+    start_cheat_block_timestamp_global(deadline - 1);
+    let current_state = state._state(id);
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_queued_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 {
+    let (id, mut proposal) = get_proposal_info();
+
+    proposal.eta_seconds = 1;
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+
+    // Quorum reached
+    start_cheat_block_timestamp_global(deadline + 1);
+    let quorum = GovernorTimelockedMock::QUORUM;
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote succeeded
+    proposal_votes.against_votes.write(quorum);
+
+    let expected = ProposalState::Queued;
+    let current_state = mock_state.governor._state(id);
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_canceled_proposal(ref state: ComponentStateTimelocked) -> felt252 {
+    let (id, proposal) = get_proposal_info();
+
+    state.Governor_proposals.write(id, proposal);
+
+    state._cancel(id, 0);
+
+    let expected = ProposalState::Canceled;
+    let current_state = state._state(id);
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_defeated_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 {
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+
+    // Quorum not reached
+    start_cheat_block_timestamp_global(deadline + 1);
+    let quorum = GovernorTimelockedMock::QUORUM;
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum - 1);
+
+    let expected = ProposalState::Defeated;
+    let current_state = mock_state.governor._state(id);
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_succeeded_proposal(ref mock_state: GovernorTimelockedMock::ContractState) -> felt252 {
+    let (id, proposal) = get_proposal_info();
+
+    mock_state.governor.Governor_proposals.write(id, proposal);
+
+    let deadline = proposal.vote_start + proposal.vote_duration;
+    let expected = ProposalState::Succeeded;
+
+    start_cheat_block_timestamp_global(deadline + 1);
+
+    // Quorum reached
+    let quorum = GovernorTimelockedMock::QUORUM;
+    let proposal_votes = mock_state.governor_counting_simple.Governor_proposals_votes.entry(id);
+    proposal_votes.for_votes.write(quorum + 1);
+
+    // Vote succeeded
+    proposal_votes.against_votes.write(quorum);
+
+    let current_state = mock_state.governor._state(id);
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+pub fn setup_executed_proposal(ref state: ComponentStateTimelocked) -> felt252 {
+    let (id, mut proposal) = get_proposal_info();
+
+    proposal.executed = true;
+    state.Governor_proposals.write(id, proposal);
+
+    let current_state = state._state(id);
+    let expected = ProposalState::Executed;
+
+    assert_eq!(current_state, expected);
+
+    id
+}
+
+//
+// Event helpers
+//
+
+#[generate_trait]
+pub(crate) impl GovernorTimelockExecutionSpyHelpersImpl of GovernorTimelockExecutionSpyHelpers {
+    fn assert_event_timelock_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_timelock: ContractAddress,
+        new_timelock: ContractAddress
+    ) {
+        let expected = GovernorTimelockExecutionComponent::Event::TimelockUpdated(
+            GovernorTimelockExecutionComponent::TimelockUpdated { old_timelock, new_timelock }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_timelock_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_timelock: ContractAddress,
+        new_timelock: ContractAddress
+    ) {
+        self.assert_event_timelock_updated(contract, old_timelock, new_timelock);
+        self.assert_no_events_left_from(contract);
+    }
+}
diff --git a/packages/governance/src/tests/governor/test_governor_votes.cairo b/packages/governance/src/tests/governor/test_governor_votes.cairo
new file mode 100644
index 000000000..5746793e0
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_votes.cairo
@@ -0,0 +1,78 @@
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::InternalImpl;
+use crate::governor::extensions::GovernorVotesComponent::{
+    GovernorVotes, VotesTokenImpl, InternalTrait
+};
+use crate::tests::governor::common::{COMPONENT_STATE, CONTRACT_STATE};
+use openzeppelin_test_common::mocks::governor::GovernorMock::SNIP12MetadataImpl;
+use openzeppelin_testing::constants::{VOTES_TOKEN, OTHER, ZERO};
+use snforge_std::{start_mock_call, start_cheat_block_timestamp_global};
+use snforge_std::{store, test_address};
+
+//
+// GovernorVotes
+//
+
+#[test]
+fn test_clock() {
+    let component_state = COMPONENT_STATE();
+    let timestamp = 10;
+
+    start_cheat_block_timestamp_global(timestamp);
+    let clock = GovernorVotes::clock(@component_state);
+    assert_eq!(clock, timestamp);
+}
+
+#[test]
+fn test_clock_mode() {
+    let component_state = COMPONENT_STATE();
+    let mode = GovernorVotes::clock_mode(@component_state);
+    assert_eq!(mode, "mode=timestamp&from=starknet::SN_MAIN");
+}
+
+#[test]
+fn test_get_votes() {
+    let mut component_state = COMPONENT_STATE();
+    let timepoint = 0;
+    let expected_weight = 100;
+    let params = array!['param'].span();
+
+    start_mock_call(ZERO(), selector!("get_past_votes"), expected_weight);
+
+    let votes = GovernorVotes::get_votes(@component_state, OTHER(), timepoint, params);
+    assert_eq!(votes, expected_weight);
+}
+
+//
+// External
+//
+
+#[test]
+fn test_token() {
+    let mock_state = CONTRACT_STATE();
+
+    store(test_address(), selector!("Governor_token"), array![VOTES_TOKEN().into()].span());
+    let token = mock_state.governor_votes.token();
+    assert_eq!(token, VOTES_TOKEN());
+}
+
+//
+// Internal
+//
+
+#[test]
+fn test_initializer() {
+    let mut mock_state = CONTRACT_STATE();
+
+    mock_state.governor_votes.initializer(VOTES_TOKEN());
+
+    let token = mock_state.governor_votes.token();
+    assert_eq!(token, VOTES_TOKEN());
+}
+
+#[test]
+#[should_panic(expected: 'Invalid votes token')]
+fn test_initializer_with_zero_token() {
+    let mut mock_state = CONTRACT_STATE();
+    mock_state.governor_votes.initializer(ZERO());
+}
diff --git a/packages/governance/src/tests/governor/test_governor_votes_quorum_fraction.cairo b/packages/governance/src/tests/governor/test_governor_votes_quorum_fraction.cairo
new file mode 100644
index 000000000..8b3189a69
--- /dev/null
+++ b/packages/governance/src/tests/governor/test_governor_votes_quorum_fraction.cairo
@@ -0,0 +1,232 @@
+use crate::governor::DefaultConfig;
+use crate::governor::GovernorComponent::InternalImpl;
+use crate::governor::GovernorComponent;
+use crate::governor::extensions::GovernorVotesQuorumFractionComponent::{
+    GovernorQuorum, GovernorVotes, QuorumFractionImpl, InternalTrait
+};
+use crate::governor::extensions::GovernorVotesQuorumFractionComponent;
+use openzeppelin_test_common::mocks::governor::GovernorQuorumFractionMock::SNIP12MetadataImpl;
+use openzeppelin_test_common::mocks::governor::GovernorQuorumFractionMock;
+use openzeppelin_testing::constants::{VOTES_TOKEN, OTHER, ZERO};
+use openzeppelin_testing::events::EventSpyExt;
+use snforge_std::{EventSpy, store, test_address, spy_events};
+use snforge_std::{start_mock_call, start_cheat_block_timestamp_global};
+use starknet::ContractAddress;
+
+pub type ComponentState =
+    GovernorComponent::ComponentState<GovernorQuorumFractionMock::ContractState>;
+
+pub fn CONTRACT_STATE() -> GovernorQuorumFractionMock::ContractState {
+    GovernorQuorumFractionMock::contract_state_for_testing()
+}
+
+pub fn COMPONENT_STATE() -> ComponentState {
+    GovernorComponent::component_state_for_testing()
+}
+
+//
+// GovernorQuorum
+//
+
+#[test]
+fn test_quorum() {
+    let component_state = COMPONENT_STATE();
+    let mock_state = CONTRACT_STATE();
+    let past_total_supply = 100;
+    let timepoint = 123;
+
+    start_mock_call(ZERO(), selector!("get_past_total_supply"), past_total_supply);
+
+    let quorum = GovernorQuorum::quorum(@component_state, timepoint);
+    let quorum_numerator = mock_state.governor_votes.quorum_numerator(timepoint);
+    let quorum_denominator = mock_state.governor_votes.quorum_denominator();
+
+    assert_eq!(quorum, quorum_numerator * past_total_supply / quorum_denominator);
+}
+
+//
+// GovernorVotes
+//
+
+#[test]
+fn test_clock() {
+    let component_state = COMPONENT_STATE();
+    let timestamp = 10;
+
+    start_cheat_block_timestamp_global(timestamp);
+    let clock = GovernorVotes::clock(@component_state);
+    assert_eq!(clock, timestamp);
+}
+
+#[test]
+fn test_clock_mode() {
+    let component_state = COMPONENT_STATE();
+    let mode = GovernorVotes::clock_mode(@component_state);
+    assert_eq!(mode, "mode=timestamp&from=starknet::SN_MAIN");
+}
+
+#[test]
+fn test_get_votes() {
+    let mut component_state = COMPONENT_STATE();
+    let timepoint = 0;
+    let expected_weight = 100;
+    let params = array!['param'].span();
+
+    start_mock_call(ZERO(), selector!("get_past_votes"), expected_weight);
+
+    let votes = GovernorVotes::get_votes(@component_state, OTHER(), timepoint, params);
+    assert_eq!(votes, expected_weight);
+}
+
+//
+// External
+//
+
+#[test]
+fn test_token() {
+    let mock_state = CONTRACT_STATE();
+
+    store(test_address(), selector!("Governor_token"), array![VOTES_TOKEN().into()].span());
+    let token = mock_state.governor_votes.token();
+    assert_eq!(token, VOTES_TOKEN());
+}
+
+#[test]
+fn test_quorum_denominator() {
+    let mock_state = CONTRACT_STATE();
+    let quorum_denominator = mock_state.governor_votes.quorum_denominator();
+    assert_eq!(quorum_denominator, 1000);
+}
+
+//
+// Internal
+//
+
+#[test]
+fn test_initializer() {
+    let mut mock_state = CONTRACT_STATE();
+    let now = starknet::get_block_timestamp();
+
+    mock_state.governor_votes.initializer(VOTES_TOKEN(), 600);
+
+    let quorum_numerator = mock_state.governor_votes.quorum_numerator(now);
+    assert_eq!(quorum_numerator, 600);
+
+    let token = mock_state.governor_votes.token();
+    assert_eq!(token, VOTES_TOKEN());
+}
+
+#[test]
+#[should_panic(expected: 'Invalid votes token')]
+fn test_initializer_with_zero_token() {
+    let mut mock_state = CONTRACT_STATE();
+    mock_state.governor_votes.initializer(ZERO(), 600);
+}
+
+
+#[test]
+#[should_panic(expected: 'Invalid quorum fraction')]
+fn test_initializer_with_invalid_numerator() {
+    let mut mock_state = CONTRACT_STATE();
+    mock_state.governor_votes.initializer(VOTES_TOKEN(), 1001);
+}
+
+//
+// update_quorum_numerator
+//
+
+#[test]
+#[should_panic(expected: 'Invalid quorum fraction')]
+fn test_update_quorum_numerator_invalid_numerator() {
+    let mut mock_state = CONTRACT_STATE();
+    mock_state.governor_votes.update_quorum_numerator(1001);
+}
+
+#[test]
+fn test_update_quorum_numerator() {
+    let mut mock_state = CONTRACT_STATE();
+    let ts1 = '10';
+    let ts2 = '20';
+    let ts3 = '30';
+    let ts4 = '15';
+    let ts5 = '35';
+    let new_quorum_numerator_1 = 700;
+    let new_quorum_numerator_2 = 800;
+    let new_quorum_numerator_3 = 900;
+
+    let mut spy = spy_events();
+    let contract_address = test_address();
+
+    // 1. Update the numerators
+    start_cheat_block_timestamp_global(ts1);
+    mock_state.governor_votes.update_quorum_numerator(new_quorum_numerator_1);
+    spy.assert_only_event_quorum_numerator_updated(contract_address, 0, new_quorum_numerator_1);
+
+    start_cheat_block_timestamp_global(ts2);
+    mock_state.governor_votes.update_quorum_numerator(new_quorum_numerator_2);
+    spy
+        .assert_only_event_quorum_numerator_updated(
+            contract_address, new_quorum_numerator_1, new_quorum_numerator_2
+        );
+
+    start_cheat_block_timestamp_global(ts3);
+    mock_state.governor_votes.update_quorum_numerator(new_quorum_numerator_3);
+    spy
+        .assert_only_event_quorum_numerator_updated(
+            contract_address, new_quorum_numerator_2, new_quorum_numerator_3
+        );
+
+    // 2. Check the current quorum numerator
+    let current_quorum_numerator = mock_state.governor_votes.current_quorum_numerator();
+    assert_eq!(current_quorum_numerator, new_quorum_numerator_3);
+
+    // 3. Check the history
+    let history = mock_state.governor_votes.quorum_numerator(ts1);
+    assert_eq!(history, new_quorum_numerator_1);
+
+    let history = mock_state.governor_votes.quorum_numerator(ts2);
+    assert_eq!(history, new_quorum_numerator_2);
+
+    let history = mock_state.governor_votes.quorum_numerator(ts3);
+    assert_eq!(history, new_quorum_numerator_3);
+
+    let history = mock_state.governor_votes.quorum_numerator(ts4);
+    assert_eq!(history, new_quorum_numerator_1);
+
+    let history = mock_state.governor_votes.quorum_numerator(ts5);
+    assert_eq!(history, new_quorum_numerator_3);
+}
+
+//
+// Event helpers
+//
+
+#[generate_trait]
+pub(crate) impl GovernorSettingsSpyHelpersImpl of GovernorSettingsSpyHelpers {
+    fn assert_event_quorum_numerator_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_quorum_numerator: u256,
+        new_quorum_numerator: u256
+    ) {
+        let expected = GovernorVotesQuorumFractionComponent::Event::QuorumNumeratorUpdated(
+            GovernorVotesQuorumFractionComponent::QuorumNumeratorUpdated {
+                old_quorum_numerator, new_quorum_numerator
+            }
+        );
+        self.assert_emitted_single(contract, expected);
+    }
+
+    fn assert_only_event_quorum_numerator_updated(
+        ref self: EventSpy,
+        contract: ContractAddress,
+        old_quorum_numerator: u256,
+        new_quorum_numerator: u256
+    ) {
+        self
+            .assert_event_quorum_numerator_updated(
+                contract, old_quorum_numerator, new_quorum_numerator
+            );
+        self.assert_no_events_left_from(contract);
+    }
+}
diff --git a/packages/test_common/src/mocks.cairo b/packages/test_common/src/mocks.cairo
index 2e6901947..e19179acc 100644
--- a/packages/test_common/src/mocks.cairo
+++ b/packages/test_common/src/mocks.cairo
@@ -5,6 +5,7 @@ pub mod erc1155;
 pub mod erc20;
 pub mod erc2981;
 pub mod erc721;
+pub mod governor;
 pub mod multisig;
 pub mod non_implementing;
 pub mod nonces;
@@ -16,4 +17,3 @@ pub mod timelock;
 pub mod upgrades;
 pub mod vesting;
 pub mod votes;
-
diff --git a/packages/test_common/src/mocks/governor.cairo b/packages/test_common/src/mocks/governor.cairo
new file mode 100644
index 000000000..e87743a70
--- /dev/null
+++ b/packages/test_common/src/mocks/governor.cairo
@@ -0,0 +1,408 @@
+#[starknet::contract]
+pub mod GovernorMock {
+    use core::num::traits::Bounded;
+    use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait;
+    use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait;
+    use openzeppelin_governance::governor::extensions::{
+        GovernorVotesComponent, GovernorCountingSimpleComponent, GovernorCoreExecutionComponent
+    };
+    use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
+    use starknet::ContractAddress;
+
+    pub const VOTING_DELAY: u64 = 86400; // 1 day
+    pub const VOTING_PERIOD: u64 = 432_000; // 1 week
+    pub const PROPOSAL_THRESHOLD: u256 = 10;
+    pub const QUORUM: u256 = 100_000_000;
+
+    component!(path: GovernorComponent, storage: governor, event: GovernorEvent);
+    component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent);
+    component!(
+        path: GovernorCountingSimpleComponent,
+        storage: governor_counting_simple,
+        event: GovernorCountingSimpleEvent
+    );
+    component!(
+        path: GovernorCoreExecutionComponent,
+        storage: governor_core_execution,
+        event: GovernorCoreExecutionEvent
+    );
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Governor
+    #[abi(embed_v0)]
+    impl GovernorImpl = GovernorComponent::GovernorImpl<ContractState>;
+
+    // Extensions external
+    #[abi(embed_v0)]
+    impl VotesTokenImpl = GovernorVotesComponent::VotesTokenImpl<ContractState>;
+
+    // Extensions internal
+    impl GovernorVotesImpl = GovernorVotesComponent::GovernorVotes<ContractState>;
+    impl GovernorCountingSimpleImpl =
+        GovernorCountingSimpleComponent::GovernorCounting<ContractState>;
+    impl GovernorCoreExecutionImpl =
+        GovernorCoreExecutionComponent::GovernorExecution<ContractState>;
+
+    // SRC5
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        pub governor: GovernorComponent::Storage,
+        #[substorage(v0)]
+        pub governor_votes: GovernorVotesComponent::Storage,
+        #[substorage(v0)]
+        pub governor_counting_simple: GovernorCountingSimpleComponent::Storage,
+        #[substorage(v0)]
+        pub governor_core_execution: GovernorCoreExecutionComponent::Storage,
+        #[substorage(v0)]
+        pub src5: SRC5Component::Storage,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        GovernorEvent: GovernorComponent::Event,
+        #[flat]
+        GovernorVotesEvent: GovernorVotesComponent::Event,
+        #[flat]
+        GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event,
+        #[flat]
+        GovernorCoreExecutionEvent: GovernorCoreExecutionComponent::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+    }
+
+    #[constructor]
+    fn constructor(ref self: ContractState, votes_token: ContractAddress) {
+        self.governor.initializer();
+        self.governor_votes.initializer(votes_token);
+    }
+
+    //
+    // SNIP12 Metadata
+    //
+
+    pub impl SNIP12MetadataImpl of SNIP12Metadata {
+        fn name() -> felt252 {
+            'DAPP_NAME'
+        }
+
+        fn version() -> felt252 {
+            'DAPP_VERSION'
+        }
+    }
+
+    //
+    // Locally implemented extensions
+    //
+
+    impl GovernorQuorum of GovernorComponent::GovernorQuorumTrait<ContractState> {
+        /// See `GovernorComponent::GovernorQuorumTrait::quorum`.
+        fn quorum(self: @GovernorComponent::ComponentState<ContractState>, timepoint: u64) -> u256 {
+            if timepoint == Bounded::MAX {
+                Bounded::MAX
+            } else {
+                QUORUM
+            }
+        }
+    }
+
+    pub impl GovernorSettings of GovernorComponent::GovernorSettingsTrait<ContractState> {
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`.
+        fn voting_delay(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
+            VOTING_DELAY
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_period`.
+        fn voting_period(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
+            VOTING_PERIOD
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`.
+        fn proposal_threshold(self: @GovernorComponent::ComponentState<ContractState>) -> u256 {
+            PROPOSAL_THRESHOLD
+        }
+    }
+}
+
+#[starknet::contract]
+pub mod GovernorQuorumFractionMock {
+    use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait;
+    use openzeppelin_governance::governor::extensions::GovernorVotesQuorumFractionComponent::InternalTrait;
+    use openzeppelin_governance::governor::extensions::{
+        GovernorVotesQuorumFractionComponent, GovernorCountingSimpleComponent,
+        GovernorCoreExecutionComponent,
+    };
+    use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
+    use starknet::ContractAddress;
+
+    pub const VOTING_DELAY: u64 = 86400; // 1 day
+    pub const VOTING_PERIOD: u64 = 432_000; // 1 week
+    pub const PROPOSAL_THRESHOLD: u256 = 10;
+    pub const QUORUM_NUMERATOR: u256 = 600; // 60%
+
+    component!(path: GovernorComponent, storage: governor, event: GovernorEvent);
+    component!(
+        path: GovernorVotesQuorumFractionComponent,
+        storage: governor_votes,
+        event: GovernorVotesEvent
+    );
+    component!(
+        path: GovernorCountingSimpleComponent,
+        storage: governor_counting_simple,
+        event: GovernorCountingSimpleEvent
+    );
+    component!(
+        path: GovernorCoreExecutionComponent,
+        storage: governor_core_execution,
+        event: GovernorCoreExecutionEvent
+    );
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Governor
+    #[abi(embed_v0)]
+    impl GovernorImpl = GovernorComponent::GovernorImpl<ContractState>;
+
+    // Extensions external
+    #[abi(embed_v0)]
+    impl QuorumFractionImpl =
+        GovernorVotesQuorumFractionComponent::QuorumFractionImpl<ContractState>;
+
+    // Extensions internal
+    impl GovernorQuorumImpl = GovernorVotesQuorumFractionComponent::GovernorQuorum<ContractState>;
+    impl GovernorVotesImpl = GovernorVotesQuorumFractionComponent::GovernorVotes<ContractState>;
+    impl GovernorCountingSimpleImpl =
+        GovernorCountingSimpleComponent::GovernorCounting<ContractState>;
+    impl GovernorCoreExecutionImpl =
+        GovernorCoreExecutionComponent::GovernorExecution<ContractState>;
+
+    // SRC5
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        pub governor: GovernorComponent::Storage,
+        #[substorage(v0)]
+        pub governor_votes: GovernorVotesQuorumFractionComponent::Storage,
+        #[substorage(v0)]
+        pub governor_counting_simple: GovernorCountingSimpleComponent::Storage,
+        #[substorage(v0)]
+        pub governor_core_execution: GovernorCoreExecutionComponent::Storage,
+        #[substorage(v0)]
+        pub src5: SRC5Component::Storage,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        GovernorEvent: GovernorComponent::Event,
+        #[flat]
+        GovernorVotesEvent: GovernorVotesQuorumFractionComponent::Event,
+        #[flat]
+        GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event,
+        #[flat]
+        GovernorCoreExecutionEvent: GovernorCoreExecutionComponent::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+    }
+
+    #[constructor]
+    fn constructor(ref self: ContractState, votes_token: ContractAddress) {
+        self.governor.initializer();
+        self.governor_votes.initializer(votes_token, QUORUM_NUMERATOR);
+    }
+
+    //
+    // SNIP12 Metadata
+    //
+
+    pub impl SNIP12MetadataImpl of SNIP12Metadata {
+        fn name() -> felt252 {
+            'DAPP_NAME'
+        }
+
+        fn version() -> felt252 {
+            'DAPP_VERSION'
+        }
+    }
+
+    //
+    // Locally implemented extensions
+    //
+
+    pub impl GovernorSettings of GovernorComponent::GovernorSettingsTrait<ContractState> {
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_delay`.
+        fn voting_delay(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
+            VOTING_DELAY
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::voting_period`.
+        fn voting_period(self: @GovernorComponent::ComponentState<ContractState>) -> u64 {
+            VOTING_PERIOD
+        }
+
+        /// See `GovernorComponent::GovernorSettingsTrait::proposal_threshold`.
+        fn proposal_threshold(self: @GovernorComponent::ComponentState<ContractState>) -> u256 {
+            PROPOSAL_THRESHOLD
+        }
+    }
+}
+
+#[starknet::contract]
+pub mod GovernorTimelockedMock {
+    use openzeppelin_governance::governor::GovernorComponent::InternalTrait as GovernorInternalTrait;
+    use openzeppelin_governance::governor::extensions::GovernorSettingsComponent::InternalTrait as GovernorSettingsInternalTrait;
+    use openzeppelin_governance::governor::extensions::GovernorTimelockExecutionComponent::InternalTrait as GovernorTimelockExecutionInternalTrait;
+    use openzeppelin_governance::governor::extensions::GovernorVotesComponent::InternalTrait as GovernorVotesInternalTrait;
+    use openzeppelin_governance::governor::extensions::{
+        GovernorVotesComponent, GovernorSettingsComponent, GovernorCountingSimpleComponent,
+        GovernorTimelockExecutionComponent
+    };
+    use openzeppelin_governance::governor::{GovernorComponent, DefaultConfig};
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
+    use starknet::ContractAddress;
+
+    pub const VOTING_DELAY: u64 = 86400; // 1 day
+    pub const VOTING_PERIOD: u64 = 432_000; // 1 week
+    pub const PROPOSAL_THRESHOLD: u256 = 10;
+    pub const QUORUM: u256 = 100_000_000;
+
+    component!(path: GovernorComponent, storage: governor, event: GovernorEvent);
+    component!(path: GovernorVotesComponent, storage: governor_votes, event: GovernorVotesEvent);
+    component!(
+        path: GovernorSettingsComponent, storage: governor_settings, event: GovernorSettingsEvent
+    );
+    component!(
+        path: GovernorCountingSimpleComponent,
+        storage: governor_counting_simple,
+        event: GovernorCountingSimpleEvent
+    );
+    component!(
+        path: GovernorTimelockExecutionComponent,
+        storage: governor_timelock_execution,
+        event: GovernorTimelockExecutionEvent
+    );
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+
+    // Governor
+    #[abi(embed_v0)]
+    impl GovernorImpl = GovernorComponent::GovernorImpl<ContractState>;
+
+    // Extensions external
+    #[abi(embed_v0)]
+    impl VotesTokenImpl = GovernorVotesComponent::VotesTokenImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl GovernorSettingsAdminImpl =
+        GovernorSettingsComponent::GovernorSettingsAdminImpl<ContractState>;
+    #[abi(embed_v0)]
+    impl TimelockedImpl =
+        GovernorTimelockExecutionComponent::TimelockedImpl<ContractState>;
+
+    // Extensions internal
+    impl GovernorVotesImpl = GovernorVotesComponent::GovernorVotes<ContractState>;
+    impl GovernorSettingsImpl = GovernorSettingsComponent::GovernorSettings<ContractState>;
+    impl GovernorCountingSimpleImpl =
+        GovernorCountingSimpleComponent::GovernorCounting<ContractState>;
+    impl GovernorTimelockExecutionImpl =
+        GovernorTimelockExecutionComponent::GovernorExecution<ContractState>;
+
+    // SRC5
+    #[abi(embed_v0)]
+    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
+
+    #[storage]
+    struct Storage {
+        #[substorage(v0)]
+        pub governor: GovernorComponent::Storage,
+        #[substorage(v0)]
+        pub governor_votes: GovernorVotesComponent::Storage,
+        #[substorage(v0)]
+        pub governor_settings: GovernorSettingsComponent::Storage,
+        #[substorage(v0)]
+        pub governor_counting_simple: GovernorCountingSimpleComponent::Storage,
+        #[substorage(v0)]
+        pub governor_timelock_execution: GovernorTimelockExecutionComponent::Storage,
+        #[substorage(v0)]
+        pub src5: SRC5Component::Storage,
+    }
+
+    #[event]
+    #[derive(Drop, starknet::Event)]
+    enum Event {
+        #[flat]
+        GovernorEvent: GovernorComponent::Event,
+        #[flat]
+        GovernorVotesEvent: GovernorVotesComponent::Event,
+        #[flat]
+        GovernorSettingsEvent: GovernorSettingsComponent::Event,
+        #[flat]
+        GovernorCountingSimpleEvent: GovernorCountingSimpleComponent::Event,
+        #[flat]
+        GovernorTimelockExecutionEvent: GovernorTimelockExecutionComponent::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
+    }
+
+    #[constructor]
+    fn constructor(
+        ref self: ContractState, votes_token: ContractAddress, timelock_controller: ContractAddress
+    ) {
+        self.governor.initializer();
+        self.governor_votes.initializer(votes_token);
+        self.governor_settings.initializer(VOTING_DELAY, VOTING_PERIOD, PROPOSAL_THRESHOLD);
+        self.governor_timelock_execution.initializer(timelock_controller);
+    }
+
+    //
+    // SNIP12 Metadata
+    //
+
+    pub impl SNIP12MetadataImpl of SNIP12Metadata {
+        fn name() -> felt252 {
+            'DAPP_NAME'
+        }
+
+        fn version() -> felt252 {
+            'DAPP_VERSION'
+        }
+    }
+
+    //
+    // Locally implemented extensions
+    //
+
+    impl GovernorQuorum of GovernorComponent::GovernorQuorumTrait<ContractState> {
+        /// See `GovernorComponent::GovernorQuorumTrait::quorum`.
+        fn quorum(self: @GovernorComponent::ComponentState<ContractState>, timepoint: u64) -> u256 {
+            QUORUM
+        }
+    }
+
+    #[abi(per_item)]
+    #[generate_trait]
+    impl ExternalImpl of ExternalTrait {
+        #[external(v0)]
+        fn cancel_operations(
+            ref self: ContractState, proposal_id: felt252, description_hash: felt252
+        ) {
+            self.governor.cancel_operations(proposal_id, description_hash);
+        }
+    }
+}
+
+#[starknet::interface]
+pub trait CancelOperations<TContractState> {
+    fn cancel_operations(ref self: TContractState, proposal_id: felt252, description_hash: felt252);
+}
diff --git a/packages/test_common/src/mocks/votes.cairo b/packages/test_common/src/mocks/votes.cairo
index fa4cb0261..eefdce4df 100644
--- a/packages/test_common/src/mocks/votes.cairo
+++ b/packages/test_common/src/mocks/votes.cairo
@@ -1,15 +1,13 @@
 #[starknet::contract]
-pub mod ERC721VotesMock {
+pub mod ERC20VotesMock {
     use openzeppelin_governance::votes::VotesComponent;
-    use openzeppelin_introspection::src5::SRC5Component;
-    use openzeppelin_token::erc721::ERC721Component;
+    use openzeppelin_token::erc20::ERC20Component;
     use openzeppelin_utils::cryptography::nonces::NoncesComponent;
     use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
     use starknet::ContractAddress;
 
-    component!(path: VotesComponent, storage: erc721_votes, event: ERC721VotesEvent);
-    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
-    component!(path: SRC5Component, storage: src5, event: SRC5Event);
+    component!(path: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent);
+    component!(path: ERC20Component, storage: erc20, event: ERC20Event);
     component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);
 
     // Votes
@@ -17,10 +15,10 @@ pub mod ERC721VotesMock {
     impl VotesImpl = VotesComponent::VotesImpl<ContractState>;
     impl VotesInternalImpl = VotesComponent::InternalImpl<ContractState>;
 
-    // ERC721
+    // ERC20
     #[abi(embed_v0)]
-    impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
-    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
+    impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
+    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
 
     // Nonces
     #[abi(embed_v0)]
@@ -29,11 +27,9 @@ pub mod ERC721VotesMock {
     #[storage]
     pub struct Storage {
         #[substorage(v0)]
-        pub erc721_votes: VotesComponent::Storage,
-        #[substorage(v0)]
-        pub erc721: ERC721Component::Storage,
+        pub erc20_votes: VotesComponent::Storage,
         #[substorage(v0)]
-        pub src5: SRC5Component::Storage,
+        pub erc20: ERC20Component::Storage,
         #[substorage(v0)]
         pub nonces: NoncesComponent::Storage
     }
@@ -42,11 +38,9 @@ pub mod ERC721VotesMock {
     #[derive(Drop, starknet::Event)]
     enum Event {
         #[flat]
-        ERC721VotesEvent: VotesComponent::Event,
-        #[flat]
-        ERC721Event: ERC721Component::Event,
+        ERC20VotesEvent: VotesComponent::Event,
         #[flat]
-        SRC5Event: SRC5Component::Event,
+        ERC20Event: ERC20Component::Event,
         #[flat]
         NoncesEvent: NoncesComponent::Event
     }
@@ -61,51 +55,47 @@ pub mod ERC721VotesMock {
         }
     }
 
-    impl ERC721VotesHooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
-        // We need to use the `before_update` hook to check the previous owner
-        // before the transfer is executed.
-        fn before_update(
-            ref self: ERC721Component::ComponentState<ContractState>,
-            to: ContractAddress,
-            token_id: u256,
-            auth: ContractAddress
+    impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
+        fn after_update(
+            ref self: ERC20Component::ComponentState<ContractState>,
+            from: ContractAddress,
+            recipient: ContractAddress,
+            amount: u256
         ) {
             let mut contract_state = self.get_contract_mut();
-
-            // We use the internal function here since it does not check if the token id exists
-            // which is necessary for mints
-            let previous_owner = self._owner_of(token_id);
-            contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1);
+            contract_state.erc20_votes.transfer_voting_units(from, recipient, amount);
         }
     }
 
     #[constructor]
     fn constructor(ref self: ContractState) {
-        self.erc721.initializer("MyToken", "MTK", "");
+        self.erc20.initializer("MyToken", "MTK");
     }
 }
 
 #[starknet::contract]
-pub mod ERC20VotesMock {
+pub mod ERC721VotesMock {
     use openzeppelin_governance::votes::VotesComponent;
-    use openzeppelin_token::erc20::ERC20Component;
+    use openzeppelin_introspection::src5::SRC5Component;
+    use openzeppelin_token::erc721::ERC721Component;
     use openzeppelin_utils::cryptography::nonces::NoncesComponent;
     use openzeppelin_utils::cryptography::snip12::SNIP12Metadata;
     use starknet::ContractAddress;
 
-    component!(path: VotesComponent, storage: erc20_votes, event: ERC20VotesEvent);
-    component!(path: ERC20Component, storage: erc20, event: ERC20Event);
+    component!(path: VotesComponent, storage: erc721_votes, event: ERC721VotesEvent);
+    component!(path: ERC721Component, storage: erc721, event: ERC721Event);
+    component!(path: SRC5Component, storage: src5, event: SRC5Event);
     component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);
 
-    // Votes and ERC20Votes
+    // Votes
     #[abi(embed_v0)]
     impl VotesImpl = VotesComponent::VotesImpl<ContractState>;
     impl VotesInternalImpl = VotesComponent::InternalImpl<ContractState>;
 
-    // ERC20
+    // ERC721
     #[abi(embed_v0)]
-    impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
-    impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
+    impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
+    impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
 
     // Nonces
     #[abi(embed_v0)]
@@ -114,9 +104,11 @@ pub mod ERC20VotesMock {
     #[storage]
     pub struct Storage {
         #[substorage(v0)]
-        pub erc20_votes: VotesComponent::Storage,
+        pub erc721_votes: VotesComponent::Storage,
         #[substorage(v0)]
-        pub erc20: ERC20Component::Storage,
+        pub erc721: ERC721Component::Storage,
+        #[substorage(v0)]
+        pub src5: SRC5Component::Storage,
         #[substorage(v0)]
         pub nonces: NoncesComponent::Storage
     }
@@ -125,9 +117,11 @@ pub mod ERC20VotesMock {
     #[derive(Drop, starknet::Event)]
     enum Event {
         #[flat]
-        ERC20VotesEvent: VotesComponent::Event,
+        ERC721VotesEvent: VotesComponent::Event,
         #[flat]
-        ERC20Event: ERC20Component::Event,
+        ERC721Event: ERC721Component::Event,
+        #[flat]
+        SRC5Event: SRC5Component::Event,
         #[flat]
         NoncesEvent: NoncesComponent::Event
     }
@@ -142,21 +136,26 @@ pub mod ERC20VotesMock {
         }
     }
 
-    impl ERC20VotesHooksImpl of ERC20Component::ERC20HooksTrait<ContractState> {
-        fn after_update(
-            ref self: ERC20Component::ComponentState<ContractState>,
-            from: ContractAddress,
-            recipient: ContractAddress,
-            amount: u256
+    impl ERC721VotesHooksImpl of ERC721Component::ERC721HooksTrait<ContractState> {
+        // We need to use the `before_update` hook to check the previous owner
+        // before the transfer is executed.
+        fn before_update(
+            ref self: ERC721Component::ComponentState<ContractState>,
+            to: ContractAddress,
+            token_id: u256,
+            auth: ContractAddress
         ) {
             let mut contract_state = self.get_contract_mut();
-            contract_state.erc20_votes.transfer_voting_units(from, recipient, amount);
+
+            // We use the internal function here since it does not check if the token id exists
+            // which is necessary for mints
+            let previous_owner = self._owner_of(token_id);
+            contract_state.erc721_votes.transfer_voting_units(previous_owner, to, 1);
         }
     }
 
     #[constructor]
     fn constructor(ref self: ContractState) {
-        self.erc20.initializer("MyToken", "MTK");
+        self.erc721.initializer("MyToken", "MTK", "");
     }
 }
-
diff --git a/packages/testing/src/constants.cairo b/packages/testing/src/constants.cairo
index d3cd64fdc..07752c82e 100644
--- a/packages/testing/src/constants.cairo
+++ b/packages/testing/src/constants.cairo
@@ -102,6 +102,14 @@ pub fn DELEGATEE() -> ContractAddress {
     contract_address_const::<'DELEGATEE'>()
 }
 
+pub fn TIMELOCK() -> ContractAddress {
+    contract_address_const::<'TIMELOCK'>()
+}
+
+pub fn VOTES_TOKEN() -> ContractAddress {
+    contract_address_const::<'VOTES_TOKEN'>()
+}
+
 pub fn ALICE() -> ContractAddress {
     contract_address_const::<'ALICE'>()
 }