Skip to content
Closed
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
1 change: 1 addition & 0 deletions contracts/sorosave/src/contribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub fn contribute(env: &Env, member: Address, group_id: u64) -> Result<(), Contr
}

storage::set_round(env, group_id, &round_info);
storage::increment_on_time_contributions(env, &member);

env.events().publish(
(crate::symbol_short!("contrib"),),
Expand Down
5 changes: 5 additions & 0 deletions contracts/sorosave/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ impl SoroSaveContract {
group::get_member_groups(&env, member)
}

/// Get the aggregate on-chain reputation for a member.
pub fn get_reputation(env: Env, member: Address) -> MemberReputation {
storage::get_member_reputation(&env, &member)
}

// ─── Contributions ──────────────────────────────────────────────

/// Contribute to the current round of a group.
Expand Down
3 changes: 3 additions & 0 deletions contracts/sorosave/src/payout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pub fn distribute_payout(env: &Env, group_id: u64) -> Result<(), ContractError>
if group.current_round >= group.total_rounds {
group.status = GroupStatus::Completed;
storage::set_group(env, &group);
for member in group.members.iter() {
storage::increment_groups_completed(env, &member);
}

env.events()
.publish((crate::symbol_short!("grp_comp"),), group_id);
Expand Down
37 changes: 36 additions & 1 deletion contracts/sorosave/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use soroban_sdk::{Address, Env, Vec};

use crate::types::{DataKey, Dispute, RoundInfo, SavingsGroup};
use crate::types::{DataKey, Dispute, MemberReputation, RoundInfo, SavingsGroup};

const INSTANCE_TTL_THRESHOLD: u32 = 100;
const INSTANCE_TTL_EXTEND: u32 = 500;
Expand Down Expand Up @@ -103,6 +103,41 @@ pub fn remove_member_group(env: &Env, member: &Address, group_id: u64) {
extend_persistent_ttl(env, &key);
}

// --- Member Reputation ---

pub fn get_member_reputation(env: &Env, member: &Address) -> MemberReputation {
let key = DataKey::MemberReputation(member.clone());
let result = env.storage().persistent().get(&key);
if let Some(reputation) = result {
extend_persistent_ttl(env, &key);
reputation
} else {
MemberReputation {
groups_completed: 0,
on_time_contributions: 0,
defaults: 0,
}
}
}

pub fn set_member_reputation(env: &Env, member: &Address, reputation: &MemberReputation) {
let key = DataKey::MemberReputation(member.clone());
env.storage().persistent().set(&key, reputation);
extend_persistent_ttl(env, &key);
}

pub fn increment_on_time_contributions(env: &Env, member: &Address) {
let mut reputation = get_member_reputation(env, member);
reputation.on_time_contributions += 1;
set_member_reputation(env, member, &reputation);
}

pub fn increment_groups_completed(env: &Env, member: &Address) {
let mut reputation = get_member_reputation(env, member);
reputation.groups_completed += 1;
set_member_reputation(env, member, &reputation);
}

// --- Dispute ---

#[allow(dead_code)]
Expand Down
82 changes: 81 additions & 1 deletion contracts/sorosave/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use soroban_sdk::{testutils::Address as _, token::StellarAssetClient, Address, Env, String};

use crate::types::GroupStatus;
use crate::types::{GroupStatus, MemberReputation};
use crate::{SoroSaveContract, SoroSaveContractClient};

fn setup_env() -> (Env, Address, SoroSaveContractClient<'static>, Address) {
Expand Down Expand Up @@ -38,6 +38,13 @@ fn create_test_group(
)
}

fn mint_test_tokens(env: &Env, token: &Address, recipients: &[Address], amount: i128) {
let token_client = StellarAssetClient::new(env, token);
for recipient in recipients {
token_client.mint(recipient, &amount);
}
}

#[test]
fn test_create_group() {
let (env, admin, client, token) = setup_env();
Expand Down Expand Up @@ -222,3 +229,76 @@ fn test_set_group_admin() {
let group = client.get_group(&group_id);
assert_eq!(group.admin, new_admin);
}

#[test]
fn test_reputation_defaults_to_zero() {
let (env, _admin, client, _token) = setup_env();
let outsider = Address::generate(&env);

assert_eq!(
client.get_reputation(&outsider),
MemberReputation {
groups_completed: 0,
on_time_contributions: 0,
defaults: 0,
}
);
}

#[test]
fn test_reputation_updates_after_contributions_and_completion() {
let (env, admin, client, token) = setup_env();
let member1 = Address::generate(&env);
let member2 = Address::generate(&env);

mint_test_tokens(
&env,
&token,
&[admin.clone(), member1.clone(), member2.clone()],
10_000_000,
);

let group_id = client.create_group(
&admin,
&String::from_str(&env, "Reputation Group"),
&token,
&1_000_000,
&86400,
&3,
);

client.join_group(&member1, &group_id);
client.join_group(&member2, &group_id);
client.start_group(&admin, &group_id);

for round in 1..=3 {
client.contribute(&admin, &group_id);
client.contribute(&member1, &group_id);
client.contribute(&member2, &group_id);

assert_eq!(
client.get_reputation(&admin).on_time_contributions,
round
);
assert_eq!(
client.get_reputation(&member1).on_time_contributions,
round
);
assert_eq!(
client.get_reputation(&member2).on_time_contributions,
round
);

client.distribute_payout(&group_id);
}

let completed_reputation = MemberReputation {
groups_completed: 1,
on_time_contributions: 3,
defaults: 0,
};

assert_eq!(client.get_reputation(&admin), completed_reputation.clone());
assert_eq!(client.get_reputation(&member1), completed_reputation.clone());
assert_eq!(client.get_reputation(&member2), completed_reputation);
}
10 changes: 10 additions & 0 deletions contracts/sorosave/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ pub struct Dispute {
pub raised_at: u64,
}

/// Aggregate contribution history for a member across groups.
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct MemberReputation {
pub groups_completed: u32,
pub on_time_contributions: u32,
pub defaults: u32,
}

/// Storage keys for all contract data.
#[contracttype]
#[derive(Clone)]
Expand All @@ -60,5 +69,6 @@ pub enum DataKey {
Group(u64),
Round(u64, u32),
MemberGroups(Address),
MemberReputation(Address),
Dispute(u64),
}