use crate::error::{CoreError, Result};
use chrono::{DateTime, Utc};
use rust_decimal::prelude::*;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct VoteDelegation {
pub delegation_id: Uuid,
pub delegator_user_id: Uuid,
pub delegate_user_id: Uuid,
pub voting_power: Decimal,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub revoked_at: Option<DateTime<Utc>>,
}
impl VoteDelegation {
pub fn new(
delegator_user_id: Uuid,
delegate_user_id: Uuid,
voting_power: Decimal,
duration_days: Option<u32>,
) -> Result<Self> {
if voting_power <= Decimal::ZERO {
return Err(CoreError::Validation(
"Voting power must be positive".to_string(),
));
}
if delegator_user_id == delegate_user_id {
return Err(CoreError::Validation(
"Cannot delegate to yourself".to_string(),
));
}
let start_time = Utc::now();
let end_time = duration_days.map(|days| start_time + chrono::Duration::days(days as i64));
Ok(Self {
delegation_id: Uuid::new_v4(),
delegator_user_id,
delegate_user_id,
voting_power,
start_time,
end_time,
is_active: true,
created_at: Utc::now(),
revoked_at: None,
})
}
pub fn is_valid(&self) -> bool {
let now = Utc::now();
self.is_active && now >= self.start_time && self.end_time.is_none_or(|end| now <= end)
}
pub fn revoke(&mut self) {
self.is_active = false;
self.revoked_at = Some(Utc::now());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct VoteEscrowLock {
pub lock_id: Uuid,
pub user_id: Uuid,
pub amount: Decimal,
pub lock_start: DateTime<Utc>,
pub lock_end: DateTime<Utc>,
pub voting_power: Decimal,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub unlocked_at: Option<DateTime<Utc>>,
}
impl VoteEscrowLock {
pub const MAX_LOCK_DAYS: i64 = 1461;
pub fn new(user_id: Uuid, amount: Decimal, lock_duration_days: i64) -> Result<Self> {
if amount <= Decimal::ZERO {
return Err(CoreError::Validation(
"Lock amount must be positive".to_string(),
));
}
if lock_duration_days <= 0 {
return Err(CoreError::Validation(
"Lock duration must be positive".to_string(),
));
}
if lock_duration_days > Self::MAX_LOCK_DAYS {
return Err(CoreError::Validation(format!(
"Lock duration cannot exceed {} days",
Self::MAX_LOCK_DAYS
)));
}
let lock_start = Utc::now();
let lock_end = lock_start + chrono::Duration::days(lock_duration_days);
let time_multiplier =
Decimal::from(lock_duration_days) / Decimal::from(Self::MAX_LOCK_DAYS);
let voting_power = amount * (Decimal::ONE + time_multiplier);
Ok(Self {
lock_id: Uuid::new_v4(),
user_id,
amount,
lock_start,
lock_end,
voting_power,
is_active: true,
created_at: Utc::now(),
unlocked_at: None,
})
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.lock_end
}
pub fn remaining_days(&self) -> i64 {
if self.is_expired() {
return 0;
}
(self.lock_end - Utc::now()).num_days()
}
pub fn current_voting_power(&self) -> Decimal {
if !self.is_active || self.is_expired() {
return Decimal::ZERO;
}
let remaining_days = self.remaining_days();
let time_multiplier = Decimal::from(remaining_days) / Decimal::from(Self::MAX_LOCK_DAYS);
self.amount * (Decimal::ONE + time_multiplier)
}
pub fn unlock(&mut self, early_unlock_penalty: Decimal) -> Result<Decimal> {
if !self.is_active {
return Err(CoreError::InvalidState("Lock is not active".to_string()));
}
self.is_active = false;
self.unlocked_at = Some(Utc::now());
if !self.is_expired() {
let penalty_amount = self.amount * early_unlock_penalty;
Ok(self.amount - penalty_amount)
} else {
Ok(self.amount)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct ConvictionVote {
pub vote_id: Uuid,
pub proposal_id: Uuid,
pub user_id: Uuid,
pub base_voting_power: Decimal,
pub vote_for: bool,
pub vote_time: DateTime<Utc>,
pub removed_at: Option<DateTime<Utc>>,
pub is_active: bool,
}
impl ConvictionVote {
pub fn new(
proposal_id: Uuid,
user_id: Uuid,
base_voting_power: Decimal,
vote_for: bool,
) -> Result<Self> {
if base_voting_power <= Decimal::ZERO {
return Err(CoreError::Validation(
"Voting power must be positive".to_string(),
));
}
Ok(Self {
vote_id: Uuid::new_v4(),
proposal_id,
user_id,
base_voting_power,
vote_for,
vote_time: Utc::now(),
removed_at: None,
is_active: true,
})
}
pub fn current_conviction(&self) -> Decimal {
if !self.is_active {
return Decimal::ZERO;
}
let days_active = (Utc::now() - self.vote_time).num_days() as f64;
if days_active < 0.0 {
return self.base_voting_power;
}
let time_factor = (days_active.sqrt() / 10.0).min(2.0); let multiplier = 1.0 + time_factor;
self.base_voting_power * Decimal::from_f64(multiplier).unwrap_or(Decimal::ONE)
}
pub fn remove(&mut self) {
self.is_active = false;
self.removed_at = Some(Utc::now());
}
pub fn conviction_after_removal(&self) -> Decimal {
let Some(removed_at) = self.removed_at else {
return self.current_conviction();
};
let days_since_removal = (Utc::now() - removed_at).num_days() as f64;
if days_since_removal < 0.0 {
return Decimal::ZERO;
}
let half_lives = days_since_removal / 7.0;
let decay_factor = 0.5_f64.powf(half_lives);
self.base_voting_power * Decimal::from_f64(decay_factor).unwrap_or(Decimal::ZERO)
}
}
pub struct DelegationManager {
delegations: HashMap<Uuid, VoteDelegation>,
delegator_index: HashMap<Uuid, Vec<Uuid>>,
delegate_index: HashMap<Uuid, Vec<Uuid>>,
}
impl DelegationManager {
pub fn new() -> Self {
Self {
delegations: HashMap::new(),
delegator_index: HashMap::new(),
delegate_index: HashMap::new(),
}
}
pub fn delegate(
&mut self,
delegator_user_id: Uuid,
delegate_user_id: Uuid,
voting_power: Decimal,
duration_days: Option<u32>,
) -> Result<Uuid> {
let delegation = VoteDelegation::new(
delegator_user_id,
delegate_user_id,
voting_power,
duration_days,
)?;
let delegation_id = delegation.delegation_id;
self.delegator_index
.entry(delegator_user_id)
.or_default()
.push(delegation_id);
self.delegate_index
.entry(delegate_user_id)
.or_default()
.push(delegation_id);
self.delegations.insert(delegation_id, delegation);
Ok(delegation_id)
}
pub fn revoke_delegation(&mut self, delegation_id: Uuid) -> Result<()> {
let delegation = self
.delegations
.get_mut(&delegation_id)
.ok_or_else(|| CoreError::NotFound("Delegation not found".to_string()))?;
delegation.revoke();
Ok(())
}
pub fn get_total_voting_power(&self, user_id: Uuid) -> Decimal {
let mut total = Decimal::ZERO;
if let Some(delegation_ids) = self.delegate_index.get(&user_id) {
for delegation_id in delegation_ids {
if let Some(delegation) = self.delegations.get(delegation_id) {
if delegation.is_valid() {
total += delegation.voting_power;
}
}
}
}
total
}
pub fn get_delegations_by_delegator(&self, user_id: Uuid) -> Vec<&VoteDelegation> {
self.delegator_index
.get(&user_id)
.map(|ids| {
ids.iter()
.filter_map(|id| self.delegations.get(id))
.collect()
})
.unwrap_or_default()
}
pub fn get_delegations_received(&self, user_id: Uuid) -> Vec<&VoteDelegation> {
self.delegate_index
.get(&user_id)
.map(|ids| {
ids.iter()
.filter_map(|id| self.delegations.get(id))
.filter(|d| d.is_valid())
.collect()
})
.unwrap_or_default()
}
}
impl Default for DelegationManager {
fn default() -> Self {
Self::new()
}
}
pub struct VoteEscrowManager {
locks: HashMap<Uuid, VoteEscrowLock>,
user_locks: HashMap<Uuid, Vec<Uuid>>,
early_unlock_penalty: Decimal,
}
impl VoteEscrowManager {
pub fn new(early_unlock_penalty: Decimal) -> Result<Self> {
if early_unlock_penalty < Decimal::ZERO || early_unlock_penalty > Decimal::ONE {
return Err(CoreError::Validation(
"Early unlock penalty must be between 0 and 1".to_string(),
));
}
Ok(Self {
locks: HashMap::new(),
user_locks: HashMap::new(),
early_unlock_penalty,
})
}
pub fn create_lock(
&mut self,
user_id: Uuid,
amount: Decimal,
lock_duration_days: i64,
) -> Result<Uuid> {
let lock = VoteEscrowLock::new(user_id, amount, lock_duration_days)?;
let lock_id = lock.lock_id;
self.user_locks.entry(user_id).or_default().push(lock_id);
self.locks.insert(lock_id, lock);
Ok(lock_id)
}
pub fn unlock(&mut self, lock_id: Uuid) -> Result<Decimal> {
let lock = self
.locks
.get_mut(&lock_id)
.ok_or_else(|| CoreError::NotFound("Lock not found".to_string()))?;
lock.unlock(self.early_unlock_penalty)
}
pub fn get_user_voting_power(&self, user_id: Uuid) -> Decimal {
self.user_locks
.get(&user_id)
.map(|lock_ids| {
lock_ids
.iter()
.filter_map(|id| self.locks.get(id))
.map(|lock| lock.current_voting_power())
.sum()
})
.unwrap_or(Decimal::ZERO)
}
pub fn get_user_locks(&self, user_id: Uuid) -> Vec<&VoteEscrowLock> {
self.user_locks
.get(&user_id)
.map(|ids| ids.iter().filter_map(|id| self.locks.get(id)).collect())
.unwrap_or_default()
}
pub fn get_lock(&self, lock_id: Uuid) -> Option<&VoteEscrowLock> {
self.locks.get(&lock_id)
}
}
pub struct ConvictionVotingManager {
votes: HashMap<Uuid, ConvictionVote>,
proposal_votes: HashMap<Uuid, Vec<Uuid>>,
user_votes: HashMap<Uuid, Vec<Uuid>>,
}
impl ConvictionVotingManager {
pub fn new() -> Self {
Self {
votes: HashMap::new(),
proposal_votes: HashMap::new(),
user_votes: HashMap::new(),
}
}
pub fn cast_vote(
&mut self,
proposal_id: Uuid,
user_id: Uuid,
base_voting_power: Decimal,
vote_for: bool,
) -> Result<Uuid> {
let vote = ConvictionVote::new(proposal_id, user_id, base_voting_power, vote_for)?;
let vote_id = vote.vote_id;
self.proposal_votes
.entry(proposal_id)
.or_default()
.push(vote_id);
self.user_votes.entry(user_id).or_default().push(vote_id);
self.votes.insert(vote_id, vote);
Ok(vote_id)
}
pub fn remove_vote(&mut self, vote_id: Uuid) -> Result<()> {
let vote = self
.votes
.get_mut(&vote_id)
.ok_or_else(|| CoreError::NotFound("Vote not found".to_string()))?;
vote.remove();
Ok(())
}
pub fn get_proposal_conviction(&self, proposal_id: Uuid) -> (Decimal, Decimal) {
let mut votes_for = Decimal::ZERO;
let mut votes_against = Decimal::ZERO;
if let Some(vote_ids) = self.proposal_votes.get(&proposal_id) {
for vote_id in vote_ids {
if let Some(vote) = self.votes.get(vote_id) {
let conviction = vote.current_conviction();
if vote.vote_for {
votes_for += conviction;
} else {
votes_against += conviction;
}
}
}
}
(votes_for, votes_against)
}
pub fn get_proposal_votes(&self, proposal_id: Uuid) -> Vec<&ConvictionVote> {
self.proposal_votes
.get(&proposal_id)
.map(|ids| ids.iter().filter_map(|id| self.votes.get(id)).collect())
.unwrap_or_default()
}
pub fn get_user_votes(&self, user_id: Uuid) -> Vec<&ConvictionVote> {
self.user_votes
.get(&user_id)
.map(|ids| ids.iter().filter_map(|id| self.votes.get(id)).collect())
.unwrap_or_default()
}
}
impl Default for ConvictionVotingManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuadraticVote {
pub vote_id: Uuid,
pub proposal_id: Uuid,
pub user_id: Uuid,
pub vote_count: u32,
pub credit_cost: Decimal,
pub in_favor: bool,
pub created_at: DateTime<Utc>,
}
impl QuadraticVote {
pub fn new(proposal_id: Uuid, user_id: Uuid, vote_count: u32, in_favor: bool) -> Result<Self> {
if vote_count == 0 {
return Err(CoreError::Validation(
"Vote count must be positive".to_string(),
));
}
let credit_cost = Decimal::from(vote_count) * Decimal::from(vote_count);
Ok(Self {
vote_id: Uuid::new_v4(),
proposal_id,
user_id,
vote_count,
credit_cost,
in_favor,
created_at: Utc::now(),
})
}
pub fn calculate_cost(vote_count: u32) -> Decimal {
Decimal::from(vote_count) * Decimal::from(vote_count)
}
pub fn votes_from_credits(credits: Decimal) -> u32 {
let credits_f64 = credits.to_f64().unwrap_or(0.0);
credits_f64.sqrt() as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuadraticVotingCredits {
pub user_id: Uuid,
pub total_credits: Decimal,
pub spent_credits: Decimal,
pub available_credits: Decimal,
pub last_allocation: DateTime<Utc>,
}
impl QuadraticVotingCredits {
pub fn new(user_id: Uuid, total_credits: Decimal) -> Self {
Self {
user_id,
total_credits,
spent_credits: Decimal::ZERO,
available_credits: total_credits,
last_allocation: Utc::now(),
}
}
pub fn spend(&mut self, amount: Decimal) -> Result<()> {
if amount > self.available_credits {
return Err(CoreError::Validation(
"Insufficient voting credits".to_string(),
));
}
self.spent_credits += amount;
self.available_credits -= amount;
Ok(())
}
pub fn allocate(&mut self, amount: Decimal) {
self.total_credits += amount;
self.available_credits += amount;
self.last_allocation = Utc::now();
}
pub fn refund(&mut self, amount: Decimal) {
self.spent_credits = (self.spent_credits - amount).max(Decimal::ZERO);
self.available_credits = (self.available_credits + amount).min(self.total_credits);
}
}
#[derive(Debug, Clone, Default)]
pub struct QuadraticVotingManager {
votes: HashMap<Uuid, Vec<QuadraticVote>>,
user_credits: HashMap<Uuid, QuadraticVotingCredits>,
proposal_votes: HashMap<Uuid, (u32, u32)>, }
impl QuadraticVotingManager {
pub fn new() -> Self {
Self::default()
}
pub fn allocate_credits(&mut self, user_id: Uuid, credits: Decimal) {
self.user_credits
.entry(user_id)
.and_modify(|c| c.allocate(credits))
.or_insert_with(|| QuadraticVotingCredits::new(user_id, credits));
}
pub fn cast_vote(
&mut self,
proposal_id: Uuid,
user_id: Uuid,
vote_count: u32,
in_favor: bool,
) -> Result<QuadraticVote> {
let vote = QuadraticVote::new(proposal_id, user_id, vote_count, in_favor)?;
let credits = self
.user_credits
.get_mut(&user_id)
.ok_or(CoreError::Validation(
"User has no voting credits".to_string(),
))?;
credits.spend(vote.credit_cost)?;
self.votes
.entry(proposal_id)
.or_default()
.push(vote.clone());
let (for_votes, against_votes) = self.proposal_votes.entry(proposal_id).or_insert((0, 0));
if in_favor {
*for_votes += vote_count;
} else {
*against_votes += vote_count;
}
Ok(vote)
}
pub fn get_user_credits(&self, user_id: Uuid) -> Option<&QuadraticVotingCredits> {
self.user_credits.get(&user_id)
}
pub fn get_proposal_votes(&self, proposal_id: Uuid) -> Vec<&QuadraticVote> {
self.votes
.get(&proposal_id)
.map(|votes| votes.iter().collect())
.unwrap_or_default()
}
pub fn get_proposal_totals(&self, proposal_id: Uuid) -> (u32, u32) {
self.proposal_votes
.get(&proposal_id)
.copied()
.unwrap_or((0, 0))
}
pub fn calculate_result(&self, proposal_id: Uuid) -> Decimal {
let (votes_for, votes_against) = self.get_proposal_totals(proposal_id);
let total_votes = votes_for + votes_against;
if total_votes == 0 {
return Decimal::ZERO;
}
(Decimal::from(votes_for) / Decimal::from(total_votes)) * Decimal::from(100)
}
pub fn detect_collusion(&self, proposal_id: Uuid) -> Vec<Uuid> {
let votes = self.get_proposal_votes(proposal_id);
let mut suspicious_users = Vec::new();
let mut vote_counts: HashMap<u32, Vec<Uuid>> = HashMap::new();
for vote in votes {
vote_counts
.entry(vote.vote_count)
.or_default()
.push(vote.user_id);
}
for (_count, users) in vote_counts {
if users.len() >= 3 {
suspicious_users.extend(users);
}
}
suspicious_users
}
pub fn calculate_sybil_score(&self, proposal_id: Uuid) -> Decimal {
let votes = self.get_proposal_votes(proposal_id);
if votes.is_empty() {
return Decimal::ZERO;
}
let total_votes: u32 = votes.iter().map(|v| v.vote_count).sum();
let num_voters = votes.len();
if total_votes == 0 || num_voters == 0 {
return Decimal::ZERO;
}
let avg_votes = Decimal::from(total_votes) / Decimal::from(num_voters);
let mut variance = Decimal::ZERO;
for vote in votes {
let diff = Decimal::from(vote.vote_count) - avg_votes;
variance += diff * diff;
}
variance /= Decimal::from(num_voters);
let score = Decimal::from(100) / (Decimal::from(1) + variance / Decimal::from(10));
score.min(Decimal::from(100))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_vote_delegation() {
let delegator = Uuid::new_v4();
let delegate = Uuid::new_v4();
let delegation = VoteDelegation::new(delegator, delegate, dec!(1000), Some(30)).unwrap();
assert!(delegation.is_valid());
assert_eq!(delegation.voting_power, dec!(1000));
}
#[test]
fn test_vote_delegation_revoke() {
let delegator = Uuid::new_v4();
let delegate = Uuid::new_v4();
let mut delegation =
VoteDelegation::new(delegator, delegate, dec!(1000), Some(30)).unwrap();
delegation.revoke();
assert!(!delegation.is_valid());
}
#[test]
fn test_vote_escrow_lock() {
let user = Uuid::new_v4();
let lock = VoteEscrowLock::new(user, dec!(1000), 365).unwrap();
assert!(lock.voting_power > dec!(1000)); assert!(!lock.is_expired());
}
#[test]
fn test_vote_escrow_voting_power() {
let user = Uuid::new_v4();
let lock = VoteEscrowLock::new(user, dec!(1000), VoteEscrowLock::MAX_LOCK_DAYS).unwrap();
assert!(lock.voting_power >= dec!(1500)); }
#[test]
fn test_conviction_vote() {
let proposal = Uuid::new_v4();
let user = Uuid::new_v4();
let vote = ConvictionVote::new(proposal, user, dec!(1000), true).unwrap();
let initial_conviction = vote.current_conviction();
assert_eq!(initial_conviction, dec!(1000)); }
#[test]
fn test_delegation_manager() {
let mut manager = DelegationManager::new();
let delegator = Uuid::new_v4();
let delegate = Uuid::new_v4();
let delegation_id = manager
.delegate(delegator, delegate, dec!(1000), Some(30))
.unwrap();
let total_power = manager.get_total_voting_power(delegate);
assert_eq!(total_power, dec!(1000));
manager.revoke_delegation(delegation_id).unwrap();
let total_power_after = manager.get_total_voting_power(delegate);
assert_eq!(total_power_after, dec!(0));
}
#[test]
fn test_vote_escrow_manager() {
let mut manager = VoteEscrowManager::new(dec!(0.5)).unwrap();
let user = Uuid::new_v4();
let lock_id = manager.create_lock(user, dec!(1000), 365).unwrap();
let voting_power = manager.get_user_voting_power(user);
assert!(voting_power > dec!(1000));
let unlocked = manager.unlock(lock_id).unwrap();
assert_eq!(unlocked, dec!(500)); }
#[test]
fn test_conviction_voting_manager() {
let mut manager = ConvictionVotingManager::new();
let proposal = Uuid::new_v4();
let user1 = Uuid::new_v4();
let user2 = Uuid::new_v4();
manager
.cast_vote(proposal, user1, dec!(1000), true)
.unwrap();
manager
.cast_vote(proposal, user2, dec!(500), false)
.unwrap();
let (votes_for, votes_against) = manager.get_proposal_conviction(proposal);
assert_eq!(votes_for, dec!(1000));
assert_eq!(votes_against, dec!(500));
}
#[test]
fn test_cannot_delegate_to_self() {
let user = Uuid::new_v4();
let result = VoteDelegation::new(user, user, dec!(1000), None);
assert!(result.is_err());
}
#[test]
fn test_vote_escrow_max_lock_validation() {
let user = Uuid::new_v4();
let result = VoteEscrowLock::new(user, dec!(1000), VoteEscrowLock::MAX_LOCK_DAYS + 1);
assert!(result.is_err());
}
}