Skip to content

Commit a1163e9

Browse files
authored
governance: use ledgers instead of timestamps (OpenZeppelin#569)
* use ledgers instead of timestamps * edge overflows
1 parent 606ec84 commit a1163e9

File tree

9 files changed

+161
-159
lines changed

9 files changed

+161
-159
lines changed

examples/timelock-controller/src/contract.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ use stellar_access::access_control::{
114114
};
115115
use stellar_governance::timelock::{
116116
cancel_operation, execute_operation, get_min_delay as timelock_get_min_delay,
117-
get_operation_state, get_timestamp, hash_operation as timelock_hash_operation,
117+
get_operation_ledger, get_operation_state, hash_operation as timelock_hash_operation,
118118
is_operation_done, is_operation_pending, is_operation_ready, operation_exists,
119119
schedule_operation, set_execute_operation, set_min_delay as timelock_set_min_delay, Operation,
120120
OperationState, TimelockError,
@@ -218,7 +218,7 @@ impl TimelockController {
218218
/// # Arguments
219219
///
220220
/// * `e` - Access to Soroban environment.
221-
/// * `min_delay` - Initial minimum delay in seconds for operations.
221+
/// * `min_delay` - Initial minimum delay in ledgers for operations.
222222
/// * `proposers` - Accounts to be granted proposer and canceller roles.
223223
/// * `executors` - Accounts to be granted executor role.
224224
/// * `admin` - Optional account to be granted admin role for initial setup.
@@ -270,7 +270,7 @@ impl TimelockController {
270270
/// * `predecessor` - The predecessor operation ID (use empty bytes for
271271
/// none).
272272
/// * `salt` - Salt for uniqueness (use empty bytes for default).
273-
/// * `delay` - The delay in seconds before the operation can be executed.
273+
/// * `delay` - The delay in ledgers before the operation can be executed.
274274
/// * `proposer` - The address proposing the operation (must have proposer
275275
/// role).
276276
///
@@ -367,7 +367,7 @@ impl TimelockController {
367367
/// # Arguments
368368
///
369369
/// * `e` - Access to Soroban environment.
370-
/// * `new_delay` - The new minimum delay in seconds.
370+
/// * `new_delay` - The new minimum delay in ledgers.
371371
///
372372
/// # Notes
373373
///
@@ -379,7 +379,7 @@ impl TimelockController {
379379
timelock_set_min_delay(e, new_delay);
380380
}
381381

382-
/// Returns the minimum delay in seconds required for operations.
382+
/// Returns the minimum delay in ledgers required for operations.
383383
pub fn get_min_delay(e: &Env) -> u32 {
384384
timelock_get_min_delay(e)
385385
}
@@ -397,9 +397,9 @@ impl TimelockController {
397397
timelock_hash_operation(e, &operation)
398398
}
399399

400-
/// Returns the timestamp at which an operation becomes ready.
401-
pub fn get_timestamp(e: &Env, operation_id: BytesN<32>) -> u64 {
402-
get_timestamp(e, &operation_id)
400+
/// Returns the ledger sequence number at which an operation becomes ready.
401+
pub fn get_operation_ledger(e: &Env, operation_id: BytesN<32>) -> u32 {
402+
get_operation_ledger(e, &operation_id)
403403
}
404404

405405
/// Returns the current state of an operation.

examples/timelock-controller/src/test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fn schedule_and_execute_operation() {
9191
assert!(!client.is_operation_ready(&operation_id));
9292

9393
// Advance ledgers to make operation ready
94-
e.ledger().with_mut(|li| li.timestamp += 10);
94+
e.ledger().with_mut(|li| li.sequence_number += 10);
9595

9696
assert!(client.is_operation_ready(&operation_id));
9797

@@ -140,7 +140,7 @@ fn schedule_and_execute_operation_no_executors() {
140140
assert!(client.is_operation_pending(&operation_id));
141141
assert!(!client.is_operation_ready(&operation_id));
142142

143-
e.ledger().with_mut(|li| li.timestamp += 10);
143+
e.ledger().with_mut(|li| li.sequence_number += 10);
144144

145145
assert!(client.is_operation_ready(&operation_id));
146146

@@ -207,7 +207,7 @@ fn schedule_and_execute_self_admin_operation() {
207207
assert!(client.is_operation_pending(&operation_id));
208208
assert!(!client.is_operation_ready(&operation_id));
209209

210-
e.ledger().with_mut(|li| li.timestamp += 10);
210+
e.ledger().with_mut(|li| li.sequence_number += 10);
211211

212212
assert!(client.is_operation_ready(&operation_id));
213213

packages/governance/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ The `votes` module provides vote tracking functionality with delegation and hist
1919

2020
- **Voting Units**: The base unit of voting power, typically 1:1 with token balance
2121
- **Delegation**: Accounts can delegate their voting power to another account (delegatee)
22-
- **Checkpoints**: Historical snapshots of voting power at specific timestamps
23-
- **Clock Mode**: Uses ledger timestamps (`e.ledger().timestamp()`) as the timepoint reference
22+
- **Checkpoints**: Historical snapshots of voting power at specific ledger sequence numbers
23+
- **Clock Mode**: Uses ledger sequence numbers (`e.ledger().sequence()`) as the timepoint reference
2424

2525
#### Key Features
2626

2727
- Track voting power per account with historical checkpoints
2828
- Support delegation (an account can delegate its voting power to another account)
29-
- Provide historical vote queries at any past timestamp
29+
- Provide historical vote queries at any past ledger sequence number
3030
- Explicit delegation required (accounts must self-delegate to use their own voting power)
3131
- Non-delegated voting units are not counted as votes
3232

packages/governance/src/timelock/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ mod test;
5353
use soroban_sdk::{contracterror, contractevent, Address, BytesN, Env, Symbol, Val, Vec};
5454

5555
pub use crate::timelock::storage::{
56-
cancel_operation, execute_operation, get_min_delay, get_operation_state, get_timestamp,
56+
cancel_operation, execute_operation, get_min_delay, get_operation_ledger, get_operation_state,
5757
hash_operation, is_operation_done, is_operation_pending, is_operation_ready, operation_exists,
5858
schedule_operation, set_execute_operation, set_min_delay, Operation, OperationState,
5959
TimelockStorageKey,
@@ -93,11 +93,11 @@ pub const TIMELOCK_EXTEND_AMOUNT: u32 = 30 * DAY_IN_LEDGERS;
9393
pub const TIMELOCK_TTL_THRESHOLD: u32 = TIMELOCK_EXTEND_AMOUNT - DAY_IN_LEDGERS;
9494

9595
/// Sentinel value for an operation that has not been scheduled.
96-
pub const UNSET_TIMESTAMP: u64 = 0;
96+
pub const UNSET_LEDGER: u32 = 0;
9797

9898
/// Sentinel value used to mark an operation as done.
9999
/// Using 1 instead of 0 to distinguish from unset operations.
100-
pub const DONE_TIMESTAMP: u64 = 1;
100+
pub const DONE_LEDGER: u32 = 1;
101101

102102
// ################## EVENTS ##################
103103

@@ -146,7 +146,7 @@ pub struct OperationScheduled {
146146
/// * `args` - The arguments to pass to the function.
147147
/// * `predecessor` - The predecessor operation ID.
148148
/// * `salt` - The salt for uniqueness.
149-
/// * `delay` - The delay in seconds.
149+
/// * `delay` - The delay in ledgers.
150150
#[allow(clippy::too_many_arguments)]
151151
pub fn emit_operation_scheduled(
152152
e: &Env,

packages/governance/src/timelock/storage.rs

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use soroban_sdk::{
44

55
use crate::timelock::{
66
emit_min_delay_changed, emit_operation_cancelled, emit_operation_executed,
7-
emit_operation_scheduled, TimelockError, DONE_TIMESTAMP, TIMELOCK_EXTEND_AMOUNT,
8-
TIMELOCK_TTL_THRESHOLD, UNSET_TIMESTAMP,
7+
emit_operation_scheduled, TimelockError, DONE_LEDGER, TIMELOCK_EXTEND_AMOUNT,
8+
TIMELOCK_TTL_THRESHOLD, UNSET_LEDGER,
99
};
1010

1111
// ################## TYPES ##################
@@ -50,25 +50,25 @@ pub enum OperationState {
5050
#[derive(Clone)]
5151
#[contracttype]
5252
pub enum TimelockStorageKey {
53-
/// Minimum delay in seconds for operations
53+
/// Minimum delay in ledgers for operations
5454
MinDelay,
55-
/// Maps operation ID to the timestamp when it will be in a
55+
/// Maps operation ID to the ledger sequence number when it will be in a
5656
/// [`OperationState::Ready`] state (Note: value is 0 for
57-
/// [`OperationState::Unset`], 1 for [`OperationState:Done`]).
58-
Timestamp(BytesN<32>),
57+
/// [`OperationState::Unset`], 1 for [`OperationState::Done`]).
58+
OperationLedger(BytesN<32>),
5959
}
6060

6161
// ################## QUERY STATE ##################
6262

63-
/// Returns the minimum delay in seconds required for operations.
63+
/// Returns the minimum delay in ledgers required for operations.
6464
///
6565
/// # Arguments
6666
///
6767
/// * `e` - Access to Soroban environment.
6868
///
6969
/// # Returns
7070
///
71-
/// The minimum delay in seconds.
71+
/// The minimum delay in ledgers.
7272
///
7373
/// # Errors
7474
///
@@ -80,7 +80,7 @@ pub fn get_min_delay(e: &Env) -> u32 {
8080
.unwrap_or_else(|| panic_with_error!(e, TimelockError::MinDelayNotSet))
8181
}
8282

83-
/// Returns the timestamp at which an operation becomes ready.
83+
/// Returns the ledger sequence number at which an operation becomes ready.
8484
///
8585
/// # Arguments
8686
///
@@ -89,16 +89,17 @@ pub fn get_min_delay(e: &Env) -> u32 {
8989
///
9090
/// # Returns
9191
///
92-
/// - `UNSET_TIMESTAMP` for unset operations
93-
/// - `DONE_TIMESTAMP` for done operations
94-
/// - Unix timestamp when the operation becomes ready for scheduled operations
95-
pub fn get_timestamp(e: &Env, operation_id: &BytesN<32>) -> u64 {
96-
let key = TimelockStorageKey::Timestamp(operation_id.clone());
97-
if let Some(timestamp) = e.storage().persistent().get::<_, u64>(&key) {
92+
/// - `UNSET_LEDGER` for unset operations
93+
/// - `DONE_LEDGER` for done operations
94+
/// - Ledger sequence number when the operation becomes ready for scheduled
95+
/// operations
96+
pub fn get_operation_ledger(e: &Env, operation_id: &BytesN<32>) -> u32 {
97+
let key = TimelockStorageKey::OperationLedger(operation_id.clone());
98+
if let Some(ready_ledger) = e.storage().persistent().get::<_, u32>(&key) {
9899
e.storage().persistent().extend_ttl(&key, TIMELOCK_TTL_THRESHOLD, TIMELOCK_EXTEND_AMOUNT);
99-
timestamp
100+
ready_ledger
100101
} else {
101-
UNSET_TIMESTAMP
102+
UNSET_LEDGER
102103
}
103104
}
104105

@@ -113,13 +114,13 @@ pub fn get_timestamp(e: &Env, operation_id: &BytesN<32>) -> u64 {
113114
///
114115
/// The current [`OperationState`] of the operation.
115116
pub fn get_operation_state(e: &Env, operation_id: &BytesN<32>) -> OperationState {
116-
let ready_timestamp = get_timestamp(e, operation_id);
117-
let current_timestamp = e.ledger().timestamp();
117+
let ready_ledger = get_operation_ledger(e, operation_id);
118+
let current_ledger = e.ledger().sequence();
118119

119-
match ready_timestamp {
120-
UNSET_TIMESTAMP => OperationState::Unset,
121-
DONE_TIMESTAMP => OperationState::Done,
122-
ready if ready > current_timestamp => OperationState::Waiting,
120+
match ready_ledger {
121+
UNSET_LEDGER => OperationState::Unset,
122+
DONE_LEDGER => OperationState::Done,
123+
ready if ready > current_ledger => OperationState::Waiting,
123124
_ => OperationState::Ready,
124125
}
125126
}
@@ -172,7 +173,7 @@ pub fn is_operation_done(e: &Env, operation_id: &BytesN<32>) -> bool {
172173
/// # Arguments
173174
///
174175
/// * `e` - Access to Soroban environment.
175-
/// * `min_delay` - The new minimum delay in seconds.
176+
/// * `min_delay` - The new minimum delay in ledgers.
176177
///
177178
/// # Events
178179
///
@@ -196,7 +197,7 @@ pub fn set_min_delay(e: &Env, min_delay: u32) {
196197
///
197198
/// * `e` - Access to Soroban environment.
198199
/// * `operation` - The operation to schedule.
199-
/// * `delay` - The delay in seconds before the operation can be executed.
200+
/// * `delay` - The delay in ledgers before the operation can be executed.
200201
///
201202
/// # Returns
202203
///
@@ -235,11 +236,11 @@ pub fn schedule_operation(e: &Env, operation: &Operation, delay: u32) -> BytesN<
235236
panic_with_error!(e, TimelockError::InsufficientDelay);
236237
}
237238

238-
let current_timestamp = e.ledger().timestamp();
239-
let ready_timestamp = current_timestamp + (delay as u64);
239+
let current_ledger = e.ledger().sequence();
240+
let ready_ledger = current_ledger.saturating_add(delay);
240241

241-
let key = TimelockStorageKey::Timestamp(id.clone());
242-
e.storage().persistent().set(&key, &ready_timestamp);
242+
let key = TimelockStorageKey::OperationLedger(id.clone());
243+
e.storage().persistent().set(&key, &ready_ledger);
243244

244245
emit_operation_scheduled(
245246
e,
@@ -337,8 +338,8 @@ pub fn set_execute_operation(e: &Env, operation: &Operation) {
337338
panic_with_error!(e, TimelockError::UnexecutedPredecessor);
338339
}
339340

340-
let key = TimelockStorageKey::Timestamp(id.clone());
341-
e.storage().persistent().set(&key, &DONE_TIMESTAMP);
341+
let key = TimelockStorageKey::OperationLedger(id.clone());
342+
e.storage().persistent().set(&key, &DONE_LEDGER);
342343

343344
emit_operation_executed(
344345
e,
@@ -377,7 +378,7 @@ pub fn cancel_operation(e: &Env, operation_id: &BytesN<32>) {
377378
panic_with_error!(e, TimelockError::InvalidOperationState);
378379
}
379380

380-
let key = TimelockStorageKey::Timestamp(operation_id.clone());
381+
let key = TimelockStorageKey::OperationLedger(operation_id.clone());
381382
e.storage().persistent().remove(&key);
382383

383384
emit_operation_cancelled(e, operation_id);

0 commit comments

Comments
 (0)