@@ -20,6 +20,10 @@ const MIN_CONTRIBUTION: i128 = 100;
2020/// multi-token donation campaigns while keeping storage costs predictable.
2121const 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 ) ]
2529pub 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]
152174pub 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