Skip to main content

ruvector_dag/qudag/tokens/
governance.rs

1//! Governance Voting System
2
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct Proposal {
7    pub id: String,
8    pub title: String,
9    pub description: String,
10    pub proposer: String,
11    pub created_at: std::time::Instant,
12    pub voting_ends: std::time::Duration,
13    pub proposal_type: ProposalType,
14    pub status: ProposalStatus,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum ProposalType {
19    ParameterChange,
20    PatternPolicy,
21    RewardAdjustment,
22    ProtocolUpgrade,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum ProposalStatus {
27    Active,
28    Passed,
29    Failed,
30    Executed,
31    Cancelled,
32}
33
34#[derive(Debug, Clone)]
35pub struct GovernanceVote {
36    pub voter: String,
37    pub proposal_id: String,
38    pub vote: VoteChoice,
39    pub weight: f64,
40    pub timestamp: std::time::Instant,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum VoteChoice {
45    For,
46    Against,
47    Abstain,
48}
49
50pub struct GovernanceSystem {
51    proposals: HashMap<String, Proposal>,
52    votes: HashMap<String, Vec<GovernanceVote>>,
53    quorum_threshold: f64,   // Minimum participation (e.g., 0.1 = 10%)
54    approval_threshold: f64, // Minimum approval (e.g., 0.67 = 67%)
55}
56
57impl GovernanceSystem {
58    pub fn new(quorum_threshold: f64, approval_threshold: f64) -> Self {
59        Self {
60            proposals: HashMap::new(),
61            votes: HashMap::new(),
62            quorum_threshold,
63            approval_threshold,
64        }
65    }
66
67    pub fn create_proposal(
68        &mut self,
69        title: String,
70        description: String,
71        proposer: String,
72        proposal_type: ProposalType,
73        voting_duration: std::time::Duration,
74    ) -> String {
75        let id = format!("prop_{}", rand::random::<u64>());
76
77        let proposal = Proposal {
78            id: id.clone(),
79            title,
80            description,
81            proposer,
82            created_at: std::time::Instant::now(),
83            voting_ends: voting_duration,
84            proposal_type,
85            status: ProposalStatus::Active,
86        };
87
88        self.proposals.insert(id.clone(), proposal);
89        self.votes.insert(id.clone(), Vec::new());
90
91        id
92    }
93
94    pub fn vote(
95        &mut self,
96        voter: String,
97        proposal_id: &str,
98        choice: VoteChoice,
99        stake_weight: f64,
100    ) -> Result<(), GovernanceError> {
101        let proposal = self
102            .proposals
103            .get(proposal_id)
104            .ok_or(GovernanceError::ProposalNotFound)?;
105
106        if proposal.status != ProposalStatus::Active {
107            return Err(GovernanceError::ProposalNotActive);
108        }
109
110        if proposal.created_at.elapsed() > proposal.voting_ends {
111            return Err(GovernanceError::VotingEnded);
112        }
113
114        // Check if already voted
115        let votes = self.votes.get_mut(proposal_id).unwrap();
116        if votes.iter().any(|v| v.voter == voter) {
117            return Err(GovernanceError::AlreadyVoted);
118        }
119
120        votes.push(GovernanceVote {
121            voter,
122            proposal_id: proposal_id.to_string(),
123            vote: choice,
124            weight: stake_weight,
125            timestamp: std::time::Instant::now(),
126        });
127
128        Ok(())
129    }
130
131    pub fn tally(&self, proposal_id: &str, total_stake: f64) -> Option<VoteTally> {
132        let votes = self.votes.get(proposal_id)?;
133
134        let mut for_weight = 0.0;
135        let mut against_weight = 0.0;
136        let mut abstain_weight = 0.0;
137
138        for vote in votes {
139            match vote.vote {
140                VoteChoice::For => for_weight += vote.weight,
141                VoteChoice::Against => against_weight += vote.weight,
142                VoteChoice::Abstain => abstain_weight += vote.weight,
143            }
144        }
145
146        let total_voted = for_weight + against_weight + abstain_weight;
147        let participation = total_voted / total_stake;
148        let approval = if for_weight + against_weight > 0.0 {
149            for_weight / (for_weight + against_weight)
150        } else {
151            0.0
152        };
153
154        let quorum_met = participation >= self.quorum_threshold;
155        let approved = approval >= self.approval_threshold && quorum_met;
156
157        Some(VoteTally {
158            for_weight,
159            against_weight,
160            abstain_weight,
161            participation,
162            approval,
163            quorum_met,
164            approved,
165        })
166    }
167
168    pub fn finalize(
169        &mut self,
170        proposal_id: &str,
171        total_stake: f64,
172    ) -> Result<ProposalStatus, GovernanceError> {
173        // First, validate the proposal without holding a mutable borrow
174        {
175            let proposal = self
176                .proposals
177                .get(proposal_id)
178                .ok_or(GovernanceError::ProposalNotFound)?;
179
180            if proposal.status != ProposalStatus::Active {
181                return Err(GovernanceError::ProposalNotActive);
182            }
183
184            if proposal.created_at.elapsed() < proposal.voting_ends {
185                return Err(GovernanceError::VotingNotEnded);
186            }
187        }
188
189        // Calculate tally (immutable borrow)
190        let tally = self
191            .tally(proposal_id, total_stake)
192            .ok_or(GovernanceError::ProposalNotFound)?;
193
194        let new_status = if tally.approved {
195            ProposalStatus::Passed
196        } else {
197            ProposalStatus::Failed
198        };
199
200        // Now update the status (mutable borrow)
201        let proposal = self.proposals.get_mut(proposal_id).unwrap();
202        proposal.status = new_status;
203        Ok(new_status)
204    }
205
206    pub fn get_proposal(&self, proposal_id: &str) -> Option<&Proposal> {
207        self.proposals.get(proposal_id)
208    }
209
210    pub fn active_proposals(&self) -> Vec<&Proposal> {
211        self.proposals
212            .values()
213            .filter(|p| p.status == ProposalStatus::Active)
214            .collect()
215    }
216}
217
218#[derive(Debug, Clone)]
219pub struct VoteTally {
220    pub for_weight: f64,
221    pub against_weight: f64,
222    pub abstain_weight: f64,
223    pub participation: f64,
224    pub approval: f64,
225    pub quorum_met: bool,
226    pub approved: bool,
227}
228
229#[derive(Debug, thiserror::Error)]
230pub enum GovernanceError {
231    #[error("Proposal not found")]
232    ProposalNotFound,
233    #[error("Proposal not active")]
234    ProposalNotActive,
235    #[error("Voting has ended")]
236    VotingEnded,
237    #[error("Voting has not ended")]
238    VotingNotEnded,
239    #[error("Already voted")]
240    AlreadyVoted,
241    #[error("Insufficient stake to propose")]
242    InsufficientStake,
243}
244
245impl Default for GovernanceSystem {
246    fn default() -> Self {
247        Self::new(0.1, 0.67) // 10% quorum, 67% approval
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use std::time::Duration;
255
256    #[test]
257    fn test_proposal_creation() {
258        let mut gov = GovernanceSystem::default();
259        let id = gov.create_proposal(
260            "Test".to_string(),
261            "Description".to_string(),
262            "proposer1".to_string(),
263            ProposalType::ParameterChange,
264            Duration::from_secs(86400),
265        );
266
267        let proposal = gov.get_proposal(&id).unwrap();
268        assert_eq!(proposal.title, "Test");
269        assert_eq!(proposal.status, ProposalStatus::Active);
270    }
271
272    #[test]
273    fn test_voting() {
274        let mut gov = GovernanceSystem::default();
275        let id = gov.create_proposal(
276            "Test".to_string(),
277            "Description".to_string(),
278            "proposer1".to_string(),
279            ProposalType::ParameterChange,
280            Duration::from_secs(86400),
281        );
282
283        // First vote succeeds
284        assert!(gov
285            .vote("voter1".to_string(), &id, VoteChoice::For, 100.0)
286            .is_ok());
287
288        // Duplicate vote fails
289        assert!(matches!(
290            gov.vote("voter1".to_string(), &id, VoteChoice::For, 50.0),
291            Err(GovernanceError::AlreadyVoted)
292        ));
293    }
294
295    #[test]
296    fn test_tally() {
297        let mut gov = GovernanceSystem::new(0.1, 0.5);
298        let id = gov.create_proposal(
299            "Test".to_string(),
300            "Description".to_string(),
301            "proposer1".to_string(),
302            ProposalType::ParameterChange,
303            Duration::from_secs(86400),
304        );
305
306        gov.vote("voter1".to_string(), &id, VoteChoice::For, 700.0)
307            .unwrap();
308        gov.vote("voter2".to_string(), &id, VoteChoice::Against, 300.0)
309            .unwrap();
310
311        let tally = gov.tally(&id, 10000.0).unwrap();
312        assert_eq!(tally.for_weight, 700.0);
313        assert_eq!(tally.against_weight, 300.0);
314        assert_eq!(tally.participation, 0.1); // 1000/10000
315        assert_eq!(tally.approval, 0.7); // 700/1000
316        assert!(tally.quorum_met);
317        assert!(tally.approved);
318    }
319
320    #[test]
321    fn test_quorum_not_met() {
322        let mut gov = GovernanceSystem::new(0.5, 0.67);
323        let id = gov.create_proposal(
324            "Test".to_string(),
325            "Description".to_string(),
326            "proposer1".to_string(),
327            ProposalType::ParameterChange,
328            Duration::from_secs(86400),
329        );
330
331        gov.vote("voter1".to_string(), &id, VoteChoice::For, 100.0)
332            .unwrap();
333
334        let tally = gov.tally(&id, 10000.0).unwrap();
335        assert!(!tally.quorum_met); // Only 1% participation
336        assert!(!tally.approved);
337    }
338}