diff --git a/src/base/errors.cairo b/src/base/errors.cairo index b8f1cd7..33c3a99 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -133,5 +133,8 @@ pub mod Errors { // Throw Error when Insufficient balance pub const INSUFFICIENT_BALANCE: felt252 = 'Error: Insufficient balance'; + + // Throw Error when Fee percent exceeds 100% + pub const PROTOCOL_FEE_PERCENTAGE_EXCEED: felt252 = 'Error: Fee percent exceeds 100%'; } diff --git a/src/campaign_donation.cairo b/src/campaign_donation.cairo index 67c0074..c516e55 100644 --- a/src/campaign_donation.cairo +++ b/src/campaign_donation.cairo @@ -22,8 +22,9 @@ pub mod CampaignDonation { CAMPAIGN_NOT_CANCELLED, CAMPAIGN_NOT_CLOSED, CAMPAIGN_NOT_FOUND, CAMPAIGN_REF_EMPTY, CAMPAIGN_REF_EXISTS, CAMPAIGN_WITHDRAWN, CANNOT_DENOTE_ZERO_AMOUNT, DONATION_NOT_FOUND, DOUBLE_WITHDRAWAL, INSUFFICIENT_ALLOWANCE, INSUFFICIENT_BALANCE, MORE_THAN_TARGET, - NFT_NOT_CONFIGURED, OPERATION_OVERFLOW, REFUND_ALREADY_CLAIMED, TARGET_NOT_REACHED, - TARGET_REACHED, WITHDRAWAL_FAILED, ZERO_ALLOWANCE, ZERO_AMOUNT, + NFT_NOT_CONFIGURED, OPERATION_OVERFLOW, PROTOCOL_FEE_ADDRESS_NOT_SET, + PROTOCOL_FEE_PERCENTAGE_EXCEED, REFUND_ALREADY_CLAIMED, TARGET_NOT_REACHED, TARGET_REACHED, + WITHDRAWAL_FAILED, ZERO_ALLOWANCE, ZERO_AMOUNT, }; use crate::base::types::{Campaigns, DonationMetadata, Donations}; @@ -58,7 +59,10 @@ pub mod CampaignDonation { refunds_claimed: Map<(u256, ContractAddress), bool>, // Map((campaign_id, donor), claimed) donations_by_donor: Map< (u256, ContractAddress), u256, - > // Map((campaign_id, donor), total_donation) + >, // Map((campaign_id, donor), total_donation) + /// Protocol fee percentage using 10000 basis points (e.g. 250 = 2.5%). Default is 0. + protocol_fee_percent: u256, + protocol_fee_address: ContractAddress, } @@ -146,9 +150,15 @@ pub mod CampaignDonation { } #[constructor] - fn constructor(ref self: ContractState, owner: ContractAddress, token: ContractAddress) { + fn constructor( + ref self: ContractState, + owner: ContractAddress, + token: ContractAddress, + protocol_fee_address: ContractAddress, + ) { self.ownable.initializer(owner); self.donation_token.write(token); + self.protocol_fee_address.write(protocol_fee_address); } #[abi(embed_v0)] @@ -429,6 +439,25 @@ pub mod CampaignDonation { ), ); } + + fn get_protocol_fee_percent(self: @ContractState) -> u256 { + self.protocol_fee_percent.read() + } + + fn set_protocol_fee_percent(ref self: ContractState, new_fee_percent: u256) { + self.ownable.assert_only_owner(); + assert(new_fee_percent <= 10000, PROTOCOL_FEE_PERCENTAGE_EXCEED); + self.protocol_fee_percent.write(new_fee_percent); + } + + fn get_protocol_fee_address(self: @ContractState) -> ContractAddress { + self.protocol_fee_address.read() + } + + fn set_protocol_fee_address(ref self: ContractState, new_fee_address: ContractAddress) { + self.ownable.assert_only_owner(); + self.protocol_fee_address.write(new_fee_address); + } } #[generate_trait] @@ -533,10 +562,21 @@ pub mod CampaignDonation { let donation_token = self.donation_token.read(); - let token = IERC20Dispatcher { contract_address: donation_token }; + let token_dispatcher = IERC20Dispatcher { contract_address: donation_token }; let withdrawn_amount = campaign.current_balance; - let transfer_from = token.transfer(campaign_owner, withdrawn_amount); + let protocol_fee = self.calculate_protocol_fee(@withdrawn_amount); + + // transfer protocol fee + if protocol_fee > 0 { + let protocol_address = self.protocol_fee_address.read(); + assert(!protocol_address.is_zero(), PROTOCOL_FEE_ADDRESS_NOT_SET); + let protocol_transfer = token_dispatcher.transfer(protocol_address, protocol_fee); + assert(protocol_transfer, WITHDRAWAL_FAILED); + } + + let amount_after_fee = withdrawn_amount - protocol_fee; + let transfer_from = token_dispatcher.transfer(campaign_owner, amount_after_fee); assert(transfer_from, WITHDRAWAL_FAILED); campaign.withdrawn_amount = campaign.withdrawn_amount + withdrawn_amount; @@ -547,6 +587,13 @@ pub mod CampaignDonation { withdrawn_amount } + fn calculate_protocol_fee(self: @ContractState, total_amount: @u256) -> u256 { + let fee_percent = self.protocol_fee_percent.read(); + let protocol_fee = (*total_amount * fee_percent) / 10000; + protocol_fee + } + + fn get_asset_address(self: @ContractState, token_name: felt252) -> ContractAddress { let mut token_address: ContractAddress = contract_address_const::<0>(); if token_name == 'USDC' || token_name == 'usdc' { diff --git a/src/interfaces/ICampaignDonation.cairo b/src/interfaces/ICampaignDonation.cairo index 468a12b..4d2c607 100644 --- a/src/interfaces/ICampaignDonation.cairo +++ b/src/interfaces/ICampaignDonation.cairo @@ -218,6 +218,26 @@ pub trait ICampaignDonation { // / * Caller must have donated to the campaign // / * Refund must not have been claimed already fn claim_refund(ref self: TContractState, campaign_id: u256); + + // ************************************************************************* + // PROTOCOL FEES + // ************************************************************************* + + /// @notice Gets the current protocol fee percentage + /// @return The protocol fee percentage (100 = 1%) + fn get_protocol_fee_percent(self: @TContractState) -> u256; + + /// @notice Sets a new protocol fee percentage + /// @param new_fee_percent The new fee percentage to set (100 = 1%) + fn set_protocol_fee_percent(ref self: TContractState, new_fee_percent: u256); + + /// @notice Gets the current protocol fee collection address + /// @return The address where protocol fees are sent + fn get_protocol_fee_address(self: @TContractState) -> ContractAddress; + + /// @notice Sets a new protocol fee collection address + /// @param new_fee_address The new address to collect protocol fees + fn set_protocol_fee_address(ref self: TContractState, new_fee_address: ContractAddress); // ************************************************************************* // ANALYTICS & INSIGHTS // ************************************************************************* diff --git a/tests/test_campaign_donation.cairo b/tests/test_campaign_donation.cairo index 7a9c8c3..e4ef5a9 100644 --- a/tests/test_campaign_donation.cairo +++ b/tests/test_campaign_donation.cairo @@ -20,7 +20,14 @@ use snforge_std::{ }; use starknet::{ContractAddress, contract_address_const}; -fn setup() -> (ContractAddress, ContractAddress, ICampaignDonationDispatcher, IERC721Dispatcher) { +fn setup() -> ( + ContractAddress, + ContractAddress, + ICampaignDonationDispatcher, + IERC721Dispatcher, + ContractAddress, + ContractAddress, +) { let sender: ContractAddress = contract_address_const::<'sender'>(); // Deploy mock ERC20 let erc20_class = declare("MockUsdc").unwrap().contract_class(); @@ -30,7 +37,8 @@ fn setup() -> (ContractAddress, ContractAddress, ICampaignDonationDispatcher, IE // Deploy Campaign Donation contract let protocol_owner: ContractAddress = contract_address_const::<'protocol_owner'>(); let campaign_donation_class = declare("CampaignDonation").unwrap().contract_class(); - let mut calldata = array![protocol_owner.into(), erc20_address.into()]; + let protocol_address = contract_address_const::<'protocol_address'>(); + let mut calldata = array![protocol_owner.into(), erc20_address.into(), protocol_address.into()]; let (campaign_donation_address, _) = campaign_donation_class.deploy(@calldata).unwrap(); ( @@ -38,6 +46,8 @@ fn setup() -> (ContractAddress, ContractAddress, ICampaignDonationDispatcher, IE sender, ICampaignDonationDispatcher { contract_address: campaign_donation_address }, IERC721Dispatcher { contract_address: campaign_donation_address }, + protocol_owner, + protocol_address, ) } @@ -58,7 +68,7 @@ fn deploy_donation_nft( #[test] fn test_successful_create_campaign() { - let (_token_address, _sender, campaign_donation, _erc721) = setup(); + let (_token_address, _sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; let campaign_ref = 'Test'; let owner = contract_address_const::<'owner'>(); @@ -83,7 +93,7 @@ fn test_successful_create_campaign() { #[test] #[should_panic(expected: 'Error: Amount must be > 0.')] fn test_create_campaign_invalid_zero_amount() { - let (_token_address, _sender, campaign_donation, _erc721) = setup(); + let (_token_address, _sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 0_u256; let campaign_ref = 'Test'; let owner = contract_address_const::<'owner'>(); @@ -92,37 +102,37 @@ fn test_create_campaign_invalid_zero_amount() { stop_cheat_caller_address(campaign_donation.contract_address); } -// #[test] -// #[should_panic(expected: 'Error: Campaign Ref Exists')] -// fn test_create_campaign_duplicate_campaign_refs() { -// let (_token_address, _sender, campaign_donation, _erc721) = setup(); -// let target_amount = 50_u256; -// let asset = 'Test'; -// let campaign_ref = 'Test'; -// let owner = contract_address_const::<'owner'>(); -// start_cheat_caller_address(campaign_donation.contract_address, owner); -// campaign_donation.create_campaign(campaign_ref, target_amount); -// campaign_donation.create_campaign(campaign_ref, target_amount); -// stop_cheat_caller_address(campaign_donation.contract_address); -// } - -// #[test] -// #[should_panic(expected: 'Error: Campaign Ref Is Required')] -// fn test_create_campaign_empty_campaign_refs() { -// let (_token_address, _sender, campaign_donation, _erc721) = setup(); -// let target_amount = 100_u256; -// let asset = 'Test'; -// let campaign_ref = ''; -// let owner = contract_address_const::<'owner'>(); -// start_cheat_caller_address(campaign_donation.contract_address, owner); -// campaign_donation.create_campaign(campaign_ref, target_amount); -// stop_cheat_caller_address(campaign_donation.contract_address); -// } - -// DONE +// // #[test] +// // #[should_panic(expected: 'Error: Campaign Ref Exists')] +// // fn test_create_campaign_duplicate_campaign_refs() { +// // let (_token_address, _sender, campaign_donation, _erc721) = setup(); +// // let target_amount = 50_u256; +// // let asset = 'Test'; +// // let campaign_ref = 'Test'; +// // let owner = contract_address_const::<'owner'>(); +// // start_cheat_caller_address(campaign_donation.contract_address, owner); +// // campaign_donation.create_campaign(campaign_ref, target_amount); +// // campaign_donation.create_campaign(campaign_ref, target_amount); +// // stop_cheat_caller_address(campaign_donation.contract_address); +// // } + +// // #[test] +// // #[should_panic(expected: 'Error: Campaign Ref Is Required')] +// // fn test_create_campaign_empty_campaign_refs() { +// // let (_token_address, _sender, campaign_donation, _erc721) = setup(); +// // let target_amount = 100_u256; +// // let asset = 'Test'; +// // let campaign_ref = ''; +// // let owner = contract_address_const::<'owner'>(); +// // start_cheat_caller_address(campaign_donation.contract_address, owner); +// // campaign_donation.create_campaign(campaign_ref, target_amount); +// // stop_cheat_caller_address(campaign_donation.contract_address); +// // } + +// // DONE #[test] fn test_successful_campaign_donation() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 5000_u256; let campaign_ref = 'Test'; @@ -173,10 +183,10 @@ fn test_successful_campaign_donation() { assert(user_balance_after == user_balance_before - 500, ' USR transfer failed'); } -// DONE +// // DONE #[test] fn test_successful_campaign_donation_twice() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; let campaign_ref = 'Test'; @@ -219,7 +229,7 @@ fn test_successful_campaign_donation_twice() { #[test] fn test_successful_multiple_users_donating_to_a_campaign() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 10000_u256; let campaign_ref = 'Test'; let another_user: ContractAddress = contract_address_const::<'another_user'>(); @@ -271,7 +281,7 @@ fn test_successful_multiple_users_donating_to_a_campaign() { #[test] fn test_target_met_successful() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; let campaign_ref = 'Test'; @@ -304,7 +314,7 @@ fn test_target_met_successful() { #[test] fn test_get_campaigns() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount_1 = 1000_u256; let target_amount_2 = 2000_u256; let target_amount_3 = 3000_u256; @@ -346,7 +356,7 @@ fn test_get_campaigns() { #[test] fn test_get_campaign_donations() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let another_user: ContractAddress = contract_address_const::<'another_user'>(); let target_amount = 5000_u256; @@ -410,7 +420,7 @@ fn test_get_campaign_donations() { #[test] fn test_get_campaign_donations_empty() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create a campaign but don't make any donations @@ -427,7 +437,7 @@ fn test_get_campaign_donations_empty() { #[test] fn test_multiple_campaigns_with_donations() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create multiple campaigns @@ -469,7 +479,7 @@ fn test_multiple_campaigns_with_donations() { #[test] #[fork(url: "https://starknet-sepolia.public.blastapi.io/rpc/v0_8", block_tag: latest)] fn test_withdraw_funds_from_campaign_successful() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 800_u256; let campaign_ref = 'Test'; let owner = contract_address_const::<'owner'>(); @@ -527,7 +537,7 @@ fn test_withdraw_funds_from_campaign_successful() { #[test] fn test_update_campaign_target_successful() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; let new_target = 2000_u256; @@ -547,7 +557,7 @@ fn test_update_campaign_target_successful() { #[test] #[should_panic(expected: 'Error: Campaign Not Found')] fn test_update_campaign_target_nonexistent() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); start_cheat_caller_address(campaign_donation.contract_address, sender); campaign_donation.update_campaign_target(999, 2000); stop_cheat_caller_address(campaign_donation.contract_address); @@ -556,7 +566,7 @@ fn test_update_campaign_target_nonexistent() { #[test] #[should_panic(expected: 'Caller is Not Campaign Owner')] fn test_update_campaign_target_not_owner() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); let other_user: ContractAddress = contract_address_const::<'other_user'>(); // Create campaign as sender @@ -573,7 +583,7 @@ fn test_update_campaign_target_not_owner() { #[test] #[should_panic(expected: 'Error: Campaign has donations')] fn test_update_campaign_target_with_donations() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create campaign and make donation @@ -600,7 +610,7 @@ fn test_update_campaign_target_with_donations() { #[test] fn test_cancel_campaign_successful() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create campaign start_cheat_caller_address(campaign_donation.contract_address, sender); @@ -619,7 +629,7 @@ fn test_cancel_campaign_successful() { #[test] #[should_panic(expected: 'Error: Campaign closed')] fn test_cancel_campaign_already_closed() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create and cancel campaign start_cheat_caller_address(campaign_donation.contract_address, sender); @@ -633,7 +643,7 @@ fn test_cancel_campaign_already_closed() { #[test] fn test_claim_refund_successful() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create campaign @@ -669,7 +679,7 @@ fn test_claim_refund_successful() { #[test] #[should_panic(expected: 'Error: Refund already claimed')] fn test_claim_refund_twice() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create campaign and make donation start_cheat_caller_address(campaign_donation.contract_address, sender); @@ -697,7 +707,7 @@ fn test_claim_refund_twice() { #[test] #[should_panic(expected: 'Error: Donation not found')] fn test_claim_refund_no_donation() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); let other_user: ContractAddress = contract_address_const::<'other_user'>(); // Create campaign @@ -716,7 +726,7 @@ fn test_claim_refund_no_donation() { #[test] fn test_mint_donation_receipt_successful() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Deploy the Donation NFT contract let (erc721, donation_nft_dispatcher) = deploy_donation_nft(campaign_donation.contract_address); // Use the protocol owner to set the donation NFT address @@ -786,7 +796,7 @@ fn test_mint_donation_receipt_successful() { #[test] #[should_panic(expected: 'Error: Caller is not the donor')] fn test_mint_donation_receipt_fail_if_not_donor() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Deploy the Donation NFT contract let (_erc721, donation_nft_dispatcher) = deploy_donation_nft( campaign_donation.contract_address, @@ -822,7 +832,7 @@ fn test_mint_donation_receipt_fail_if_not_donor() { #[test] #[should_panic(expected: 'NFT already minted')] fn test_mint_donation_receipt_fail_if_already_minted() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Deploy the Donation NFT contract let (_erc721, donation_nft_dispatcher) = deploy_donation_nft( campaign_donation.contract_address, @@ -865,7 +875,7 @@ fn test_mint_donation_receipt_fail_if_already_minted() { #[test] #[should_panic(expected: 'Donation data not found')] fn test_get_donation_data_fail_if_not_found() { - let (_token_address, _sender, campaign_donation, _erc721) = setup(); + let (_token_address, _sender, campaign_donation, _erc721, _, _) = setup(); // Deploy the Donation NFT contract let (_erc721, donation_nft_dispatcher) = deploy_donation_nft( campaign_donation.contract_address, @@ -875,7 +885,7 @@ fn test_get_donation_data_fail_if_not_found() { donation_nft_dispatcher.get_donation_data(999_u256); } fn test_get_donations_by_donor_no_donations() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Fetch donations for sender let donations = campaign_donation.get_donations_by_donor(sender); @@ -884,7 +894,7 @@ fn test_get_donations_by_donor_no_donations() { #[test] fn test_get_donations_by_donor_single_donation() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create new campaign @@ -917,7 +927,7 @@ fn test_get_donations_by_donor_single_donation() { #[test] fn test_get_donations_by_donor_across_multiple_campaigns() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create two campaigns @@ -963,7 +973,7 @@ fn test_get_donations_by_donor_across_multiple_campaigns() { #[test] fn test_get_donations_by_donor_multiple_donations() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); let target_amount = 1000_u256; // Create new campaign @@ -1004,7 +1014,7 @@ fn test_get_donations_by_donor_multiple_donations() { #[test] fn test_get_total_donated_by_donor_no_donations() { - let (_token_address, sender, campaign_donation, _erc721) = setup(); + let (_token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Fetch total donated for sender let total_donated = campaign_donation.get_total_donated_by_donor(sender); @@ -1015,7 +1025,7 @@ fn test_get_total_donated_by_donor_no_donations() { #[test] fn test_get_total_donated_by_donor_single_donation() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create new campaign let target_amount = 1000_u256; @@ -1044,7 +1054,7 @@ fn test_get_total_donated_by_donor_single_donation() { #[test] fn test_get_total_donated_by_donor_multiple_donations_same_campaign() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create new campaign let target_amount = 1000_u256; @@ -1075,7 +1085,7 @@ fn test_get_total_donated_by_donor_multiple_donations_same_campaign() { #[test] fn test_get_total_donated_by_donor_across_multiple_campaigns() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Create two campaigns let target_amount = 1000_u256; @@ -1107,9 +1117,61 @@ fn test_get_total_donated_by_donor_across_multiple_campaigns() { assert(total_donated == amount_1 + amount_2 + amount_3, 'Total donated doesnt match'); } +#[test] +fn test_protocol_donation_fee_calculation() { + let (token_address, sender, campaign_donation, _erc721, protocol_owner, protocol_address) = + setup(); + let token = _erc721; + // Set protocol fee to 250 basis points (2.5%) + start_cheat_caller_address(campaign_donation.contract_address, protocol_owner); + campaign_donation.set_protocol_fee_address(protocol_address); + campaign_donation.set_protocol_fee_percent(250); + stop_cheat_caller_address(campaign_donation.contract_address); + + // Create a campaign and make a donation + let target_amount = 1000_u256; + start_cheat_caller_address(campaign_donation.contract_address, sender); + let campaign_id = campaign_donation.create_campaign('Test', target_amount); + stop_cheat_caller_address(campaign_donation.contract_address); + + // Setup token approval and make donation + let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + start_cheat_caller_address(token_address, sender); + token_dispatcher.approve(campaign_donation.contract_address, 1000); + stop_cheat_caller_address(token_address); + + start_cheat_caller_address(campaign_donation.contract_address, sender); + campaign_donation.donate_to_campaign(campaign_id, 1000); + stop_cheat_caller_address(campaign_donation.contract_address); + + // Test withdrawal with protocol fee calculation + let protocol_balance_before = token_dispatcher.balance_of(protocol_address); + let owner_balance_before = token_dispatcher.balance_of(sender); + + start_cheat_caller_address(campaign_donation.contract_address, sender); + campaign_donation.withdraw_from_campaign(campaign_id); + stop_cheat_caller_address(campaign_donation.contract_address); + + let protocol_balance_after = token_dispatcher.balance_of(protocol_address); + let owner_balance_after = token_dispatcher.balance_of(sender); + + // Verify protocol fee (2.5% of 1000 = 25) + let expected_protocol_fee = 25; + let expected_owner_amount = 975; + + assert( + protocol_balance_after - protocol_balance_before == expected_protocol_fee, + 'Protocol fee calculation error', + ); + assert( + owner_balance_after - owner_balance_before == expected_owner_amount, + 'Owner amount calculation error', + ); +} + #[test] fn test_has_donated_to_campaign_single_donation() { - let (token_address, sender, campaign_donation, _erc721) = setup(); + let (token_address, sender, campaign_donation, _erc721, _, _) = setup(); // Check that sender has not donated to nonexistent campaign let nonexistent_campaign_id = 42; @@ -1142,3 +1204,4 @@ fn test_has_donated_to_campaign_single_donation() { let has_donated = campaign_donation.has_donated_to_campaign(campaign_id, sender); assert(has_donated, 'Donation exists'); } +