Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisig Docs #1254

Merged
merged 28 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fc8c4b3
Add Multisig API references
immrsd Dec 10, 2024
0acb875
Fix Votes broken link
immrsd Dec 10, 2024
c962eef
Add Multisig doc page
immrsd Dec 10, 2024
300bd57
Fix RoleGranted event link in doc
immrsd Dec 10, 2024
cb291f3
Rename refs to events from Votes component to be explicit
immrsd Dec 10, 2024
dbf36cc
Add newlines after "Requirements:"
immrsd Dec 11, 2024
30387bb
Improve description of the is_confirmed fn
immrsd Dec 11, 2024
b0308c7
Add requirements and events to Multisig component function references
immrsd Dec 11, 2024
9d715d6
Fix typo
immrsd Dec 11, 2024
af8bef4
Fix doc line regarding QuorumUpdated event
immrsd Dec 11, 2024
4d9b491
Fix event links
immrsd Dec 11, 2024
31d4188
Fix functions order in doc
immrsd Dec 13, 2024
992bb78
Fix capitalisation
immrsd Dec 13, 2024
95cb2d0
Add import section
immrsd Dec 13, 2024
634343b
Add description for Multisig module
immrsd Dec 13, 2024
1e4c871
Add newlines where necessary
immrsd Dec 15, 2024
0b6cd5b
Improve multisig doc indices
immrsd Dec 15, 2024
7e24ff6
Add detailed explanation of ID computation of a multisig tx
immrsd Dec 15, 2024
9ead17d
Add detailed explanation of ID computation of a timelock operation
immrsd Dec 15, 2024
b8e4d8a
Fix order of Multisig functions
immrsd Dec 15, 2024
9e0f29b
Add descriptions for possible states of a Timelock operation
immrsd Dec 15, 2024
5aa6363
Add multisig tx states description
immrsd Dec 15, 2024
c1715c7
Fix minor issues in governance doc
immrsd Dec 15, 2024
3d22a75
Improve doc for Timelock cancel function
immrsd Dec 16, 2024
7af622b
Fix description of Waiting state of a Timelock tx
immrsd Dec 17, 2024
ba26c69
Fix links
immrsd Dec 17, 2024
7b4ba48
Fix doc review issues
immrsd Dec 17, 2024
6146c2b
Add corresponding newlines in in-code Multisig doc
immrsd Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

** Governance
*** xref:/governance/governor.adoc[Governor]
*** xref:/governance/multisig.adoc[Multisig]
*** xref:/governance/timelock.adoc[Timelock Controller]
*** xref:/governance/votes.adoc[Votes]
*** xref:/api/governance.adoc[API Reference]
Expand Down
888 changes: 846 additions & 42 deletions docs/modules/ROOT/pages/api/governance.adoc

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/governance/governor.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
= Governor

:votes-component: xref:api/governance.adoc#VotesComponent[VotesComponent]
:governor-component: xref:api/governance.adoc#GovernorComponent[GovernorComponent]
:access-control: xref:access.adoc#role_based_accesscontrol[AccessControl]
Expand Down
154 changes: 154 additions & 0 deletions docs/modules/ROOT/pages/governance/multisig.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
= Multisig

:multisig-component: xref:api/governance.adoc#MultisigComponent[MultisigComponent]
:snip12-metadata: xref:api/utilities.adoc#snip12[SNIP12Metadata]

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.

NOTE: 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:

`NotFound` → `Pending` → `Confirmed` → `Executed`

- *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 {multisig-component}.
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:

[,cairo]
----
#[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 {multisig-component}:

[,cairo]
----
#[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);
}
----
18 changes: 16 additions & 2 deletions docs/modules/ROOT/pages/governance/timelock.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,31 @@
:accesscontrol-component: xref:api/access.adoc#AccessControlComponent[AccessControlComponent]
:src5-component: xref:api/introspection.adoc#SRC5Component[SRC5Component]


The Timelock Controller provides a means of enforcing time delays on the execution of transactions. This is considered good practice regarding governance systems because it allows users the opportunity to exit the system if they disagree with a decision before it is executed.

NOTE: The Timelock contract itself executes transactions, not the user. The Timelock should, therefore, hold associated funds, ownership, and access control roles.

== Operation lifecycle

Timelocked operations are identified by a unique id (their hash) and follow a specific `OperationState` lifecycle:
The state of an operation is represented by the `OperationState` enum and can be retrieved
by calling the `get_operation_state` function with the operation's identifier.

The identifier of an operation is a `felt252` value, computed as the Pedersen hash of the
operation's call or calls, its predecessor, and salt. It can be computed by invoking the
implementing contract's `hash_operation` function for single-call operations or
`hash_operation_batch` for multi-call operations. Submitting an operation with identical calls,
predecessor, and the same salt value a second time will fail, as operation identifiers must be
unique. To resolve this, use a different salt value to generate a unique identifier.

Timelocked operations follow a specific lifecycle:

`Unset` → `Waiting` → `Ready` → `Done`

- `Unset`: the operation has not been scheduled or has been canceled.
- `Waiting`: the operation has been scheduled and is pending the minimum delay.
immrsd marked this conversation as resolved.
Show resolved Hide resolved
- `Ready`: the timer has expired, and the operation is eligible for execution.
- `Done`: the operation has been executed.

== Timelock flow
immrsd marked this conversation as resolved.
Show resolved Hide resolved

=== Schedule
Expand Down
6 changes: 2 additions & 4 deletions docs/modules/ROOT/pages/governance/votes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
:delegate: xref:api/governance.adoc#VotesComponent-delegate[delegate]
:delegate_by_sig: xref:api/governance.adoc#VotesComponent-delegate_by_sig[delegate_by_sig]
:voting_units_trait: xref:api/governance.adoc#VotingUnitsTrait[VotingUnitsTrait]
:votes-usage: xref:../governance.adoc#usage_2[usage]
:votes-usage: xref:Usage[usage]
immrsd marked this conversation as resolved.
Show resolved Hide resolved
:nonces-component: xref:api/utilities.adoc#NoncesComponent[NoncesComponent]
:snip12-metadata: xref:api/utilities.adoc#snip12[SNIP12Metadata]


The {votes-component} provides a flexible system for tracking and delegating voting power. This system allows users to delegate their voting power to other addresses, enabling more active participation in governance.

NOTE: By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked.

IMPORTANT: The transferring of voting units must be handled by the implementing contract. In the case of `ERC20` and `ERC721` this is usually done via the hooks. You can check the {votes-usage} section for examples of how to implement this.

== Key Features
== Key features

1. *Delegation*: Users can delegate their voting power to any address, including themselves. Vote power can be delegated either by calling the {delegate} function directly, or by providing a signature to be used with {delegate_by_sig}.
2. *Historical lookups*: The system keeps track of historical snapshots for each account, which allows the voting power of an account to be queried at a specific timestamp. +
Expand All @@ -29,7 +28,6 @@ Additionally, you must implement the {nonces-component} and the {snip12-metadata

Here's an example of how to structure a simple ERC20Votes contract:


[source,cairo]
----
#[starknet::contract]
Expand Down
66 changes: 34 additions & 32 deletions packages/governance/src/multisig/multisig.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,20 @@ pub mod MultisigComponent {
self.Multisig_tx_info.read(id).is_executed
}

/// Returns the block number when the transaction with the given `id` was submitted.
fn get_submitted_block(self: @ComponentState<TContractState>, id: TransactionID) -> u64 {
self.Multisig_tx_info.read(id).submitted_block
}

/// Returns the current state of the transaction with the given `id`.
///
/// The possible states are:
/// - `NotFound`: The transaction does not exist.
/// - `Pending`: The transaction exists but hasn't reached the required confirmations.
/// - `Confirmed`: The transaction has reached the required confirmations but hasn't been
///
/// - `NotFound`: the transaction does not exist.
/// - `Pending`: the transaction exists but hasn't reached the required confirmations.
/// - `Confirmed`: the transaction has reached the required confirmations but hasn't been
/// executed.
/// - `Executed`: The transaction has been executed.
/// - `Executed`: the transaction has been executed.
fn get_transaction_state(
self: @ComponentState<TContractState>, id: TransactionID,
) -> TransactionState {
Expand All @@ -219,9 +225,23 @@ pub mod MultisigComponent {
result
}

/// Returns the block number when the transaction with the given `id` was submitted.
fn get_submitted_block(self: @ComponentState<TContractState>, id: TransactionID) -> u64 {
self.Multisig_tx_info.read(id).submitted_block
/// Returns the computed identifier of a transaction containing a single call.
fn hash_transaction(
self: @ComponentState<TContractState>,
to: ContractAddress,
selector: felt252,
calldata: Span<felt252>,
salt: felt252,
) -> TransactionID {
let call = Call { to, selector, calldata };
self.hash_transaction_batch(array![call].span(), salt)
}

/// Returns the computed identifier of a transaction containing a batch of calls.
fn hash_transaction_batch(
self: @ComponentState<TContractState>, calls: Span<Call>, salt: felt252,
) -> TransactionID {
PedersenTrait::new(0).update_with(calls).update_with(salt).finalize()
}

/// Adds new signers and updates the quorum.
Expand Down Expand Up @@ -430,25 +450,6 @@ pub mod MultisigComponent {
},
};
}

/// Returns the computed identifier of a transaction containing a single call.
fn hash_transaction(
self: @ComponentState<TContractState>,
to: ContractAddress,
selector: felt252,
calldata: Span<felt252>,
salt: felt252,
) -> TransactionID {
let call = Call { to, selector, calldata };
self.hash_transaction_batch(array![call].span(), salt)
}

/// Returns the computed identifier of a transaction containing a batch of calls.
fn hash_transaction_batch(
self: @ComponentState<TContractState>, calls: Span<Call>, salt: felt252,
) -> TransactionID {
PedersenTrait::new(0).update_with(calls).update_with(salt).finalize()
}
}

//
Expand All @@ -467,7 +468,7 @@ pub mod MultisigComponent {
/// - `quorum` must be non-zero and less than or equal to the number of `signers`.
///
/// Emits a `SignerAdded` event for each signer added.
/// Emits a `QuorumUpdated` event if the quorum changes.
/// Emits a `QuorumUpdated` event.
fn initializer(
ref self: ComponentState<TContractState>, quorum: u32, signers: Span<ContractAddress>,
) {
Expand All @@ -477,11 +478,12 @@ pub mod MultisigComponent {
/// Resolves and returns the current state of the transaction with the given `id`.
///
/// The possible states are:
/// - `NotFound`: The transaction does not exist.
/// - `Pending`: The transaction exists but hasn't reached the required confirmations.
/// - `Confirmed`: The transaction has reached the required confirmations but hasn't been
///
/// - `NotFound`: the transaction does not exist.
/// - `Pending`: the transaction exists but hasn't reached the required confirmations.
/// - `Confirmed`: the transaction has reached the required confirmations but hasn't been
/// executed.
/// - `Executed`: The transaction has been executed.
/// - `Executed`: the transaction has been executed.
fn resolve_tx_state(
self: @ComponentState<TContractState>, id: TransactionID,
) -> TransactionState {
Expand Down Expand Up @@ -513,7 +515,7 @@ pub mod MultisigComponent {
///
/// Requirements:
///
/// - The transaction with `id` must have been submitted.
/// - The transaction with the given `id` must have been submitted.
fn assert_tx_exists(self: @ComponentState<TContractState>, id: TransactionID) {
assert(self.get_submitted_block(id).is_non_zero(), Errors::TX_NOT_FOUND);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/governance/src/timelock/timelock_controller.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ pub mod TimelockControllerComponent {
}

/// Returns the OperationState for `id`.
///
/// The possible states are:
///
/// - `Unset`: the operation has not been scheduled or has been canceled.
/// - `Waiting`: the operation has been scheduled and is pending the minimum delay.
immrsd marked this conversation as resolved.
Show resolved Hide resolved
/// - `Ready`: the timer has expired, and the operation is eligible for execution.
/// - `Done`: the operation has been executed.
fn get_operation_state(
self: @ComponentState<TContractState>, id: felt252,
) -> OperationState {
Expand Down Expand Up @@ -260,7 +267,7 @@ pub mod TimelockControllerComponent {
}
}

/// Cancels an operation.
/// Cancels an operation. A canceled operation returns to `Unset` OperationState.
///
/// Requirements:
///
Expand Down
Loading