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
88 changes: 88 additions & 0 deletions contracts/GOVERNANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Governance Weight / Token Integration

## Overview
This implementation provides voting power calculation for users based on their lifetime deposited funds in the Nestera savings protocol.

## Voting Weight Source
**Current Implementation:** Total deposited funds (lifetime_deposited)

The voting power is calculated from the `lifetime_deposited` field in the user's rewards data, which tracks the cumulative amount a user has deposited across all savings plans.

### Why Lifetime Deposits?
- **Fair representation**: Users who have contributed more to the protocol have proportionally more influence
- **Sybil resistance**: Prevents vote manipulation through multiple accounts
- **Already tracked**: Leverages existing rewards infrastructure
- **Simple & transparent**: Easy to understand and verify

## Functions

### `get_voting_power(user: Address) -> u128`
Returns the voting power for a given user address.

**Parameters:**
- `user`: The address of the user

**Returns:**
- `u128`: The voting power (equal to lifetime deposited amount)

**Example:**
```rust
let power = contract.get_voting_power(&user_address);
// power = 1500 if user has deposited 1500 tokens total
```

### `cast_vote(user: Address, proposal_id: u64, support: bool) -> Result<(), SavingsError>`
Casts a weighted vote on a proposal.

**Parameters:**
- `user`: The address of the voter (requires authentication)
- `proposal_id`: The ID of the proposal to vote on
- `support`: `true` for yes, `false` for no

**Returns:**
- `Ok(())` on success
- `Err(SavingsError::InsufficientBalance)` if user has no voting power

**Events Emitted:**
- `vote`: Contains (user, proposal_id, support, weight)

## Vote Scaling
Votes are automatically scaled by the user's voting power:
- User with 1000 deposited = 1000 voting power
- User with 5000 deposited = 5000 voting power
- User with 0 deposited = 0 voting power (cannot vote)

## Future Enhancements
The current implementation provides a foundation for:
1. **Reward points weighting**: Alternative voting power based on earned points
2. **Staked governance token**: Dedicated governance token for voting
3. **Proposal storage**: Full proposal lifecycle management
4. **Vote tallying**: Automated vote counting and execution
5. **Delegation**: Allow users to delegate voting power
6. **Quadratic voting**: Non-linear voting power calculation

## Testing
Run governance tests:
```bash
cd contracts
cargo test governance_tests
```

All tests verify:
- ✅ New users have zero voting power
- ✅ Voting power increases with deposits
- ✅ Voting power accumulates across multiple deposits
- ✅ Votes require non-zero voting power
- ✅ Votes succeed when user has voting power

## Integration
The governance module integrates seamlessly with:
- **Rewards system**: Uses existing `lifetime_deposited` tracking
- **User management**: Works with existing user initialization
- **Event system**: Emits vote events for off-chain tracking
- **Error handling**: Uses standard `SavingsError` types

## Contract Build Status
✅ Contract builds successfully
✅ All tests pass
✅ No breaking changes to existing functionality
32 changes: 32 additions & 0 deletions contracts/src/governance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::errors::SavingsError;
use crate::rewards::storage::get_user_rewards;
use soroban_sdk::{Address, Env};

/// 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)
pub fn cast_vote(
env: &Env,
user: Address,
proposal_id: u64,
support: bool,
) -> 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(())
}
96 changes: 96 additions & 0 deletions contracts/src/governance_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#[cfg(test)]
mod governance_tests {
use crate::{NesteraContract, NesteraContractClient, PlanType};
use crate::rewards::storage_types::RewardsConfig;
use soroban_sdk::{
testutils::Address as _,
Address, BytesN, Env,
};

fn setup_contract() -> (Env, NesteraContractClient<'static>, Address) {
let env = Env::default();
let contract_id = env.register(NesteraContract, ());
let client = NesteraContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let admin_pk = BytesN::from_array(&env, &[1u8; 32]);

env.mock_all_auths();
client.initialize(&admin, &admin_pk);

let config = RewardsConfig {
points_per_token: 10,
streak_bonus_bps: 0,
long_lock_bonus_bps: 0,
goal_completion_bonus: 0,
enabled: true,
min_deposit_for_rewards: 0,
action_cooldown_seconds: 0,
max_daily_points: 1_000_000,
max_streak_multiplier: 10_000,
};
let _ = client.initialize_rewards_config(&config);

(env, client, admin)
}

#[test]
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);
}

#[test]
fn test_voting_power_increases_with_deposits() {
let (env, client, _) = setup_contract();
let user = Address::generate(&env);
env.mock_all_auths();

client.initialize_user(&user);
let _ = client.create_savings_plan(&user, &PlanType::Flexi, &1000);

let power = client.get_voting_power(&user);
assert_eq!(power, 1000);
}

#[test]
fn test_voting_power_accumulates_across_deposits() {
let (env, client, _) = setup_contract();
let user = Address::generate(&env);
env.mock_all_auths();

client.initialize_user(&user);
let _ = client.create_savings_plan(&user, &PlanType::Flexi, &1000);
let _ = client.create_savings_plan(&user, &PlanType::Flexi, &500);

let power = client.get_voting_power(&user);
assert_eq!(power, 1500);
}

#[test]
fn test_cast_vote_requires_voting_power() {
let (env, client, _) = setup_contract();
let user = Address::generate(&env);
env.mock_all_auths();

client.initialize_user(&user);

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

#[test]
fn test_cast_vote_succeeds_with_voting_power() {
let (env, client, _) = setup_contract();
let user = Address::generate(&env);
env.mock_all_auths();

client.initialize_user(&user);
let _ = client.create_savings_plan(&user, &PlanType::Flexi, &1000);

let result = client.try_cast_vote(&user, &1, &true);
assert!(result.is_ok());
}
}
20 changes: 20 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod config;
mod errors;
mod flexi;
mod goal;
mod governance;
mod group;
mod invariants;
mod lock;
Expand Down Expand Up @@ -772,13 +773,32 @@ impl NesteraContract {
pub fn version(env: Env) -> u32 {
upgrade::get_version(&env)
}

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

/// 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(
env: Env,
user: Address,
proposal_id: u64,
support: bool,
) -> Result<(), SavingsError> {
governance::cast_vote(&env, user, proposal_id, support)
}
}

#[cfg(test)]
mod admin_tests;
#[cfg(test)]
mod config_tests;
#[cfg(test)]
mod governance_tests;
#[cfg(test)]
mod rates_test;
#[cfg(test)]
mod test;
Expand Down
Loading