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
418 changes: 403 additions & 15 deletions contracts/src/governance.rs

Large diffs are not rendered by default.

111 changes: 102 additions & 9 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,8 +74,8 @@ mod governance_tests {
env.mock_all_auths();

client.initialize_user(&user);
let result = client.try_cast_vote(&user, &1, &true);

let result = client.try_vote(&1, &1, &user);
assert!(result.is_err());
}

Expand All @@ -90,7 +88,102 @@ mod governance_tests {
client.initialize_user(&user);
let _ = client.create_savings_plan(&user, &PlanType::Flexi, &1000);

let result = client.try_cast_vote(&user, &1, &true);
let result = client.try_vote(&1, &1, &user);
assert!(result.is_err());
}

#[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);
}
}
144 changes: 99 additions & 45 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,29 +415,25 @@ impl NesteraContract {
Ok(())
}

pub fn set_flexi_rate(env: Env, rate: i128) -> Result<(), SavingsError> {
let admin = env.storage().instance().get(&DataKey::Admin).unwrap();
let admin_address: Address = admin; // Type casting for clarity, though get returns generic
admin_address.require_auth();
rates::set_flexi_rate(&env, rate)
pub fn set_flexi_rate(env: Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
rates::set_flexi_rate(&env, caller, rate)
}

pub fn set_goal_rate(env: Env, rate: i128) -> Result<(), SavingsError> {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
rates::set_goal_rate(&env, rate)
pub fn set_goal_rate(env: Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
rates::set_goal_rate(&env, caller, rate)
}

pub fn set_group_rate(env: Env, rate: i128) -> Result<(), SavingsError> {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
rates::set_group_rate(&env, rate)
pub fn set_group_rate(env: Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
rates::set_group_rate(&env, caller, rate)
}

pub fn set_lock_rate(env: Env, duration_days: u64, rate: i128) -> Result<(), SavingsError> {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();
rates::set_lock_rate(&env, duration_days, rate)
pub fn set_lock_rate(
env: Env,
caller: Address,
duration_days: u64,
rate: i128,
) -> Result<(), SavingsError> {
rates::set_lock_rate(&env, caller, duration_days, rate)
}

pub fn set_early_break_fee_bps(env: Env, bps: u32) -> Result<(), SavingsError> {
Expand Down Expand Up @@ -474,39 +470,23 @@ impl NesteraContract {
Ok(())
}

pub fn pause(env: Env, admin: Address) -> Result<(), SavingsError> {
admin.require_auth();
let stored_admin: Option<Address> = env.storage().instance().get(&DataKey::Admin);

// Use .clone() here so 'admin' isn't moved
if stored_admin != Some(admin.clone()) {
return Err(SavingsError::Unauthorized);
}
pub fn pause(env: Env, caller: Address) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(&env, &caller)?;

env.storage().persistent().set(&DataKey::Paused, &true);

// Extend TTL on config update
ttl::extend_config_ttl(&env, &DataKey::Paused);

env.events().publish((symbol_short!("pause"), admin), ());
env.events().publish((symbol_short!("pause"), caller), ());
Ok(())
}

pub fn unpause(env: Env, admin: Address) -> Result<(), SavingsError> {
admin.require_auth();
let stored_admin: Option<Address> = env.storage().instance().get(&DataKey::Admin);

// Use .clone() here too
if stored_admin != Some(admin.clone()) {
return Err(SavingsError::Unauthorized);
}
pub fn unpause(env: Env, caller: Address) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(&env, &caller)?;

env.storage().persistent().set(&DataKey::Paused, &false);

// Extend TTL on config update
ttl::extend_config_ttl(&env, &DataKey::Paused);

env.events().publish((symbol_short!("unpause"), admin), ());
env.events().publish((symbol_short!("unpause"), caller), ());
Ok(())
}

Expand Down Expand Up @@ -776,19 +756,89 @@ 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)
}

/// Creates a governance proposal with an action
pub fn create_action_proposal(
env: Env,
creator: Address,
description: String,
action: governance::ProposalAction,
) -> Result<u64, SavingsError> {
governance::create_action_proposal(&env, creator, description, action)
}

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

/// Gets an action proposal by ID
pub fn get_action_proposal(env: Env, proposal_id: u64) -> Option<governance::ActionProposal> {
governance::get_action_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)
}

/// Casts a weighted vote on a proposal
pub fn cast_vote(
pub fn vote(
env: Env,
user: Address,
proposal_id: u64,
support: bool,
vote_type: u32,
voter: Address,
) -> Result<(), SavingsError> {
governance::cast_vote(&env, user, proposal_id, support)
governance::vote(&env, proposal_id, vote_type, voter)
}

/// Checks if a user has voted on a proposal
pub fn has_voted(env: Env, proposal_id: u64, voter: Address) -> bool {
governance::has_voted(&env, proposal_id, &voter)
}

/// Activates governance (admin only, one-time)
pub fn activate_governance(env: Env, admin: Address) -> Result<(), SavingsError> {
governance::activate_governance(&env, admin)
}

/// Checks if governance is active
pub fn is_governance_active(env: Env) -> bool {
governance::is_governance_active(&env)
}
}

Expand All @@ -803,4 +853,8 @@ mod rates_test;
#[cfg(test)]
mod test;
#[cfg(test)]
mod transition_tests;
#[cfg(test)]
mod ttl_tests;
#[cfg(test)]
mod voting_tests;
30 changes: 24 additions & 6 deletions contracts/src/rates.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
use crate::governance;
use crate::storage_types::DataKey;
use crate::SavingsError;
use soroban_sdk::Env;
use soroban_sdk::{Address, Env};

// --- Admin Setters ---
// --- Admin Setters (with governance transition) ---

pub fn set_flexi_rate(env: &Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(env, &caller)?;

pub fn set_flexi_rate(env: &Env, rate: i128) -> Result<(), SavingsError> {
if rate < 0 {
return Err(SavingsError::InvalidInterestRate);
}
env.storage().instance().set(&DataKey::FlexiRate, &rate);
Ok(())
}

pub fn set_goal_rate(env: &Env, rate: i128) -> Result<(), SavingsError> {
pub fn set_goal_rate(env: &Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(env, &caller)?;

if rate < 0 {
return Err(SavingsError::InvalidInterestRate);
}
env.storage().instance().set(&DataKey::GoalRate, &rate);
Ok(())
}

pub fn set_group_rate(env: &Env, rate: i128) -> Result<(), SavingsError> {
pub fn set_group_rate(env: &Env, caller: Address, rate: i128) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(env, &caller)?;

if rate < 0 {
return Err(SavingsError::InvalidInterestRate);
}
env.storage().instance().set(&DataKey::GroupRate, &rate);
Ok(())
}

pub fn set_lock_rate(env: &Env, duration_days: u64, rate: i128) -> Result<(), SavingsError> {
pub fn set_lock_rate(
env: &Env,
caller: Address,
duration_days: u64,
rate: i128,
) -> Result<(), SavingsError> {
caller.require_auth();
governance::validate_admin_or_governance(env, &caller)?;

if rate < 0 {
return Err(SavingsError::InvalidInterestRate);
}
Expand Down
Loading