use crate::error::{CoreError, Result};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SuccessMetric {
TradingVolume,
TotalValueLocked,
ActiveUsers,
AvgTransactionValue,
PlatformRevenue,
TokenPrice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalMarket {
pub market_id: String,
pub proposal_id: String,
pub condition: bool,
pub predicted_value: Decimal,
pub liquidity: Decimal,
pub positions: HashMap<String, MarketPosition>,
pub resolved: bool,
pub actual_value: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketPosition {
pub user_id: String,
pub stake: Decimal,
pub predicted_value: Decimal,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FutarchyProposal {
pub proposal_id: String,
pub title: String,
pub description: String,
pub metric: SuccessMetric,
pub pass_market: ConditionalMarket,
pub fail_market: ConditionalMarket,
pub prediction_end: chrono::DateTime<chrono::Utc>,
pub measurement_end: chrono::DateTime<chrono::Utc>,
pub status: FutarchyProposalStatus,
pub decision: Option<bool>,
pub creator: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FutarchyProposalStatus {
PredictionPhase,
Decided,
AwaitingOutcome,
Resolved,
Cancelled,
}
pub struct FutarchyGovernor {
pub min_liquidity: Decimal,
pub min_prediction_period: i64,
pub measurement_period_days: i64,
}
impl FutarchyGovernor {
pub fn new(
min_liquidity: Decimal,
min_prediction_period: i64,
measurement_period_days: i64,
) -> Self {
Self {
min_liquidity,
min_prediction_period,
measurement_period_days,
}
}
pub fn create_proposal(
&self,
title: String,
description: String,
metric: SuccessMetric,
creator: String,
prediction_period_hours: i64,
) -> Result<FutarchyProposal> {
if prediction_period_hours < self.min_prediction_period {
return Err(CoreError::Validation(format!(
"Prediction period must be at least {} hours",
self.min_prediction_period
)));
}
let now = chrono::Utc::now();
let proposal_id = Uuid::new_v4().to_string();
let pass_market = ConditionalMarket {
market_id: format!("{}-pass", proposal_id),
proposal_id: proposal_id.clone(),
condition: true,
predicted_value: Decimal::ZERO,
liquidity: Decimal::ZERO,
positions: HashMap::new(),
resolved: false,
actual_value: None,
};
let fail_market = ConditionalMarket {
market_id: format!("{}-fail", proposal_id),
proposal_id: proposal_id.clone(),
condition: false,
predicted_value: Decimal::ZERO,
liquidity: Decimal::ZERO,
positions: HashMap::new(),
resolved: false,
actual_value: None,
};
Ok(FutarchyProposal {
proposal_id,
title,
description,
metric,
pass_market,
fail_market,
prediction_end: now + chrono::Duration::hours(prediction_period_hours),
measurement_end: now
+ chrono::Duration::hours(prediction_period_hours)
+ chrono::Duration::days(self.measurement_period_days),
status: FutarchyProposalStatus::PredictionPhase,
decision: None,
creator,
created_at: now,
})
}
pub fn place_bet(
&self,
proposal: &mut FutarchyProposal,
user_id: String,
condition: bool,
stake: Decimal,
predicted_value: Decimal,
) -> Result<()> {
if proposal.status != FutarchyProposalStatus::PredictionPhase {
return Err(CoreError::InvalidState(
"Proposal is not in prediction phase".to_string(),
));
}
if chrono::Utc::now() > proposal.prediction_end {
return Err(CoreError::InvalidState(
"Prediction period has ended".to_string(),
));
}
if stake <= Decimal::ZERO {
return Err(CoreError::Validation("Stake must be positive".to_string()));
}
let market = if condition {
&mut proposal.pass_market
} else {
&mut proposal.fail_market
};
market.liquidity += stake;
let total_stake = market.positions.values().map(|p| p.stake).sum::<Decimal>() + stake;
let weighted_sum = market
.positions
.values()
.map(|p| p.stake * p.predicted_value)
.sum::<Decimal>()
+ stake * predicted_value;
market.predicted_value = weighted_sum / total_stake;
let position = MarketPosition {
user_id: user_id.clone(),
stake,
predicted_value,
timestamp: chrono::Utc::now(),
};
market.positions.insert(user_id, position);
Ok(())
}
pub fn make_decision(&self, proposal: &mut FutarchyProposal) -> Result<bool> {
if proposal.status != FutarchyProposalStatus::PredictionPhase {
return Err(CoreError::InvalidState(
"Proposal is not in prediction phase".to_string(),
));
}
if chrono::Utc::now() < proposal.prediction_end {
return Err(CoreError::InvalidState(
"Prediction period has not ended yet".to_string(),
));
}
if proposal.pass_market.liquidity < self.min_liquidity
|| proposal.fail_market.liquidity < self.min_liquidity
{
proposal.status = FutarchyProposalStatus::Cancelled;
return Err(CoreError::InvalidState(
"Insufficient liquidity in markets".to_string(),
));
}
let decision = proposal.pass_market.predicted_value > proposal.fail_market.predicted_value;
proposal.decision = Some(decision);
proposal.status = FutarchyProposalStatus::Decided;
Ok(decision)
}
pub fn resolve_markets(
&self,
proposal: &mut FutarchyProposal,
actual_value: Decimal,
) -> Result<()> {
if proposal.status != FutarchyProposalStatus::Decided
&& proposal.status != FutarchyProposalStatus::AwaitingOutcome
{
return Err(CoreError::InvalidState(
"Proposal must be decided before resolution".to_string(),
));
}
if chrono::Utc::now() < proposal.measurement_end {
proposal.status = FutarchyProposalStatus::AwaitingOutcome;
return Err(CoreError::InvalidState(
"Measurement period has not ended yet".to_string(),
));
}
let decision = proposal
.decision
.ok_or_else(|| CoreError::InvalidState("No decision made".to_string()))?;
if decision {
proposal.pass_market.resolved = true;
proposal.pass_market.actual_value = Some(actual_value);
} else {
proposal.fail_market.resolved = true;
proposal.fail_market.actual_value = Some(actual_value);
}
proposal.status = FutarchyProposalStatus::Resolved;
Ok(())
}
pub fn calculate_payout(&self, market: &ConditionalMarket, user_id: &str) -> Result<Decimal> {
if !market.resolved {
return Err(CoreError::InvalidState(
"Market not resolved yet".to_string(),
));
}
let actual_value = market
.actual_value
.ok_or_else(|| CoreError::InvalidState("Actual value not set".to_string()))?;
let position = market
.positions
.get(user_id)
.ok_or_else(|| CoreError::NotFound("User has no position".to_string()))?;
let user_error = (position.predicted_value - actual_value).abs();
let mut total_weighted_stake = Decimal::ZERO;
for pos in market.positions.values() {
let error = (pos.predicted_value - actual_value).abs();
let weight = if error == Decimal::ZERO {
Decimal::from(1000) } else {
Decimal::ONE / error
};
total_weighted_stake += pos.stake * weight;
}
let user_weight = if user_error == Decimal::ZERO {
Decimal::from(1000)
} else {
Decimal::ONE / user_error
};
let user_weighted_stake = position.stake * user_weight;
let payout = (user_weighted_stake / total_weighted_stake) * market.liquidity;
Ok(payout)
}
pub fn get_market_stats(&self, market: &ConditionalMarket) -> MarketStats {
let num_participants = market.positions.len();
let avg_prediction = if num_participants > 0 {
market.predicted_value
} else {
Decimal::ZERO
};
let predictions: Vec<Decimal> = market
.positions
.values()
.map(|p| p.predicted_value)
.collect();
let variance = if predictions.len() > 1 {
let mean = avg_prediction;
let sum_squared_diff: Decimal =
predictions.iter().map(|&p| (p - mean) * (p - mean)).sum();
sum_squared_diff / Decimal::from(predictions.len())
} else {
Decimal::ZERO
};
MarketStats {
liquidity: market.liquidity,
num_participants,
predicted_value: market.predicted_value,
prediction_variance: variance,
resolved: market.resolved,
actual_value: market.actual_value,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketStats {
pub liquidity: Decimal,
pub num_participants: usize,
pub predicted_value: Decimal,
pub prediction_variance: Decimal,
pub resolved: bool,
pub actual_value: Option<Decimal>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_proposal() {
let governor = FutarchyGovernor::new(
Decimal::from(100), 24, 30, );
let proposal = governor
.create_proposal(
"Reduce trading fees".to_string(),
"Reduce trading fees by 50%".to_string(),
SuccessMetric::TradingVolume,
"creator123".to_string(),
48, )
.unwrap();
assert_eq!(proposal.title, "Reduce trading fees");
assert_eq!(proposal.status, FutarchyProposalStatus::PredictionPhase);
assert!(proposal.pass_market.liquidity == Decimal::ZERO);
assert!(proposal.fail_market.liquidity == Decimal::ZERO);
}
#[test]
fn test_place_bet() {
let governor = FutarchyGovernor::new(Decimal::from(100), 24, 30);
let mut proposal = governor
.create_proposal(
"Test proposal".to_string(),
"Test".to_string(),
SuccessMetric::TradingVolume,
"creator".to_string(),
48,
)
.unwrap();
governor
.place_bet(
&mut proposal,
"alice".to_string(),
true, Decimal::from(50),
Decimal::from(1000), )
.unwrap();
assert_eq!(proposal.pass_market.liquidity, Decimal::from(50));
assert_eq!(proposal.pass_market.predicted_value, Decimal::from(1000));
governor
.place_bet(
&mut proposal,
"bob".to_string(),
true,
Decimal::from(50),
Decimal::from(1200), )
.unwrap();
assert_eq!(proposal.pass_market.liquidity, Decimal::from(100));
assert_eq!(proposal.pass_market.predicted_value, Decimal::from(1100));
}
#[test]
fn test_make_decision() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut proposal = governor
.create_proposal(
"Test proposal".to_string(),
"Test".to_string(),
SuccessMetric::TradingVolume,
"creator".to_string(),
1, )
.unwrap();
proposal.prediction_end = chrono::Utc::now() - chrono::Duration::hours(1);
proposal.pass_market.liquidity = Decimal::from(150);
proposal.pass_market.predicted_value = Decimal::from(1200);
proposal.fail_market.liquidity = Decimal::from(150);
proposal.fail_market.predicted_value = Decimal::from(1000);
let decision = governor.make_decision(&mut proposal).unwrap();
assert!(decision); assert_eq!(proposal.status, FutarchyProposalStatus::Decided);
assert_eq!(proposal.decision, Some(true));
}
#[test]
fn test_resolve_markets() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut proposal = governor
.create_proposal(
"Test proposal".to_string(),
"Test".to_string(),
SuccessMetric::TradingVolume,
"creator".to_string(),
1,
)
.unwrap();
proposal.status = FutarchyProposalStatus::Decided;
proposal.decision = Some(true); proposal.measurement_end = chrono::Utc::now() - chrono::Duration::hours(1);
governor
.resolve_markets(&mut proposal, Decimal::from(1150))
.unwrap();
assert_eq!(proposal.status, FutarchyProposalStatus::Resolved);
assert!(proposal.pass_market.resolved);
assert_eq!(proposal.pass_market.actual_value, Some(Decimal::from(1150)));
assert!(!proposal.fail_market.resolved); }
#[test]
fn test_calculate_payout() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut market = ConditionalMarket {
market_id: "test".to_string(),
proposal_id: "test".to_string(),
condition: true,
predicted_value: Decimal::from(1100),
liquidity: Decimal::from(200),
positions: HashMap::new(),
resolved: true,
actual_value: Some(Decimal::from(1150)), };
market.positions.insert(
"alice".to_string(),
MarketPosition {
user_id: "alice".to_string(),
stake: Decimal::from(100),
predicted_value: Decimal::from(1200),
timestamp: chrono::Utc::now(),
},
);
market.positions.insert(
"bob".to_string(),
MarketPosition {
user_id: "bob".to_string(),
stake: Decimal::from(100),
predicted_value: Decimal::from(1100),
timestamp: chrono::Utc::now(),
},
);
let alice_payout = governor.calculate_payout(&market, "alice").unwrap();
let bob_payout = governor.calculate_payout(&market, "bob").unwrap();
assert_eq!(alice_payout, Decimal::from(100));
assert_eq!(bob_payout, Decimal::from(100));
assert_eq!(alice_payout + bob_payout, market.liquidity);
}
#[test]
fn test_market_stats() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut market = ConditionalMarket {
market_id: "test".to_string(),
proposal_id: "test".to_string(),
condition: true,
predicted_value: Decimal::from(1100),
liquidity: Decimal::from(200),
positions: HashMap::new(),
resolved: false,
actual_value: None,
};
market.positions.insert(
"alice".to_string(),
MarketPosition {
user_id: "alice".to_string(),
stake: Decimal::from(100),
predicted_value: Decimal::from(1200),
timestamp: chrono::Utc::now(),
},
);
market.positions.insert(
"bob".to_string(),
MarketPosition {
user_id: "bob".to_string(),
stake: Decimal::from(100),
predicted_value: Decimal::from(1000),
timestamp: chrono::Utc::now(),
},
);
let stats = governor.get_market_stats(&market);
assert_eq!(stats.liquidity, Decimal::from(200));
assert_eq!(stats.num_participants, 2);
assert_eq!(stats.predicted_value, Decimal::from(1100));
assert!(!stats.resolved);
assert_eq!(stats.actual_value, None);
}
#[test]
fn test_insufficient_liquidity() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut proposal = governor
.create_proposal(
"Test proposal".to_string(),
"Test".to_string(),
SuccessMetric::TradingVolume,
"creator".to_string(),
1,
)
.unwrap();
proposal.prediction_end = chrono::Utc::now() - chrono::Duration::hours(1);
proposal.pass_market.liquidity = Decimal::from(50); proposal.fail_market.liquidity = Decimal::from(150);
let result = governor.make_decision(&mut proposal);
assert!(result.is_err());
assert_eq!(proposal.status, FutarchyProposalStatus::Cancelled);
}
#[test]
fn test_decision_based_on_better_prediction() {
let governor = FutarchyGovernor::new(Decimal::from(100), 0, 30);
let mut proposal = governor
.create_proposal(
"Test proposal".to_string(),
"Test".to_string(),
SuccessMetric::TradingVolume,
"creator".to_string(),
1,
)
.unwrap();
proposal.prediction_end = chrono::Utc::now() - chrono::Duration::hours(1);
proposal.pass_market.liquidity = Decimal::from(150);
proposal.pass_market.predicted_value = Decimal::from(1000);
proposal.fail_market.liquidity = Decimal::from(150);
proposal.fail_market.predicted_value = Decimal::from(1500);
let decision = governor.make_decision(&mut proposal).unwrap();
assert!(!decision); assert_eq!(proposal.decision, Some(false));
}
}