Skip to content

Commit ce9a736

Browse files
authored
Merge pull request #163 from vicajohn/feature/asset-based-discount
2 parents 60cd98b + 9814077 commit ce9a736

File tree

9 files changed

+746
-10
lines changed

9 files changed

+746
-10
lines changed

contract/contract/src/base/errors.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ pub enum CrowdfundingError {
5050
RefundGracePeriodNotPassed = 44,
5151
PoolAlreadyClosed = 45,
5252
PoolNotDisbursedOrRefunded = 46,
53-
InvalidGoalUpdate = 47,
54-
InsufficientFees = 48,
55-
UserBlacklisted = 49,
53+
InsufficientFees = 47,
54+
UserBlacklisted = 48,
55+
InvalidGoalUpdate = 49,
5656
CampaignCancelled = 50,
5757
}
5858

contract/contract/src/base/events.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ pub fn platform_fees_withdrawn(env: &Env, admin: Address, amount: i128) {
144144
env.events().publish(topics, amount);
145145
}
146146

147+
feature/asset-based-discount
148+
pub fn asset_discount_set(env: &Env, admin: Address, asset: Address, discount_bps: u32) {
149+
let topics = (Symbol::new(env, "asset_discount_set"), admin, asset);
150+
env.events().publish(topics, discount_bps);
151+
}
152+
153+
pub fn platform_fee_percentage_set(env: &Env, admin: Address, fee_bps: u32) {
154+
let topics = (Symbol::new(env, "platform_fee_percentage_set"), admin);
155+
env.events().publish(topics, fee_bps);
156+
}
157+
158+
159+
main
147160
pub fn address_blacklisted(env: &Env, admin: Address, address: Address) {
148161
let topics = (Symbol::new(env, "address_blacklisted"), admin);
149162
env.events().publish(topics, address);
@@ -153,8 +166,11 @@ pub fn address_unblacklisted(env: &Env, admin: Address, address: Address) {
153166
let topics = (Symbol::new(env, "address_unblacklisted"), admin);
154167
env.events().publish(topics, address);
155168
}
169+
feature/asset-based-discount
170+
156171

157172
pub fn pool_metadata_updated(env: &Env, pool_id: u64, updater: Address, new_metadata_hash: String) {
158173
let topics = (Symbol::new(env, "pool_metadata_updated"), pool_id, updater);
159174
env.events().publish(topics, new_metadata_hash);
160175
}
176+
main

contract/contract/src/base/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,19 @@ pub enum StorageKey {
242242
GlobalTotalRaised,
243243
CampaignCancelled(BytesN<32>),
244244
EmergencyContact,
245+
feature/asset-based-discount
246+
AssetDiscount(Address),
247+
PlatformFeePercentage,
248+
CampaignFeeHistory(BytesN<32>),
249+
Blacklist(Address),
250+
245251
CampaignFeeHistory(BytesN<32>),
246252
Blacklist(Address),
247253

248254
ReentrancyLock(u64),
249255
EmergencyWithdrawalLock,
250256
PoolCreator(u64),
257+
main
251258
}
252259

253260
#[cfg(test)]

contract/contract/src/crowdfunding.rs

Lines changed: 149 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,21 @@ impl CrowdfundingTrait for CrowdfundingContract {
10781078
return Err(CrowdfundingError::InvalidPoolState);
10791079
}
10801080

1081+
feature/asset-based-discount
1082+
// Calculate platform fee with discount
1083+
let base_fee_bps = Self::get_platform_fee_percentage(env.clone());
1084+
let discount_bps = Self::get_asset_discount(env.clone(), asset.clone());
1085+
1086+
// Apply discount: effective_fee = base_fee * (1 - discount/10000)
1087+
let effective_fee_bps = if discount_bps > 0 {
1088+
base_fee_bps.saturating_sub((base_fee_bps * discount_bps) / 10000)
1089+
} else {
1090+
base_fee_bps
1091+
};
1092+
1093+
// Calculate fee amount: fee = amount * effective_fee_bps / 10000
1094+
let platform_fee = (amount * effective_fee_bps as i128) / 10000;
1095+
let net_contribution = amount - platform_fee;
10811096
// Load pool configuration to enforce minimum contribution
10821097
let pool_key = StorageKey::Pool(pool_id);
10831098
let pool: PoolConfig = env
@@ -1089,15 +1104,27 @@ impl CrowdfundingTrait for CrowdfundingContract {
10891104
if amount < pool.min_contribution {
10901105
return Err(CrowdfundingError::InvalidAmount);
10911106
}
1107+
main
10921108

10931109
// Transfer tokens
1094-
// Note: In a real implementation we would use the token client.
1095-
// For this task we assume the token interface is available via soroban_sdk::token
10961110
use soroban_sdk::token;
10971111
let token_client = token::Client::new(&env, &asset);
10981112
token_client.transfer(&contributor, env.current_contract_address(), &amount);
10991113

1100-
// Update metrics
1114+
// Track platform fees if any
1115+
if platform_fee > 0 {
1116+
let platform_fees_key = StorageKey::PlatformFees;
1117+
let current_fees: i128 = env
1118+
.storage()
1119+
.instance()
1120+
.get(&platform_fees_key)
1121+
.unwrap_or(0);
1122+
env.storage()
1123+
.instance()
1124+
.set(&platform_fees_key, &(current_fees + platform_fee));
1125+
}
1126+
1127+
// Update metrics with net contribution
11011128
let metrics_key = StorageKey::PoolMetrics(pool_id);
11021129
let mut metrics: PoolMetrics = env
11031130
.storage()
@@ -1123,16 +1150,16 @@ impl CrowdfundingTrait for CrowdfundingContract {
11231150
metrics.contributor_count += 1;
11241151
}
11251152

1126-
metrics.total_raised += amount;
1153+
metrics.total_raised += net_contribution;
11271154
metrics.last_donation_at = env.ledger().timestamp();
11281155

11291156
env.storage().instance().set(&metrics_key, &metrics);
11301157

1131-
// Update per-user contribution tracking
1158+
// Update per-user contribution tracking with net contribution
11321159
let updated_contribution = PoolContribution {
11331160
pool_id,
11341161
contributor: contributor.clone(),
1135-
amount: existing_contribution.amount + amount,
1162+
amount: existing_contribution.amount + net_contribution,
11361163
asset: asset.clone(),
11371164
};
11381165
env.storage()
@@ -1159,7 +1186,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
11591186
pool_id,
11601187
contributor,
11611188
asset,
1162-
amount,
1189+
net_contribution,
11631190
env.ledger().timestamp(),
11641191
is_private,
11651192
);
@@ -1580,10 +1607,124 @@ impl CrowdfundingTrait for CrowdfundingContract {
15801607
.ok_or(CrowdfundingError::NotInitialized)
15811608
}
15821609

1610+
feature/asset-based-discount
1611+
fn set_asset_discount(
1612+
env: Env,
1613+
asset: Address,
1614+
discount_bps: u32,
1615+
) -> Result<(), CrowdfundingError> {
1616+
let admin: Address = env
1617+
.storage()
1618+
.instance()
1619+
.get(&StorageKey::Admin)
1620+
.ok_or(CrowdfundingError::NotInitialized)?;
1621+
admin.require_auth();
1622+
1623+
// Validate discount is not more than 100% (10000 basis points)
1624+
if discount_bps > 10000 {
1625+
return Err(CrowdfundingError::InvalidFee);
1626+
}
1627+
1628+
let key = StorageKey::AssetDiscount(asset.clone());
1629+
env.storage().instance().set(&key, &discount_bps);
1630+
1631+
events::asset_discount_set(&env, admin, asset, discount_bps);
1632+
1633+
Ok(())
1634+
}
1635+
1636+
fn get_asset_discount(env: Env, asset: Address) -> u32 {
1637+
let key = StorageKey::AssetDiscount(asset);
1638+
env.storage().instance().get(&key).unwrap_or(0)
1639+
}
1640+
1641+
fn set_platform_fee_percentage(env: Env, fee_bps: u32) -> Result<(), CrowdfundingError> {
1642+
let admin: Address = env
1643+
.storage()
1644+
.instance()
1645+
.get(&StorageKey::Admin)
1646+
.ok_or(CrowdfundingError::NotInitialized)?;
1647+
admin.require_auth();
1648+
1649+
// Validate fee is not more than 100% (10000 basis points)
1650+
if fee_bps > 10000 {
1651+
return Err(CrowdfundingError::InvalidFee);
1652+
}
1653+
1654+
let key = StorageKey::PlatformFeePercentage;
1655+
env.storage().instance().set(&key, &fee_bps);
1656+
1657+
events::platform_fee_percentage_set(&env, admin, fee_bps);
1658+
1659+
Ok(())
1660+
}
1661+
1662+
fn get_platform_fee_percentage(env: Env) -> u32 {
1663+
let key = StorageKey::PlatformFeePercentage;
1664+
env.storage().instance().get(&key).unwrap_or(0)
1665+
}
1666+
1667+
main
15831668
fn get_contract_version(env: Env) -> String {
15841669
String::from_str(&env, "1.2.0")
15851670
}
15861671

1672+
feature/asset-based-discount
1673+
fn blacklist_address(env: Env, address: Address) -> Result<(), CrowdfundingError> {
1674+
let admin: Address = env
1675+
.storage()
1676+
.instance()
1677+
.get(&StorageKey::Admin)
1678+
.ok_or(CrowdfundingError::NotInitialized)?;
1679+
admin.require_auth();
1680+
1681+
let blacklist_key = StorageKey::Blacklist(address.clone());
1682+
env.storage().persistent().set(&blacklist_key, &true);
1683+
1684+
events::address_blacklisted(&env, admin, address);
1685+
1686+
Ok(())
1687+
}
1688+
1689+
fn unblacklist_address(env: Env, address: Address) -> Result<(), CrowdfundingError> {
1690+
let admin: Address = env
1691+
.storage()
1692+
.instance()
1693+
.get(&StorageKey::Admin)
1694+
.ok_or(CrowdfundingError::NotInitialized)?;
1695+
admin.require_auth();
1696+
1697+
let blacklist_key = StorageKey::Blacklist(address.clone());
1698+
env.storage().persistent().remove(&blacklist_key);
1699+
1700+
events::address_unblacklisted(&env, admin, address);
1701+
1702+
Ok(())
1703+
}
1704+
1705+
fn is_blacklisted(env: Env, address: Address) -> bool {
1706+
let blacklist_key = StorageKey::Blacklist(address);
1707+
env.storage()
1708+
.persistent()
1709+
.get(&blacklist_key)
1710+
.unwrap_or(false)
1711+
}
1712+
1713+
fn get_campaign_fee_history(
1714+
env: Env,
1715+
campaign_id: BytesN<32>,
1716+
) -> Result<i128, CrowdfundingError> {
1717+
// Validate campaign exists
1718+
Self::get_campaign(env.clone(), campaign_id.clone())?;
1719+
1720+
let fee_history_key = StorageKey::CampaignFeeHistory(campaign_id);
1721+
let current_fees: i128 = env
1722+
.storage()
1723+
.persistent()
1724+
.get(&fee_history_key)
1725+
.unwrap_or(0);
1726+
Ok(current_fees)
1727+
15871728
fn get_pool_contributions_paginated(
15881729
env: Env,
15891730
pool_id: u64,
@@ -1632,6 +1773,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
16321773
}
16331774

16341775
Ok(result)
1776+
main
16351777
}
16361778
}
16371779

contract/contract/src/interfaces/crowdfunding.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,47 @@ pub trait CrowdfundingTrait {
174174
fn set_emergency_contact(env: Env, contact: Address) -> Result<(), CrowdfundingError>;
175175

176176
fn get_emergency_contact(env: Env) -> Result<Address, CrowdfundingError>;
177+
feature/asset-based-discount
178+
fn set_asset_discount(
179+
env: Env,
180+
asset: Address,
181+
discount_bps: u32,
182+
) -> Result<(), CrowdfundingError>;
183+
184+
fn get_asset_discount(env: Env, asset: Address) -> u32;
185+
186+
fn set_platform_fee_percentage(env: Env, fee_bps: u32) -> Result<(), CrowdfundingError>;
187+
188+
fn get_platform_fee_percentage(env: Env) -> u32;
189+
190+
fn get_contract_version(env: Env) -> String;
191+
192+
fn blacklist_address(env: Env, address: Address) -> Result<(), CrowdfundingError>;
193+
194+
fn unblacklist_address(env: Env, address: Address) -> Result<(), CrowdfundingError>;
195+
196+
fn is_blacklisted(env: Env, address: Address) -> bool;
197+
198+
fn get_campaign_fee_history(
199+
env: Env,
200+
campaign_id: BytesN<32>,
201+
) -> Result<i128, CrowdfundingError>;
202+
203+
fn update_campaign_goal(
204+
env: Env,
205+
campaign_id: BytesN<32>,
206+
new_goal: i128,
207+
) -> Result<(), CrowdfundingError>;
208+
209+
fn extend_campaign_deadline(
210+
env: Env,
211+
campaign_id: BytesN<32>,
212+
new_deadline: u64,
213+
) -> Result<(), CrowdfundingError>;
214+
215+
fn get_campaigns(env: Env, ids: Vec<BytesN<32>>) -> Vec<CampaignDetails>;
216+
217+
fn renounce_admin(env: Env) -> Result<(), CrowdfundingError>;
177218

178219
fn get_contract_version(env: Env) -> String;
179220

@@ -185,4 +226,5 @@ pub trait CrowdfundingTrait {
185226
) -> Result<Vec<PoolContribution>, CrowdfundingError>;
186227

187228
fn get_pool_remaining_time(env: Env, pool_id: u64) -> Result<u64, CrowdfundingError>;
229+
main
188230
}

0 commit comments

Comments
 (0)