Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 149 additions & 6 deletions contracts/src/governance.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,158 @@
use crate::errors::SavingsError;
use crate::rewards::storage::get_user_rewards;
use soroban_sdk::{Address, Env};
use crate::storage_types::DataKey;
use soroban_sdk::{contracttype, Address, Env, String, Vec};

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Proposal {
pub id: u64,
pub creator: Address,
pub description: String,
pub start_time: u64,
pub end_time: u64,
pub executed: bool,
pub for_votes: u128,
pub against_votes: u128,
pub abstain_votes: u128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VotingConfig {
pub quorum: u32,
pub voting_period: u64,
pub timelock_duration: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum GovernanceKey {
Proposal(u64),
NextProposalId,
VotingConfig,
AllProposals,
}

/// Calculates voting power for a user based on their lifetime deposited funds
pub fn get_voting_power(env: &Env, user: &Address) -> u128 {
let rewards = get_user_rewards(env, user.clone());
rewards.lifetime_deposited.max(0) as u128
}

/// Casts a weighted vote (placeholder for future implementation)
/// Creates a new governance proposal
pub fn create_proposal(
env: &Env,
creator: Address,
description: String,
) -> Result<u64, SavingsError> {
creator.require_auth();

let config = get_voting_config(env)?;
let proposal_id = get_next_proposal_id(env);
let now = env.ledger().timestamp();

let proposal = Proposal {
id: proposal_id,
creator: creator.clone(),
description,
start_time: now,
end_time: now + config.voting_period,
executed: false,
for_votes: 0,
against_votes: 0,
abstain_votes: 0,
};

env.storage()
.persistent()
.set(&GovernanceKey::Proposal(proposal_id), &proposal);

let mut all_proposals: Vec<u64> = env
.storage()
.persistent()
.get(&GovernanceKey::AllProposals)
.unwrap_or(Vec::new(env));
all_proposals.push_back(proposal_id);
env.storage()
.persistent()
.set(&GovernanceKey::AllProposals, &all_proposals);

env.storage()
.persistent()
.set(&GovernanceKey::NextProposalId, &(proposal_id + 1));

env.events().publish(
(soroban_sdk::symbol_short!("proposal"), creator, proposal_id),
(),
);

Ok(proposal_id)
}

/// Gets a proposal by ID
pub fn get_proposal(env: &Env, proposal_id: u64) -> Option<Proposal> {
env.storage()
.persistent()
.get(&GovernanceKey::Proposal(proposal_id))
}

/// Lists all proposal IDs
pub fn list_proposals(env: &Env) -> Vec<u64> {
env.storage()
.persistent()
.get(&GovernanceKey::AllProposals)
.unwrap_or(Vec::new(env))
}

/// Gets the voting configuration
pub fn get_voting_config(env: &Env) -> Result<VotingConfig, SavingsError> {
env.storage()
.persistent()
.get(&GovernanceKey::VotingConfig)
.ok_or(SavingsError::InternalError)
}

/// Initializes voting configuration (admin only)
pub fn init_voting_config(
env: &Env,
admin: Address,
config: VotingConfig,
) -> Result<(), SavingsError> {
admin.require_auth();

let stored_admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(SavingsError::Unauthorized)?;

if admin != stored_admin {
return Err(SavingsError::Unauthorized);
}

if env.storage().persistent().has(&GovernanceKey::VotingConfig) {
return Err(SavingsError::ConfigAlreadyInitialized);
}

env.storage()
.persistent()
.set(&GovernanceKey::VotingConfig, &config);
env.storage()
.persistent()
.set(&GovernanceKey::NextProposalId, &1u64);

Ok(())
}

fn get_next_proposal_id(env: &Env) -> u64 {
env.storage()
.persistent()
.get(&GovernanceKey::NextProposalId)
.unwrap_or(1)
}

/// Casts a weighted vote on a proposal
pub fn cast_vote(
env: &Env,
user: Address,
Expand All @@ -17,16 +161,15 @@ pub fn cast_vote(
) -> Result<(), SavingsError> {
user.require_auth();
let weight = get_voting_power(env, &user);

if weight == 0 {
return Err(SavingsError::InsufficientBalance);
}

// TODO: Store vote with weight

env.events().publish(
(soroban_sdk::symbol_short!("vote"), user, proposal_id),
(support, weight),
);

Ok(())
}
107 changes: 100 additions & 7 deletions contracts/src/governance_tests.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
#[cfg(test)]
mod governance_tests {
use crate::{NesteraContract, NesteraContractClient, PlanType};
use crate::governance::VotingConfig;
use crate::rewards::storage_types::RewardsConfig;
use soroban_sdk::{
testutils::Address as _,
Address, BytesN, Env,
};
use crate::{NesteraContract, NesteraContractClient, PlanType};
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String};

fn setup_contract() -> (Env, NesteraContractClient<'static>, Address) {
let env = Env::default();
Expand Down Expand Up @@ -37,7 +35,7 @@ mod governance_tests {
fn test_voting_power_zero_for_new_user() {
let (env, client, _) = setup_contract();
let user = Address::generate(&env);

let power = client.get_voting_power(&user);
assert_eq!(power, 0);
}
Expand Down Expand Up @@ -76,7 +74,7 @@ mod governance_tests {
env.mock_all_auths();

client.initialize_user(&user);

let result = client.try_cast_vote(&user, &1, &true);
assert!(result.is_err());
}
Expand All @@ -93,4 +91,99 @@ mod governance_tests {
let result = client.try_cast_vote(&user, &1, &true);
assert!(result.is_ok());
}

#[test]
fn test_init_voting_config() {
let (env, client, admin) = setup_contract();
env.mock_all_auths();

let result = client.try_init_voting_config(&admin, &5000, &604800, &86400);
assert!(result.is_ok());

let config = client.try_get_voting_config().unwrap().unwrap();
assert_eq!(config.quorum, 5000);
assert_eq!(config.voting_period, 604800);
assert_eq!(config.timelock_duration, 86400);
}

#[test]
fn test_create_proposal() {
let (env, client, admin) = setup_contract();
env.mock_all_auths();

let _ = client.init_voting_config(&admin, &5000, &604800, &86400);

let creator = Address::generate(&env);
let description = String::from_str(&env, "Test proposal");
let proposal_id = client
.try_create_proposal(&creator, &description)
.unwrap()
.unwrap();

assert_eq!(proposal_id, 1);
}

#[test]
fn test_get_proposal() {
let (env, client, admin) = setup_contract();
env.mock_all_auths();

let _ = client.init_voting_config(&admin, &5000, &604800, &86400);

let creator = Address::generate(&env);
let description = String::from_str(&env, "Test proposal");
let proposal_id = client
.try_create_proposal(&creator, &description)
.unwrap()
.unwrap();

let proposal = client.get_proposal(&proposal_id).unwrap();
assert_eq!(proposal.id, 1);
assert_eq!(proposal.creator, creator);
assert_eq!(proposal.executed, false);
assert_eq!(proposal.for_votes, 0);
assert_eq!(proposal.against_votes, 0);
}

#[test]
fn test_list_proposals() {
let (env, client, admin) = setup_contract();
env.mock_all_auths();

let _ = client.init_voting_config(&admin, &5000, &604800, &86400);

let creator = Address::generate(&env);
let desc1 = String::from_str(&env, "Proposal 1");
let desc2 = String::from_str(&env, "Proposal 2");

let _ = client.try_create_proposal(&creator, &desc1);
let _ = client.try_create_proposal(&creator, &desc2);

let proposals = client.list_proposals();
assert_eq!(proposals.len(), 2);
assert_eq!(proposals.get(0).unwrap(), 1);
assert_eq!(proposals.get(1).unwrap(), 2);
}

#[test]
fn test_proposal_stored_correctly() {
let (env, client, admin) = setup_contract();
env.mock_all_auths();

let _ = client.init_voting_config(&admin, &5000, &604800, &86400);

let creator = Address::generate(&env);
let description = String::from_str(&env, "Store test");
let proposal_id = client
.try_create_proposal(&creator, &description)
.unwrap()
.unwrap();

let proposal = client.get_proposal(&proposal_id).unwrap();
let now = env.ledger().timestamp();

assert_eq!(proposal.description, description);
assert_eq!(proposal.start_time, now);
assert_eq!(proposal.end_time, now + 604800);
}
}
40 changes: 40 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,46 @@ impl NesteraContract {

// ========== Governance Functions ==========

/// Initializes voting configuration (admin only)
pub fn init_voting_config(
env: Env,
admin: Address,
quorum: u32,
voting_period: u64,
timelock_duration: u64,
) -> Result<(), SavingsError> {
let config = governance::VotingConfig {
quorum,
voting_period,
timelock_duration,
};
governance::init_voting_config(&env, admin, config)
}

/// Gets the voting configuration
pub fn get_voting_config(env: Env) -> Result<governance::VotingConfig, SavingsError> {
governance::get_voting_config(&env)
}

/// Creates a new governance proposal
pub fn create_proposal(
env: Env,
creator: Address,
description: String,
) -> Result<u64, SavingsError> {
governance::create_proposal(&env, creator, description)
}

/// Gets a proposal by ID
pub fn get_proposal(env: Env, proposal_id: u64) -> Option<governance::Proposal> {
governance::get_proposal(&env, proposal_id)
}

/// Lists all proposal IDs
pub fn list_proposals(env: Env) -> Vec<u64> {
governance::list_proposals(&env)
}

/// Gets the voting power for a user based on their lifetime deposited funds
pub fn get_voting_power(env: Env, user: Address) -> u128 {
governance::get_voting_power(&env, &user)
Expand Down