Skip to content
Open
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
38 changes: 38 additions & 0 deletions contracts/sorosave/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@ use crate::errors::ContractError;
use crate::storage;
use crate::types::{Dispute, GroupStatus};

pub fn set_protocol_fee_bps(env: &Env, admin: Address, bps: u32) -> Result<(), ContractError> {
admin.require_auth();

if admin != storage::get_admin(env) {
return Err(ContractError::Unauthorized);
}

if bps > storage::MAX_PROTOCOL_FEE_BPS {
return Err(ContractError::InvalidAmount);
}

storage::set_protocol_fee_bps(env, bps);

env.events()
.publish((crate::symbol_short!("fee_set"),), bps);

Ok(())
}

pub fn set_protocol_treasury(
env: &Env,
admin: Address,
treasury: Address,
) -> Result<(), ContractError> {
admin.require_auth();

if admin != storage::get_admin(env) {
return Err(ContractError::Unauthorized);
}

storage::set_protocol_treasury(env, &treasury);

env.events()
.publish((crate::symbol_short!("treasury"),), treasury);

Ok(())
}

pub fn pause_group(env: &Env, admin: Address, group_id: u64) -> Result<(), ContractError> {
admin.require_auth();

Expand Down
24 changes: 24 additions & 0 deletions contracts/sorosave/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,30 @@ impl SoroSaveContract {
) -> Result<(), ContractError> {
admin::set_group_admin(&env, current_admin, group_id, new_admin)
}

/// Get the protocol fee charged on payouts, in basis points.
pub fn get_protocol_fee_bps(env: Env) -> u32 {
storage::get_protocol_fee_bps(&env)
}

/// Set the protocol fee charged on payouts, in basis points.
pub fn set_protocol_fee_bps(env: Env, admin: Address, bps: u32) -> Result<(), ContractError> {
admin::set_protocol_fee_bps(&env, admin, bps)
}

/// Get the protocol treasury address that receives payout fees.
pub fn get_protocol_treasury(env: Env) -> Address {
storage::get_protocol_treasury(&env)
}

/// Set the protocol treasury address that receives payout fees.
pub fn set_protocol_treasury(
env: Env,
admin: Address,
treasury: Address,
) -> Result<(), ContractError> {
admin::set_protocol_treasury(&env, admin, treasury)
}
}

#[cfg(test)]
Expand Down
34 changes: 23 additions & 11 deletions contracts/sorosave/src/payout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,33 @@ pub fn distribute_payout(env: &Env, group_id: u64) -> Result<(), ContractError>
return Err(ContractError::RoundNotComplete);
}

// Transfer the pot to the round's recipient
// Split the completed pot into protocol fee and recipient payout.
let token_client = soroban_sdk::token::Client::new(env, &group.token);
token_client.transfer(
&env.current_contract_address(),
&round_info.recipient,
&round_info.total_contributed,
);
let contract_addr = env.current_contract_address();
let fee_bps = storage::get_protocol_fee_bps(env);
let fee_denominator = storage::MAX_PROTOCOL_FEE_BPS as i128;
let fee_bps_i128 = fee_bps as i128;
let fee_amount = (round_info.total_contributed / fee_denominator) * fee_bps_i128
+ (round_info.total_contributed % fee_denominator) * fee_bps_i128 / fee_denominator;
let payout_amount = round_info.total_contributed - fee_amount;

if fee_amount > 0 {
let treasury = storage::get_protocol_treasury(env);
token_client.transfer(&contract_addr, &treasury, &fee_amount);

env.events().publish(
(crate::symbol_short!("fee_paid"),),
(group_id, treasury, fee_amount),
);
}

if payout_amount > 0 {
token_client.transfer(&contract_addr, &round_info.recipient, &payout_amount);
}

env.events().publish(
(crate::symbol_short!("payout"),),
(
group_id,
round_info.recipient.clone(),
round_info.total_contributed,
),
(group_id, round_info.recipient.clone(), payout_amount),
);

// Advance to next round or complete the group
Expand Down
29 changes: 29 additions & 0 deletions contracts/sorosave/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const INSTANCE_TTL_THRESHOLD: u32 = 100;
const INSTANCE_TTL_EXTEND: u32 = 500;
const PERSISTENT_TTL_THRESHOLD: u32 = 100;
const PERSISTENT_TTL_EXTEND: u32 = 1000;
pub const MAX_PROTOCOL_FEE_BPS: u32 = 10_000;

// --- Admin ---

Expand All @@ -22,6 +23,34 @@ pub fn has_admin(env: &Env) -> bool {
env.storage().instance().has(&DataKey::Admin)
}

// --- Protocol Config ---

pub fn get_protocol_fee_bps(env: &Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::ProtocolFeeBps)
.unwrap_or(0)
}

pub fn set_protocol_fee_bps(env: &Env, bps: u32) {
env.storage().instance().set(&DataKey::ProtocolFeeBps, &bps);
extend_instance_ttl(env);
}

pub fn get_protocol_treasury(env: &Env) -> Address {
env.storage()
.instance()
.get(&DataKey::ProtocolTreasury)
.unwrap_or_else(|| get_admin(env))
}

pub fn set_protocol_treasury(env: &Env, treasury: &Address) {
env.storage()
.instance()
.set(&DataKey::ProtocolTreasury, treasury);
extend_instance_ttl(env);
}

// --- Group Counter ---

pub fn get_group_counter(env: &Env) -> u64 {
Expand Down
82 changes: 80 additions & 2 deletions contracts/sorosave/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use soroban_sdk::{testutils::Address as _, token::StellarAssetClient, Address, Env, String};
use soroban_sdk::{
testutils::Address as _,
token::{Client as TokenClient, StellarAssetClient},
Address, Env, String,
};

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

fn setup_env() -> (Env, Address, SoroSaveContractClient<'static>, Address) {
let env = Env::default();
Expand Down Expand Up @@ -222,3 +226,77 @@ fn test_set_group_admin() {
let group = client.get_group(&group_id);
assert_eq!(group.admin, new_admin);
}

#[test]
fn test_protocol_fee_defaults_and_admin_updates() {
let (env, admin, client, _) = setup_env();
let member = Address::generate(&env);
let treasury = Address::generate(&env);

assert_eq!(client.get_protocol_fee_bps(), 0);
assert_eq!(client.get_protocol_treasury(), admin);

assert_eq!(
client.try_set_protocol_fee_bps(&member, &50),
Err(Ok(ContractError::Unauthorized))
);
assert_eq!(
client.try_set_protocol_treasury(&member, &treasury),
Err(Ok(ContractError::Unauthorized))
);
assert_eq!(
client.try_set_protocol_fee_bps(&admin, &10_001),
Err(Ok(ContractError::InvalidAmount))
);

client.set_protocol_fee_bps(&admin, &50);
client.set_protocol_treasury(&admin, &treasury);

assert_eq!(client.get_protocol_fee_bps(), 50);
assert_eq!(client.get_protocol_treasury(), treasury);
}

#[test]
fn test_protocol_fee_deducted_from_payout() {
let env = Env::default();
env.mock_all_auths();

let admin = Address::generate(&env);
let member = Address::generate(&env);
let treasury = Address::generate(&env);

let contract_id = env.register(SoroSaveContract, (&admin,));
let client = SoroSaveContractClient::new(&env, &contract_id);

let token_admin = Address::generate(&env);
let token_id = env.register_stellar_asset_contract_v2(token_admin);
let token_sac = StellarAssetClient::new(&env, &token_id.address());
token_sac.mint(&admin, &10_000_000);
token_sac.mint(&member, &10_000_000);

client.set_protocol_fee_bps(&admin, &50);
client.set_protocol_treasury(&admin, &treasury);

let group_id = client.create_group(
&admin,
&String::from_str(&env, "Protocol Fee Test"),
&token_id.address(),
&1_000_000,
&86400,
&5,
);
client.join_group(&member, &group_id);
client.start_group(&admin, &group_id);

client.contribute(&admin, &group_id);
client.contribute(&member, &group_id);
client.distribute_payout(&group_id);

let token_client = TokenClient::new(&env, &token_id.address());
assert_eq!(token_client.balance(&treasury), 10_000);
assert_eq!(token_client.balance(&admin), 10_990_000);
assert_eq!(token_client.balance(&member), 9_000_000);

let group = client.get_group(&group_id);
assert_eq!(group.current_round, 2);
}
2 changes: 2 additions & 0 deletions contracts/sorosave/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ pub struct Dispute {
#[derive(Clone)]
pub enum DataKey {
Admin,
ProtocolFeeBps,
ProtocolTreasury,
GroupCounter,
Group(u64),
Round(u64, u32),
Expand Down