Skip to content

Commit f72e71d

Browse files
authored
Merge pull request #766 from DevScoopee/main
Implement configurable platform fee on campaign claims with admin-controlled basis points (default 50 = 0.5%) and fee recipient address, plus pre-existing bug fixes.
2 parents 3e2dd95 + ab7931c commit f72e71d

2 files changed

Lines changed: 421 additions & 7 deletions

File tree

contracts/src/lib.rs

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const MIN_CONTRIBUTION: i128 = 100;
2020
/// multi-token donation campaigns while keeping storage costs predictable.
2121
const MAX_ACCEPTED_TOKENS: u32 = 10;
2222

23+
/// Default platform fee in basis points (50 = 0.5%). Admin can override
24+
/// via [`set_fee`]. Set to 0 to disable the fee mechanism entirely.
25+
const DEFAULT_PLATFORM_FEE_BPS: i128 = 50;
26+
2327
#[contracttype]
2428
#[derive(Clone, Debug, Eq, PartialEq)]
2529
pub struct Campaign {
@@ -54,6 +58,14 @@ pub enum DataKey {
5458
HasContributed(u64, Address), // (campaign_id, contributor)
5559
/// Tracks which (old_contract_id, campaign_id) pairs have already been migrated.
5660
MigratedId(Address, u64),
61+
/// Track contributor addresses for a campaign (used in refund_all).
62+
Contributors(u64),
63+
/// Platform fee in basis points (e.g. 50 = 0.5%). Defaults to
64+
/// [`DEFAULT_PLATFORM_FEE_BPS`] when absent. 0 disables the fee.
65+
PlatformFeeBps,
66+
/// Address that receives platform fees on campaign claims. When absent no
67+
/// fee is deducted regardless of [`PlatformFeeBps`].
68+
FeeRecipient,
5769
}
5870

5971
#[contracttype]
@@ -148,6 +160,16 @@ pub struct ExtensionRequested {
148160
pub new_deadline: u64,
149161
}
150162

163+
/// Emitted when a platform fee is deducted from a campaign claim.
164+
#[contracttype]
165+
#[derive(Clone, Debug, Eq, PartialEq)]
166+
pub struct FeeCollected {
167+
pub campaign_id: u64,
168+
pub token: Address,
169+
pub fee_amount: i128,
170+
pub fee_recipient: Address,
171+
}
172+
151173
#[contract]
152174
pub struct StellarGoalVaultContract;
153175

@@ -216,6 +238,55 @@ impl StellarGoalVaultContract {
216238
.unwrap_or_else(|| panic!("not initialized"))
217239
}
218240

241+
/// Sets the platform fee in basis points (e.g. 50 = 0.5%).
242+
/// Only the admin can call this. Pass 0 to disable the fee.
243+
pub fn set_fee(env: Env, admin: Address, bps: i128) {
244+
admin.require_auth();
245+
let stored_admin: Address = env
246+
.storage()
247+
.instance()
248+
.get(&DataKey::Admin)
249+
.unwrap_or_else(|| panic!("not initialized"));
250+
if admin != stored_admin {
251+
panic!("caller is not admin");
252+
}
253+
if bps < 0 {
254+
panic!("fee must be non-negative");
255+
}
256+
env.storage().instance().set(&DataKey::PlatformFeeBps, &bps);
257+
}
258+
259+
/// Sets the address that receives platform fees on campaign claims.
260+
/// Only the admin can call this.
261+
pub fn set_fee_recipient(env: Env, admin: Address, recipient: Address) {
262+
admin.require_auth();
263+
let stored_admin: Address = env
264+
.storage()
265+
.instance()
266+
.get(&DataKey::Admin)
267+
.unwrap_or_else(|| panic!("not initialized"));
268+
if admin != stored_admin {
269+
panic!("caller is not admin");
270+
}
271+
env.storage()
272+
.instance()
273+
.set(&DataKey::FeeRecipient, &recipient);
274+
}
275+
276+
/// Returns the current platform fee in basis points. Defaults to
277+
/// [`DEFAULT_PLATFORM_FEE_BPS`] (50) when not explicitly configured.
278+
pub fn get_platform_fee_bps(env: Env) -> i128 {
279+
env.storage()
280+
.instance()
281+
.get(&DataKey::PlatformFeeBps)
282+
.unwrap_or(DEFAULT_PLATFORM_FEE_BPS)
283+
}
284+
285+
/// Returns the fee recipient address, or `None` if not set.
286+
pub fn get_fee_recipient(env: Env) -> Option<Address> {
287+
env.storage().instance().get(&DataKey::FeeRecipient)
288+
}
289+
219290
/// Creator can cancel an active campaign, allowing contributors to refund.
220291
pub fn cancel_campaign(env: Env, campaign_id: u64, creator: Address) {
221292
require_not_paused(&env);
@@ -378,6 +449,11 @@ impl StellarGoalVaultContract {
378449
if !has_contributed {
379450
campaign.contributor_count += 1;
380451
env.storage().persistent().set(&has_contributed_key, &true);
452+
// Track contributor for refund_all
453+
let contributors_key = DataKey::Contributors(campaign_id);
454+
let mut contributors: Vec<Address> = env.storage().persistent().get(&contributors_key).unwrap_or_else(|| Vec::new(&env));
455+
contributors.push_back(contributor.clone());
456+
env.storage().persistent().set(&contributors_key, &contributors);
381457
}
382458

383459

@@ -393,6 +469,8 @@ impl StellarGoalVaultContract {
393469
.persistent()
394470
.set(&balance_key, &(current_balance + amount));
395471

472+
let contribution_key = DataKey::Contribution(campaign_id, contributor.clone(), token.clone());
473+
let current_contribution: i128 = env.storage().persistent().get(&contribution_key).unwrap_or(0);
396474
env.storage()
397475
.persistent()
398476
.set(&contribution_key, &(current_contribution + amount));
@@ -590,14 +668,45 @@ impl StellarGoalVaultContract {
590668

591669
let contract_address = env.current_contract_address();
592670

593-
// Transfer all accepted tokens to creator
671+
let fee_bps: i128 = env
672+
.storage()
673+
.instance()
674+
.get(&DataKey::PlatformFeeBps)
675+
.unwrap_or(DEFAULT_PLATFORM_FEE_BPS);
676+
let fee_recipient: Option<Address> = env.storage().instance().get(&DataKey::FeeRecipient);
677+
let take_fee = fee_bps > 0;
678+
594679
for token in campaign.accepted_tokens.iter() {
595680
let balance_key = DataKey::CampaignTokenBalance(campaign_id, token.clone());
596681
let balance: i128 = env.storage().persistent().get(&balance_key).unwrap_or(0);
597682

598683
if balance > 0 {
599684
let token_client = TokenClient::new(&env, &token);
600-
token_client.transfer(&contract_address, &creator, &balance);
685+
686+
if take_fee {
687+
if let Some(ref recipient) = fee_recipient {
688+
let fee_amount = balance * fee_bps / 10000;
689+
let creator_amount = balance - fee_amount;
690+
691+
if fee_amount > 0 {
692+
token_client.transfer(&contract_address, recipient, &fee_amount);
693+
env.events().publish(
694+
(symbol_short!("Goal"), symbol_short!("Fee")),
695+
FeeCollected {
696+
campaign_id,
697+
token: token.clone(),
698+
fee_amount,
699+
fee_recipient: recipient.clone(),
700+
},
701+
);
702+
}
703+
token_client.transfer(&contract_address, &creator, &creator_amount);
704+
} else {
705+
token_client.transfer(&contract_address, &creator, &balance);
706+
}
707+
} else {
708+
token_client.transfer(&contract_address, &creator, &balance);
709+
}
601710

602711
// Clear the balance
603712
env.storage().persistent().set(&balance_key, &0_i128);
@@ -826,3 +935,40 @@ fn read_campaign(env: &Env, campaign_id: u64) -> Campaign {
826935
.unwrap_or_else(|| panic!("campaign not found"))
827936
}
828937

938+
fn refund_contributor(
939+
env: &Env,
940+
campaign: &mut Campaign,
941+
campaign_id: u64,
942+
contributor: &Address,
943+
) -> i128 {
944+
let mut total_refunded = 0_i128;
945+
let contract_address = env.current_contract_address();
946+
for token in campaign.accepted_tokens.iter() {
947+
let contribution_key =
948+
DataKey::Contribution(campaign_id, contributor.clone(), token.clone());
949+
let amount: i128 = env.storage().persistent().get(&contribution_key).unwrap_or(0);
950+
if amount > 0 {
951+
let token_client = TokenClient::new(env, &token);
952+
token_client.transfer(&contract_address, contributor, &amount);
953+
env.storage().persistent().set(&contribution_key, &0_i128);
954+
let balance_key = DataKey::CampaignTokenBalance(campaign_id, token.clone());
955+
let balance: i128 = env.storage().persistent().get(&balance_key).unwrap_or(0);
956+
env.storage()
957+
.persistent()
958+
.set(&balance_key, &(balance - amount));
959+
campaign.pledged_amount -= amount;
960+
total_refunded += amount;
961+
env.events().publish(
962+
(symbol_short!("Goal"), symbol_short!("Refund")),
963+
CampaignRefunded {
964+
campaign_id,
965+
contributor: contributor.clone(),
966+
token: token.clone(),
967+
amount,
968+
},
969+
);
970+
}
971+
}
972+
total_refunded
973+
}
974+

0 commit comments

Comments
 (0)