forked from OpenZeppelin/cairo-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Governor documentation (OpenZeppelin#1235)
* docs: document main component * feat: add extensions API References * Update docs/modules/ROOT/pages/governance/governor.adoc Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com> * Update docs/modules/ROOT/pages/governance/governor.adoc Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com> * feat: apply review updates * feat: add interface id * feat: apply review updates * feat: add interface * feat: apply review updates --------- Co-authored-by: Andrew Fleming <fleming.andrew@protonmail.com>
- Loading branch information
1 parent
efbbe4d
commit 0676415
Showing
14 changed files
with
2,914 additions
and
473 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
= Timelock Controller | ||
|
||
:timelock-component: xref:api/governance.adoc#TimelockControllerComponent[TimelockControllerComponent] | ||
: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: | ||
|
||
`Unset` → `Waiting` → `Ready` → `Done` | ||
|
||
== Timelock flow | ||
|
||
=== Schedule | ||
|
||
:schedule: xref:api/governance.adoc#ITimelock-schedule[schedule] | ||
:get_timestamp: xref:api/governance.adoc#ITimelock-get_timestamp[get_timestamp] | ||
|
||
When a proposer calls {schedule}, the `OperationState` moves from `Unset` to `Waiting`. | ||
This starts a timer that must be greater than or equal to the minimum delay. | ||
The timer expires at a timestamp accessible through {get_timestamp}. | ||
Once the timer expires, the `OperationState` automatically moves to the `Ready` state. | ||
At this point, it can be executed. | ||
|
||
=== Execute | ||
|
||
:execute: xref:api/governance.adoc#ITimelock-execute[execute] | ||
|
||
By calling {execute}, an executor triggers the operation's underlying transactions and moves it to the `Done` state. If the operation has a predecessor, the predecessor's operation must be in the `Done` state for this transaction to succeed. | ||
|
||
=== Cancel | ||
|
||
:cancel: xref:api/governance.adoc#ITimelock-cancel[cancel] | ||
|
||
The {cancel} function allows cancellers to cancel any pending operations. | ||
This resets the operation to the `Unset` state. | ||
It is therefore possible for a proposer to re-schedule an operation that has been cancelled. | ||
In this case, the timer restarts when the operation is re-scheduled. | ||
|
||
=== Roles | ||
|
||
{timelock-component} leverages an {accesscontrol-component} setup that we need to understand in order to set up roles. | ||
|
||
- `PROPOSER_ROLE` - in charge of queueing operations. | ||
|
||
- `CANCELLER_ROLE` - may cancel scheduled operations. | ||
During initialization, accounts granted with `PROPOSER_ROLE` will also be granted `CANCELLER_ROLE`. | ||
Therefore, the initial proposers may also cancel operations after they are scheduled. | ||
|
||
- `EXECUTOR_ROLE` - in charge of executing already available operations. | ||
|
||
- `DEFAULT_ADMIN_ROLE` - can grant and revoke the three previous roles. | ||
|
||
CAUTION: The `DEFAULT_ADMIN_ROLE` is a sensitive role that will be granted automatically to the timelock itself and optionally to a second account. | ||
The latter case may be required to ease a contract's initial configuration; however, this role should promptly be renounced. | ||
|
||
Furthermore, the timelock component supports the concept of open roles for the `EXECUTOR_ROLE`. | ||
This allows anyone to execute an operation once it's in the `Ready` OperationState. | ||
To enable the `EXECUTOR_ROLE` to be open, grant the zero address with the `EXECUTOR_ROLE`. | ||
|
||
CAUTION: Be very careful with enabling open roles as _anyone_ can call the function. | ||
|
||
=== Minimum delay | ||
|
||
:get_min_delay: xref:api/governance.adoc#ITimelock-get_min_delay[get_min_delay] | ||
|
||
The minimum delay of the timelock acts as a buffer from when a proposer schedules an operation to the earliest point at which an executor may execute that operation. | ||
The idea is for users, should they disagree with a scheduled proposal, to have options such as exiting the system or making their case for cancellers to cancel the operation. | ||
|
||
After initialization, the only way to change the timelock's minimum delay is to schedule it and execute it with the same flow as any other operation. | ||
|
||
The minimum delay of a contract is accessible through {get_min_delay}. | ||
|
||
=== Usage | ||
|
||
Integrating the timelock into a contract requires integrating {timelock-component} as well as {src5-component} and {accesscontrol-component} as dependencies. | ||
The contract's constructor should initialize the timelock which consists of setting the: | ||
|
||
- Proposers and executors. | ||
- Minimum delay between scheduling and executing an operation. | ||
- Optional admin if additional configuration is required. | ||
|
||
NOTE: The optional admin should renounce their role once configuration is complete. | ||
|
||
Here's an example of a simple timelock contract: | ||
|
||
[,cairo] | ||
---- | ||
#[starknet::contract] | ||
mod TimelockControllerContract { | ||
use openzeppelin_access::accesscontrol::AccessControlComponent; | ||
use openzeppelin_governance::timelock::TimelockControllerComponent; | ||
use openzeppelin_introspection::src5::SRC5Component; | ||
use starknet::ContractAddress; | ||
component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); | ||
component!(path: TimelockControllerComponent, storage: timelock, event: TimelockEvent); | ||
component!(path: SRC5Component, storage: src5, event: SRC5Event); | ||
// Timelock Mixin | ||
#[abi(embed_v0)] | ||
impl TimelockMixinImpl = | ||
TimelockControllerComponent::TimelockMixinImpl<ContractState>; | ||
impl TimelockInternalImpl = TimelockControllerComponent::InternalImpl<ContractState>; | ||
#[storage] | ||
struct Storage { | ||
#[substorage(v0)] | ||
access_control: AccessControlComponent::Storage, | ||
#[substorage(v0)] | ||
timelock: TimelockControllerComponent::Storage, | ||
#[substorage(v0)] | ||
src5: SRC5Component::Storage | ||
} | ||
#[event] | ||
#[derive(Drop, starknet::Event)] | ||
enum Event { | ||
#[flat] | ||
AccessControlEvent: AccessControlComponent::Event, | ||
#[flat] | ||
TimelockEvent: TimelockControllerComponent::Event, | ||
#[flat] | ||
SRC5Event: SRC5Component::Event | ||
} | ||
#[constructor] | ||
fn constructor( | ||
ref self: ContractState, | ||
min_delay: u64, | ||
proposers: Span<ContractAddress>, | ||
executors: Span<ContractAddress>, | ||
admin: ContractAddress | ||
) { | ||
self.timelock.initializer(min_delay, proposers, executors, admin); | ||
} | ||
} | ||
---- | ||
|
||
=== Interface | ||
|
||
This is the full interface of the TimelockMixinImpl implementation: | ||
|
||
[,cairo] | ||
---- | ||
#[starknet::interface] | ||
pub trait TimelockABI<TState> { | ||
// ITimelock | ||
fn is_operation(self: @TState, id: felt252) -> bool; | ||
fn is_operation_pending(self: @TState, id: felt252) -> bool; | ||
fn is_operation_ready(self: @TState, id: felt252) -> bool; | ||
fn is_operation_done(self: @TState, id: felt252) -> bool; | ||
fn get_timestamp(self: @TState, id: felt252) -> u64; | ||
fn get_operation_state(self: @TState, id: felt252) -> OperationState; | ||
fn get_min_delay(self: @TState) -> u64; | ||
fn hash_operation(self: @TState, call: Call, predecessor: felt252, salt: felt252) -> felt252; | ||
fn hash_operation_batch( | ||
self: @TState, calls: Span<Call>, predecessor: felt252, salt: felt252 | ||
) -> felt252; | ||
fn schedule(ref self: TState, call: Call, predecessor: felt252, salt: felt252, delay: u64); | ||
fn schedule_batch( | ||
ref self: TState, calls: Span<Call>, predecessor: felt252, salt: felt252, delay: u64 | ||
); | ||
fn cancel(ref self: TState, id: felt252); | ||
fn execute(ref self: TState, call: Call, predecessor: felt252, salt: felt252); | ||
fn execute_batch(ref self: TState, calls: Span<Call>, predecessor: felt252, salt: felt252); | ||
fn update_delay(ref self: TState, new_delay: u64); | ||
// ISRC5 | ||
fn supports_interface(self: @TState, interface_id: felt252) -> bool; | ||
// IAccessControl | ||
fn has_role(self: @TState, role: felt252, account: ContractAddress) -> bool; | ||
fn get_role_admin(self: @TState, role: felt252) -> felt252; | ||
fn grant_role(ref self: TState, role: felt252, account: ContractAddress); | ||
fn revoke_role(ref self: TState, role: felt252, account: ContractAddress); | ||
fn renounce_role(ref self: TState, role: felt252, account: ContractAddress); | ||
// IAccessControlCamel | ||
fn hasRole(self: @TState, role: felt252, account: ContractAddress) -> bool; | ||
fn getRoleAdmin(self: @TState, role: felt252) -> felt252; | ||
fn grantRole(ref self: TState, role: felt252, account: ContractAddress); | ||
fn revokeRole(ref self: TState, role: felt252, account: ContractAddress); | ||
fn renounceRole(ref self: TState, role: felt252, account: ContractAddress); | ||
} | ||
---- |
Oops, something went wrong.