From 30c703400254841017b822929a804d25d3be5d91 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 09:34:38 +0200 Subject: [PATCH] add start_time --- .../advanced_factory/src/contract.cairo | 14 +- .../advanced_factory/src/tests.cairo | 24 ++- .../crowdfunding/src/campaign.cairo | 151 +++++++------ .../crowdfunding/src/mock_upgrade.cairo | 199 ++++++++---------- .../applications/crowdfunding/src/tests.cairo | 22 +- 5 files changed, 218 insertions(+), 192 deletions(-) diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo index 7d777bae..b72913f3 100644 --- a/listings/applications/advanced_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -8,13 +8,14 @@ pub trait ICampaignFactory { title: ByteArray, description: ByteArray, goal: u256, - duration: u64, + start_time: u64, + end_time: u64, token_address: ContractAddress ) -> ContractAddress; fn get_campaign_class_hash(self: @TContractState) -> ClassHash; fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash); fn upgrade_campaign_implementation( - ref self: TContractState, campaign_address: ContractAddress, new_duration: Option + ref self: TContractState, campaign_address: ContractAddress, new_end_time: Option ); } @@ -94,14 +95,15 @@ pub mod CampaignFactory { title: ByteArray, description: ByteArray, goal: u256, - duration: u64, + start_time: u64, + end_time: u64, token_address: ContractAddress, ) -> ContractAddress { let creator = get_caller_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((creator, title, description, goal), duration, token_address) + ((creator, title, description, goal), start_time, end_time, token_address) .serialize(ref constructor_calldata); // Contract deployment @@ -134,7 +136,7 @@ pub mod CampaignFactory { } fn upgrade_campaign_implementation( - ref self: ContractState, campaign_address: ContractAddress, new_duration: Option + ref self: ContractState, campaign_address: ContractAddress, new_end_time: Option ) { assert(campaign_address.is_non_zero(), Errors::ZERO_ADDRESS); @@ -144,7 +146,7 @@ pub mod CampaignFactory { assert(old_class_hash != self.campaign_class_hash.read(), Errors::SAME_IMPLEMENTATION); let campaign = ICampaignDispatcher { contract_address: campaign_address }; - campaign.upgrade(self.campaign_class_hash.read(), new_duration); + campaign.upgrade(self.campaign_class_hash.read(), new_end_time); } } } diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 0fd5c37e..6526fdd7 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -64,18 +64,20 @@ fn test_create_campaign() { let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; let goal: u256 = 10000; - let duration: u64 = 60; + let start_time = get_block_timestamp(); + let end_time = start_time + 60; let token = contract_address_const::<'token'>(); let campaign_address = factory - .create_campaign(title.clone(), description.clone(), goal, duration, token); + .create_campaign(title.clone(), description.clone(), goal, start_time, end_time, token); let campaign = ICampaignDispatcher { contract_address: campaign_address }; let details = campaign.get_details(); assert_eq!(details.title, title); assert_eq!(details.description, description); assert_eq!(details.goal, goal); - assert_eq!(details.end_time, get_block_timestamp() + duration); + assert_eq!(details.start_time, start_time); + assert_eq!(details.end_time, end_time); assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, token); assert_eq!(details.total_pledges, 0); @@ -108,16 +110,26 @@ fn test_uprade_campaign_class_hash() { let token = contract_address_const::<'token'>(); // deploy a pending campaign with the old class hash + let start_time_pending = get_block_timestamp() + 20; + let end_time_pending = start_time_pending + 60; let pending_campaign_creator = contract_address_const::<'pending_campaign_creator'>(); start_cheat_caller_address(factory.contract_address, pending_campaign_creator); - let pending_campaign = factory.create_campaign("title 1", "description 1", 10000, 60, token); + let pending_campaign = factory + .create_campaign( + "title 1", "description 1", 10000, start_time_pending, end_time_pending, token + ); assert_eq!(old_class_hash, get_class_hash(pending_campaign)); // deploy an active campaign with the old class hash + let start_time_active = get_block_timestamp(); + let end_time_active = start_time_active + 60; let active_campaign_creator = contract_address_const::<'active_campaign_creator'>(); start_cheat_caller_address(factory.contract_address, active_campaign_creator); - let active_campaign = factory.create_campaign("title 2", "description 2", 20000, 100, token); + let active_campaign = factory + .create_campaign( + "title 2", "description 2", 20000, start_time_active, end_time_active, token + ); assert_eq!(old_class_hash, get_class_hash(active_campaign)); @@ -165,7 +177,7 @@ fn test_uprade_campaign_class_hash() { // upgrade active campaign start_cheat_caller_address(factory.contract_address, active_campaign_creator); - factory.upgrade_campaign_implementation(active_campaign, Option::Some(60)); + factory.upgrade_campaign_implementation(active_campaign, Option::None); assert_eq!(get_class_hash(pending_campaign), new_class_hash); assert_eq!(get_class_hash(active_campaign), new_class_hash); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index ce6bc490..ae4500ad 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -16,6 +16,7 @@ pub struct Details { pub creator: ContractAddress, pub goal: u256, pub title: ByteArray, + pub start_time: u64, pub end_time: u64, pub description: ByteArray, pub status: Status, @@ -32,7 +33,7 @@ pub trait ICampaign { fn get_pledges(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray); - fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); + fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_end_time: Option); fn unpledge(ref self: TContractState, reason: ByteArray); } @@ -64,6 +65,7 @@ pub mod Campaign { ownable: ownable_component::Storage, #[substorage(v0)] pledges: pledgeable_component::Storage, + start_time: u64, end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, @@ -140,25 +142,31 @@ pub mod Campaign { pub mod Errors { pub const NOT_CREATOR: felt252 = 'Not creator'; pub const ENDED: felt252 = 'Campaign already ended'; + pub const NOT_STARTED: felt252 = 'Campaign not started'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const CANCELED: felt252 = 'Campaign canceled'; pub const FAILED: felt252 = 'Campaign failed'; pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; pub const ZERO_TARGET: felt252 = 'Target must be > 0'; - pub const ZERO_DURATION: felt252 = 'Duration must be > 0'; + pub const START_TIME_IN_PAST: felt252 = 'Start time < now'; + pub const END_BEFORE_START: felt252 = 'End time < start time'; + pub const END_BEFORE_NOW: felt252 = 'End time < now'; + pub const END_BIGGER_THAN_MAX: felt252 = 'End time > max duration'; pub const ZERO_FUNDS: felt252 = 'No funds to claim'; - pub const ZERO_ADDRESS_CONTRIBUTOR: felt252 = 'Contributor cannot be zero'; + pub const ZERO_ADDRESS_PLEDGER: felt252 = 'Contributor cannot be zero'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const TITLE_EMPTY: felt252 = 'Title empty'; pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero'; pub const CREATOR_ZERO: felt252 = 'Creator address cannot be zero'; pub const TARGET_NOT_REACHED: felt252 = 'Target not reached'; pub const TARGET_ALREADY_REACHED: felt252 = 'Target already reached'; - pub const NOTHING_TO_WITHDRAW: felt252 = 'Nothing to unpledge'; + pub const NOTHING_TO_UNPLEDGE: felt252 = 'Nothing to unpledge'; pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; } + const NINETY_DAYS: u64 = consteval_int!(90 * 24 * 60 * 60); + #[constructor] fn constructor( ref self: ContractState, @@ -166,15 +174,16 @@ pub mod Campaign { title: ByteArray, description: ByteArray, goal: u256, - duration: u64, + start_time: u64, + end_time: u64, token_address: ContractAddress, ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(goal > 0, Errors::ZERO_TARGET); - assert(duration > 0, Errors::ZERO_DURATION); - - self.token.write(IERC20Dispatcher { contract_address: token_address }); + assert(start_time >= get_block_timestamp(), Errors::START_TIME_IN_PAST); + assert(end_time >= start_time, Errors::END_BEFORE_START); + assert(end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX); self.title.write(title); self.goal.write(goal); @@ -182,16 +191,33 @@ pub mod Campaign { self.creator.write(creator); self.ownable._init(get_caller_address()); self.status.write(Status::ACTIVE); - self.end_time.write(get_block_timestamp() + duration); + self.start_time.write(start_time); + self.end_time.write(end_time); + self.token.write(IERC20Dispatcher { contract_address: token_address }); } #[abi(embed_v0)] impl Campaign of super::ICampaign { + fn cancel(ref self: ContractState, reason: ByteArray) { + self._assert_only_creator(); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + + if !self._is_goal_reached() && self._is_expired() { + self.status.write(Status::FAILED); + } else { + self.status.write(Status::CANCELED); + } + + self._refund_all(reason.clone()); + let status = self.status.read(); + + self.emit(Event::Canceled(Canceled { reason, status })); + } + fn claim(ref self: ContractState) { self._assert_only_creator(); - assert( - self.status.read() == Status::ACTIVE && self._is_expired(), Errors::STILL_ACTIVE - ); + self._assert_active(); + assert(self._is_expired(), Errors::STILL_ACTIVE); assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); @@ -212,25 +238,31 @@ pub mod Campaign { self.emit(Event::Claimed(Claimed { amount })); } - fn cancel(ref self: ContractState, reason: ByteArray) { - self._assert_only_creator(); - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - - if !self._is_goal_reached() && self._is_expired() { - self.status.write(Status::FAILED); - } else { - self.status.write(Status::CANCELED); + fn get_details(self: @ContractState) -> Details { + Details { + creator: self.creator.read(), + title: self.title.read(), + description: self.description.read(), + goal: self.goal.read(), + start_time: self.start_time.read(), + end_time: self.end_time.read(), + status: self.status.read(), + token: self.token.read().contract_address, + total_pledges: self.total_pledges.read(), } + } - self._refund_all(reason.clone()); - let status = self.status.read(); + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) + } - self.emit(Event::Canceled(Canceled { reason, status })); + fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.pledges.get_pledges_as_arr() } fn pledge(ref self: ContractState, amount: u256) { - // start time check - assert(self.status.read() == Status::ACTIVE && !self._is_expired(), Errors::ENDED); + self._assert_active(); + assert(!self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let pledger = get_caller_address(); @@ -244,32 +276,10 @@ pub mod Campaign { self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } - fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { - self.pledges.get(pledger) - } - - fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.pledges.get_pledges_as_arr() - } - - fn get_details(self: @ContractState) -> Details { - Details { - creator: self.creator.read(), - title: self.title.read(), - description: self.description.read(), - goal: self.goal.read(), - end_time: self.end_time.read(), - status: self.status.read(), - token: self.token.read().contract_address, - total_pledges: self.total_pledges.read(), - } - } - fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); - // start time check - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + self._assert_active(); + assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); let amount = self._refund(pledger); @@ -277,17 +287,29 @@ pub mod Campaign { self.emit(Event::Refunded(Refunded { pledger, amount, reason })) } - fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_duration: Option) { + fn unpledge(ref self: ContractState, reason: ByteArray) { + self._assert_active(); + assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); + assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); + + let pledger = get_caller_address(); + let amount = self._refund(pledger); + + self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_end_time: Option) { self.ownable._assert_only_owner(); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // start time check - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); // only active campaigns have funds to refund and duration to update - if self.status.read() == Status::ACTIVE { - if let Option::Some(duration) = new_duration { - assert(duration > 0, Errors::ZERO_DURATION); - self.end_time.write(get_block_timestamp() + duration); + if get_block_timestamp() >= self.start_time.read() { + if let Option::Some(end_time) = new_end_time { + assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW); + assert( + end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX + ); + self.end_time.write(end_time); }; self._refund_all("contract upgraded"); } @@ -296,18 +318,6 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - - fn unpledge(ref self: ContractState, reason: ByteArray) { - // start time check - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); - assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); - - let pledger = get_caller_address(); - let amount = self._refund(pledger); - - self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); - } } #[generate_trait] @@ -318,6 +328,11 @@ pub mod Campaign { assert(caller == self.creator.read(), Errors::NOT_CREATOR); } + fn _assert_active(self: @ContractState) { + assert(get_block_timestamp() >= self.start_time.read(), Errors::NOT_STARTED); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + } + fn _is_expired(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 9a46c450..965c98e0 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -26,6 +26,7 @@ pub mod MockUpgrade { ownable: ownable_component::Storage, #[substorage(v0)] pledges: pledgeable_component::Storage, + start_time: u64, end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, @@ -36,31 +37,26 @@ pub mod MockUpgrade { status: Status } - #[event] #[derive(Drop, starknet::Event)] pub enum Event { #[flat] OwnableEvent: ownable_component::Event, - Launched: Launched, Claimed: Claimed, Canceled: Canceled, + Launched: Launched, PledgeableEvent: pledgeable_component::Event, PledgeMade: PledgeMade, Refunded: Refunded, - Upgraded: Upgraded, - Unpledged: Unpledged, RefundedAll: RefundedAll, + Unpledged: Unpledged, + Upgraded: Upgraded, } #[derive(Drop, starknet::Event)] - pub struct Launched {} - - #[derive(Drop, starknet::Event)] - pub struct PledgeMade { - #[key] - pub pledger: ContractAddress, - pub amount: u256, + pub struct Canceled { + pub reason: ByteArray, + pub status: Status, } #[derive(Drop, starknet::Event)] @@ -69,9 +65,13 @@ pub mod MockUpgrade { } #[derive(Drop, starknet::Event)] - pub struct Canceled { - pub reason: ByteArray, - pub status: Status, + pub struct Launched {} + + #[derive(Drop, starknet::Event)] + pub struct PledgeMade { + #[key] + pub pledger: ContractAddress, + pub amount: u256, } #[derive(Drop, starknet::Event)] @@ -87,11 +87,6 @@ pub mod MockUpgrade { pub reason: ByteArray, } - #[derive(Drop, starknet::Event)] - pub struct Upgraded { - pub implementation: ClassHash - } - #[derive(Drop, starknet::Event)] pub struct Unpledged { #[key] @@ -100,6 +95,13 @@ pub mod MockUpgrade { pub reason: ByteArray, } + #[derive(Drop, starknet::Event)] + pub struct Upgraded { + pub implementation: ClassHash + } + + const NINETY_DAYS: u64 = consteval_int!(90 * 24 * 60 * 60); + #[constructor] fn constructor( ref self: ContractState, @@ -107,30 +109,50 @@ pub mod MockUpgrade { title: ByteArray, description: ByteArray, goal: u256, - duration: u64, + start_time: u64, + end_time: u64, token_address: ContractAddress, ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(goal > 0, Errors::ZERO_TARGET); - assert(duration > 0, Errors::ZERO_DURATION); - - self.token.write(IERC20Dispatcher { contract_address: token_address }); + assert(start_time >= get_block_timestamp(), Errors::START_TIME_IN_PAST); + assert(end_time >= start_time, Errors::END_BEFORE_START); + assert(end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX); self.title.write(title); self.goal.write(goal); self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - self.end_time.write(get_block_timestamp() + duration); self.status.write(Status::ACTIVE); + self.start_time.write(start_time); + self.end_time.write(end_time); + self.token.write(IERC20Dispatcher { contract_address: token_address }); } #[abi(embed_v0)] impl MockUpgrade of ICampaign { + fn cancel(ref self: ContractState, reason: ByteArray) { + self._assert_only_creator(); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + + if !self._is_goal_reached() && self._is_expired() { + self.status.write(Status::FAILED); + } else { + self.status.write(Status::CANCELED); + } + + self._refund_all(reason.clone()); + let status = self.status.read(); + + self.emit(Event::Canceled(Canceled { reason, status })); + } + fn claim(ref self: ContractState) { self._assert_only_creator(); - assert(self._is_active() && self._is_expired(), Errors::STILL_ACTIVE); + self._assert_active(); + assert(self._is_expired(), Errors::STILL_ACTIVE); assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); @@ -151,25 +173,31 @@ pub mod MockUpgrade { self.emit(Event::Claimed(Claimed { amount })); } - fn cancel(ref self: ContractState, reason: ByteArray) { - self._assert_only_creator(); - assert(self._is_active(), Errors::ENDED); - - if !self._is_goal_reached() && self._is_expired() { - self.status.write(Status::FAILED); - } else { - self.status.write(Status::CANCELED); + fn get_details(self: @ContractState) -> Details { + Details { + creator: self.creator.read(), + title: self.title.read(), + description: self.description.read(), + goal: self.goal.read(), + start_time: self.start_time.read(), + end_time: self.end_time.read(), + status: self.status.read(), + token: self.token.read().contract_address, + total_pledges: self.total_pledges.read(), } + } - self._refund_all(reason.clone()); - let status = self.status.read(); + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) + } - self.emit(Event::Canceled(Canceled { reason, status })); + fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.pledges.get_pledges_as_arr() } fn pledge(ref self: ContractState, amount: u256) { - // start time check - assert(self._is_active() && !self._is_expired(), Errors::ENDED); + self._assert_active(); + assert(!self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let pledger = get_caller_address(); @@ -183,32 +211,10 @@ pub mod MockUpgrade { self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } - fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { - self.pledges.get(pledger) - } - - fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.pledges.get_pledges_as_arr() - } - - fn get_details(self: @ContractState) -> Details { - Details { - creator: self.creator.read(), - title: self.title.read(), - description: self.description.read(), - goal: self.goal.read(), - end_time: self.end_time.read(), - status: self.status.read(), - token: self.token.read().contract_address, - total_pledges: self.total_pledges.read(), - } - } - fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); - // start time check - assert(self._is_active(), Errors::ENDED); + self._assert_active(); + assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); let amount = self._refund(pledger); @@ -216,75 +222,54 @@ pub mod MockUpgrade { self.emit(Event::Refunded(Refunded { pledger, amount, reason })) } - /// There are currently 3 possibilities for performing contract upgrades: - /// 1. Trust the campaign factory owner -> this is suboptimal, as factory owners have no responsibility to either creators or pledgers, - /// and there's nothing stopping them from implementing a malicious upgrade. - /// 2. Trust the campaign creator -> the pledgers already trust the campaign creator that they'll do what they promised in the campaign. - /// It's not a stretch to trust them with verifying that the contract upgrade is necessary. - /// 3. Trust no one, contract upgrades are forbidden -> could be a problem if a vulnerability is discovered and campaign funds are in danger. - /// - /// This function implements the 2nd option, as it seems to be the most optimal solution, especially from the point of view of what to do if - /// any of the upgrades fail for whatever reason - campaign creator is solely responsible for upgrading their contracts. - /// - /// To improve pledger trust, contract upgrades refund all of pledger funds, so that on the off chance that the creator is in cahoots - /// with factory owners to implement a malicious upgrade, the pledger funds would be returned. - /// There are some problems with this though: - /// - pledgers wouldn't have even been donating if they weren't trusting the creator - since the funds end up with them in the end, they - /// have to trust that creators would use the campaign funds as they promised when creating the campaign. - /// - since the funds end up with the creators, they have no incentive to implement a malicious upgrade - they'll have the funds either way. - /// - each time there's an upgrade, the campaign gets reset, which introduces a new problem - what if the Campaign was cancel to ending? - /// We just took all of their pledges away, and there might not be enough time to get them back. We solve this by letting the creators - /// prolong the duration of the campaign. - fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_duration: Option) { + fn unpledge(ref self: ContractState, reason: ByteArray) { + self._assert_active(); + assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); + assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); + + let pledger = get_caller_address(); + let amount = self._refund(pledger); + + self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_end_time: Option) { self.ownable._assert_only_owner(); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // start time check - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); // only active campaigns have funds to refund and duration to update if self.status.read() == Status::ACTIVE { - let duration = match new_duration { - Option::Some(val) => val, - Option::None => 0, + if let Option::Some(end_time) = new_end_time { + assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW); + assert( + end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX + ); + self.end_time.write(end_time); }; - assert(duration > 0, Errors::ZERO_DURATION); self._refund_all("contract upgraded"); - self.total_pledges.write(0); - self.end_time.write(get_block_timestamp() + duration); } starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - - fn unpledge(ref self: ContractState, reason: ByteArray) { - // start time check - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); - assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); - - let pledger = get_caller_address(); - let amount = self._refund(pledger); - - self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); - } } #[generate_trait] - impl MockUpgradeInternalImpl of CampaignInternalTrait { + impl MockUpgradeInternalImpl of MockUpgradeInternalTrait { fn _assert_only_creator(self: @ContractState) { let caller = get_caller_address(); assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER); assert(caller == self.creator.read(), Errors::NOT_CREATOR); } - fn _is_expired(self: @ContractState) -> bool { - get_block_timestamp() >= self.end_time.read() + fn _assert_active(self: @ContractState) { + assert(get_block_timestamp() >= self.start_time.read(), Errors::NOT_STARTED); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); } - fn _is_active(self: @ContractState) -> bool { - self.status.read() == Status::ACTIVE + fn _is_expired(self: @ContractState) -> bool { + get_block_timestamp() >= self.end_time.read() } fn _is_goal_reached(self: @ContractState) -> bool { diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 7c9b40c6..3f8b684d 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -21,12 +21,13 @@ fn deploy( title: ByteArray, description: ByteArray, goal: u256, - duration: u64, + start_time: u64, + end_time: u64, token: ContractAddress ) -> ICampaignDispatcher { let creator = contract_address_const::<'creator'>(); let mut calldata: Array:: = array![]; - ((creator, title, description, goal), duration, token).serialize(ref calldata); + ((creator, title, description, goal), start_time, end_time, token).serialize(ref calldata); let contract_address = contract.precalculate_address(@calldata); let owner = contract_address_const::<'owner'>(); @@ -65,8 +66,10 @@ fn deploy_with_token( token_dispatcher.transfer(pledger_3, 10000); // deploy the actual Campaign contract + let start_time = get_block_timestamp(); + let end_time = start_time + 60; let campaign_dispatcher = deploy( - contract, "title 1", "description 1", 10000, 60, token_address + contract, "title 1", "description 1", 10000, start_time, end_time, token_address ); // approve the pledges for each pledger @@ -87,16 +90,25 @@ fn deploy_with_token( #[test] fn test_deploy() { + let start_time = get_block_timestamp(); + let end_time = start_time + 60; let contract = declare("Campaign").unwrap(); let campaign = deploy( - contract, "title 1", "description 1", 10000, 60, contract_address_const::<'token'>() + contract, + "title 1", + "description 1", + 10000, + start_time, + end_time, + contract_address_const::<'token'>() ); let details = campaign.get_details(); assert_eq!(details.title, "title 1"); assert_eq!(details.description, "description 1"); assert_eq!(details.goal, 10000); - assert_eq!(details.end_time, get_block_timestamp() + 60); + assert_eq!(details.start_time, start_time); + assert_eq!(details.end_time, end_time); assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_pledges, 0);