Skip to content

Commit bd76d1a

Browse files
perf(staking)+docs(governance): transient reward snapshots and governance constitution (#686)
* perf(staking): transient reward snapshots updated only on withdraw calls * docs(governance): add constitutional rules for quorum, timelocks, and voting lifecycle
1 parent 2d27c44 commit bd76d1a

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#[derive(Clone, Default)]
2+
pub struct RewardSnapshot {
3+
pub staker: [u8; 32],
4+
pub accrued_per_token: u128,
5+
pub last_updated_block: u64,
6+
}
7+
8+
impl RewardSnapshot {
9+
pub fn new(staker: [u8; 32]) -> Self {
10+
Self { staker, accrued_per_token: 0, last_updated_block: 0 }
11+
}
12+
13+
pub fn pending(&self, global_accrued: u128, staked: u128) -> u128 {
14+
global_accrued
15+
.saturating_sub(self.accrued_per_token)
16+
.saturating_mul(staked)
17+
.saturating_div(1_000_000_000)
18+
}
19+
20+
pub fn flush(&mut self, global_accrued: u128, block: u64) {
21+
self.accrued_per_token = global_accrued;
22+
self.last_updated_block = block;
23+
}
24+
}
25+
26+
#[cfg(test)]
27+
mod tests {
28+
use super::*;
29+
const S: [u8; 32] = [1u8; 32];
30+
31+
#[test]
32+
fn pending_before_flush_is_positive() {
33+
let snap = RewardSnapshot { staker: S, accrued_per_token: 0, last_updated_block: 0 };
34+
assert!(snap.pending(500_000_000, 2_000_000_000) > 0);
35+
}
36+
37+
#[test]
38+
fn no_pending_after_flush() {
39+
let mut snap = RewardSnapshot::new(S);
40+
snap.flush(500_000_000, 100);
41+
assert_eq!(snap.pending(500_000_000, 1_000_000_000), 0);
42+
}
43+
44+
#[test]
45+
fn rewards_accumulate_between_flushes() {
46+
let mut snap = RewardSnapshot::new(S);
47+
snap.flush(100_000_000, 1);
48+
assert!(snap.pending(200_000_000, 1_000_000_000) > 0);
49+
}
50+
51+
#[test]
52+
fn zero_stake_yields_no_pending() {
53+
let snap = RewardSnapshot::new(S);
54+
assert_eq!(snap.pending(1_000_000_000, 0), 0);
55+
}
56+
}

docs/governance-constitution.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Governance Constitution
2+
3+
## Quorum & Voting Period
4+
5+
| Parameter | Value |
6+
|--------------------|------------------------------------|
7+
| Quorum | 10% of total voting power |
8+
| Voting Period | 28,800 blocks (~2 days at 6s/block)|
9+
| Approval Threshold | >50% of votes cast |
10+
11+
## Timelocks
12+
13+
| Proposal Type | Minimum Timelock |
14+
|------------------|-----------------|
15+
| Parameter Change | 1 day |
16+
| Fund Release | 2 days |
17+
| Emergency Pause | 0 (immediate) |
18+
19+
## Proposal Lifecycle
20+
21+
1. **Creation** — Staker with ≥1% of supply submits proposal; voting power snapshotted at this block.
22+
2. **Voting** — Open for the voting period; only snapshot-block balances count.
23+
3. **Quorum Check** — Participation must reach 10% before voting closes.
24+
4. **Timelock** — Approved proposals wait the configured timelock before execution.
25+
5. **Execution** — Anyone may trigger execution after timelock expires.
26+
6. **Rejection** — Proposals failing quorum or threshold are rejected and cannot be re-submitted for 24 hours.
27+
28+
## Emergency Pause
29+
30+
Admin may invoke `emergency_override` to pause immediately. Resumption requires a governance vote with ≥60% approval.
31+
32+
## Edge Case Recovery
33+
34+
- **Stuck proposals**: Admin may cancel proposals open > 30 days with zero participation.
35+
- **Delegate unavailability**: Delegators may reclaim voting power at any block before voting closes.
36+
- **Quorum failure**: Proposal may be resubmitted after a 24-hour cooldown.

0 commit comments

Comments
 (0)