Multisig

The Multisig component implements a multi-signature mechanism to enhance the security and governance of smart contract transactions. It ensures that no single signer can unilaterally execute critical actions, requiring multiple registered signers to approve and collectively execute transactions.

This component is designed to secure operations such as fund management or protocol governance, where collective decision-making is essential. The Multisig Component is self-administered, meaning that changes to signers or quorum must be approved through the multisig process itself.

Key features

  • Multi-Signature Security: transactions must be approved by multiple signers, ensuring distributed governance.

  • Quorum Enforcement: defines the minimum number of approvals required for transaction execution.

  • Self-Administration: all modifications to the component (e.g., adding or removing signers) must pass through the multisig process.

  • Event Logging: provides comprehensive event logging for transparency and auditability.

Signer management

The Multisig component introduces the concept of signers and quorum:

  • Signers: only registered signers can submit, confirm, revoke, or execute transactions. The Multisig Component supports adding, removing, or replacing signers.

  • Quorum: the quorum defines the minimum number of confirmations required to approve a transaction.

To prevent unauthorized modifications, only the contract itself can add, remove, or replace signers or change the quorum. This ensures that all modifications pass through the multisig approval process.

Transaction lifecycle

The state of a transaction is represented by the TransactionState enum and can be retrieved by calling the get_transaction_state function with the transaction’s identifier.

The identifier of a multisig transaction is a felt252 value, computed as the Pedersen hash of the transaction’s calls and salt. It can be computed by invoking the implementing contract’s hash_transaction method for single-call transactions or hash_transaction_batch for multi-call transactions. Submitting a transaction with identical calls and the same salt value a second time will fail, as transaction identifiers must be unique. To resolve this, use a different salt value to generate a unique identifier.

A transaction in the Multisig component follows a specific lifecycle:

NotFoundPendingConfirmedExecuted

  • NotFound: the transaction does not exist.

  • Pending: the transaction exists but has not reached the required confirmations.

  • Confirmed: the transaction has reached the quorum but has not yet been executed.

  • Executed: the transaction has been successfully executed.

Usage

Integrating the Multisig functionality into a contract requires implementing MultisigComponent. The contract’s constructor should initialize the component with a quorum value and a list of initial signers.

Here’s an example of a simple wallet contract featuring the Multisig functionality:

#[starknet::contract]
mod MultisigWallet {
    use openzeppelin_governance::multisig::MultisigComponent;
    use starknet::ContractAddress;

    component!(path: MultisigComponent, storage: multisig, event: MultisigEvent);

    #[abi(embed_v0)]
    impl MultisigImpl = MultisigComponent::MultisigImpl<ContractState>;
    impl MultisigInternalImpl = MultisigComponent::InternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        multisig: MultisigComponent::Storage,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        MultisigEvent: MultisigComponent::Event,
    }

    #[constructor]
    fn constructor(ref self: ContractState, quorum: u32, signers: Span<ContractAddress>) {
        self.multisig.initializer(quorum, signers);
    }
}

Interface

This is the interface of a contract implementing the MultisigComponent:

#[starknet::interface]
pub trait MultisigABI<TState> {
    // Read functions
    fn get_quorum(self: @TState) -> u32;
    fn is_signer(self: @TState, signer: ContractAddress) -> bool;
    fn get_signers(self: @TState) -> Span<ContractAddress>;
    fn is_confirmed(self: @TState, id: TransactionID) -> bool;
    fn is_confirmed_by(self: @TState, id: TransactionID, signer: ContractAddress) -> bool;
    fn is_executed(self: @TState, id: TransactionID) -> bool;
    fn get_submitted_block(self: @TState, id: TransactionID) -> u64;
    fn get_transaction_state(self: @TState, id: TransactionID) -> TransactionState;
    fn get_transaction_confirmations(self: @TState, id: TransactionID) -> u32;
    fn hash_transaction(
        self: @TState,
        to: ContractAddress,
        selector: felt252,
        calldata: Span<felt252>,
        salt: felt252,
    ) -> TransactionID;
    fn hash_transaction_batch(self: @TState, calls: Span<Call>, salt: felt252) -> TransactionID;

    // Write functions
    fn add_signers(ref self: TState, new_quorum: u32, signers_to_add: Span<ContractAddress>);
    fn remove_signers(ref self: TState, new_quorum: u32, signers_to_remove: Span<ContractAddress>);
    fn replace_signer(
        ref self: TState, signer_to_remove: ContractAddress, signer_to_add: ContractAddress,
    );
    fn change_quorum(ref self: TState, new_quorum: u32);
    fn submit_transaction(
        ref self: TState,
        to: ContractAddress,
        selector: felt252,
        calldata: Span<felt252>,
        salt: felt252,
    ) -> TransactionID;
    fn submit_transaction_batch(
        ref self: TState, calls: Span<Call>, salt: felt252,
    ) -> TransactionID;
    fn confirm_transaction(ref self: TState, id: TransactionID);
    fn revoke_confirmation(ref self: TState, id: TransactionID);
    fn execute_transaction(
        ref self: TState,
        to: ContractAddress,
        selector: felt252,
        calldata: Span<felt252>,
        salt: felt252,
    );
    fn execute_transaction_batch(ref self: TState, calls: Span<Call>, salt: felt252);
}