diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 8b9d1bd3..a9e79006 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -87,6 +87,7 @@ pub mod Campaign { Closed: Closed, Upgraded: Upgraded, Withdrawn: Withdrawn, + WithdrawnAll: WithdrawnAll, } #[derive(Drop, starknet::Event)] @@ -121,6 +122,11 @@ pub mod Campaign { pub amount: u256, } + #[derive(Drop, starknet::Event)] + pub struct WithdrawnAll { + pub reason: ByteArray, + } + pub mod Errors { pub const NOT_CREATOR: felt252 = 'Not creator'; pub const ENDED: felt252 = 'Campaign already ended'; @@ -150,7 +156,8 @@ pub mod Campaign { title: ByteArray, description: ByteArray, target: u256, - token_address: ContractAddress + token_address: ContractAddress, + // TODO: add recepient address ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); @@ -201,7 +208,7 @@ pub mod Campaign { self.status.write(Status::CLOSED); } - self._withdraw_all(); + self._withdraw_all(reason.clone()); self.emit(Event::Closed(Closed { reason })); } @@ -288,7 +295,7 @@ pub mod Campaign { Option::None => 0, }; assert(duration > 0, Errors::ZERO_DURATION); - self._withdraw_all(); + self._withdraw_all("contract upgraded"); self.total_contributions.write(0); self.end_time.write(get_block_timestamp() + duration); } @@ -327,7 +334,7 @@ pub mod Campaign { } fn _is_expired(self: @ContractState) -> bool { - get_block_timestamp() < self.end_time.read() + get_block_timestamp() >= self.end_time.read() } fn _is_active(self: @ContractState) -> bool { @@ -338,7 +345,7 @@ pub mod Campaign { self.total_contributions.read() >= self.target.read() } - fn _withdraw_all(ref self: ContractState) { + fn _withdraw_all(ref self: ContractState, reason: ByteArray) { let mut contributions = self.contributions.get_contributions_as_arr(); while let Option::Some((contributor, amt)) = contributions .pop_front() { @@ -346,6 +353,8 @@ pub mod Campaign { let success = self.token.read().transfer(contributor, amt); assert(success, Errors::TRANSFER_FAILED); }; + + self.emit(Event::WithdrawnAll(WithdrawnAll { reason })); } } } diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 919c40ae..2b3227ef 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -1,8 +1,321 @@ #[starknet::contract] -pub mod MockContract { +pub mod MockUpgrade { + use components::ownable::ownable_component::OwnableInternalTrait; + use core::num::traits::zero::Zero; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + use starknet::{ + ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, contract_address_const, + get_caller_address, get_contract_address, class_hash::class_hash_const + }; + use components::ownable::ownable_component; + use crowdfunding::campaign::contributions::contributable_component; + use crowdfunding::campaign::{ICampaign, Details, Status}; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + component!(path: contributable_component, storage: contributions, event: ContributableEvent); + + #[abi(embed_v0)] + pub impl OwnableImpl = ownable_component::Ownable; + impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; + #[abi(embed_v0)] + impl ContributableImpl = contributable_component::Contributable; + #[storage] - struct Storage {} + struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, + #[substorage(v0)] + contributions: contributable_component::Storage, + end_time: u64, + token: IERC20Dispatcher, + creator: ContractAddress, + target: u256, + title: ByteArray, + description: ByteArray, + total_contributions: u256, + status: Status + } + #[event] #[derive(Drop, starknet::Event)] - enum Event {} + pub enum Event { + #[flat] + OwnableEvent: ownable_component::Event, + Activated: Activated, + ContributableEvent: contributable_component::Event, + ContributionMade: ContributionMade, + Claimed: Claimed, + Closed: Closed, + Upgraded: Upgraded, + Withdrawn: Withdrawn, + WithdrawnAll: WithdrawnAll, + } + + #[derive(Drop, starknet::Event)] + pub struct ContributionMade { + #[key] + pub contributor: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Claimed { + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Activated {} + + #[derive(Drop, starknet::Event)] + pub struct Closed { + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Upgraded { + pub implementation: ClassHash + } + + #[derive(Drop, starknet::Event)] + pub struct Withdrawn { + #[key] + pub contributor: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct WithdrawnAll { + pub reason: ByteArray, + } + + pub mod Errors { + pub const NOT_CREATOR: felt252 = 'Not creator'; + pub const ENDED: felt252 = 'Campaign already ended'; + pub const NOT_PENDING: felt252 = 'Campaign not pending'; + pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; + pub const STILL_PENDING: felt252 = 'Campaign not yet active'; + pub const CLOSED: felt252 = 'Campaign closed'; + 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 ZERO_FUNDS: felt252 = 'No funds to claim'; + 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 withdraw'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + creator: ContractAddress, + title: ByteArray, + description: ByteArray, + target: u256, + token_address: ContractAddress, + // TODO: add recepient address + ) { + assert(creator.is_non_zero(), Errors::CREATOR_ZERO); + assert(title.len() > 0, Errors::TITLE_EMPTY); + assert(target > 0, Errors::ZERO_TARGET); + + self.token.write(IERC20Dispatcher { contract_address: token_address }); + + self.title.write(title); + self.target.write(target); + self.description.write(description); + self.creator.write(creator); + self.ownable._init(get_caller_address()); + self.status.write(Status::PENDING) + } + + #[abi(embed_v0)] + impl MockUpgrade of ICampaign { + fn claim(ref self: ContractState) { + self._assert_only_creator(); + assert(self._is_active() && self._is_expired(), Errors::STILL_ACTIVE); + assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); + + let this = get_contract_address(); + let token = self.token.read(); + + let amount = token.balance_of(this); + assert(amount > 0, Errors::ZERO_FUNDS); + + self.status.write(Status::SUCCESSFUL); + + // no need to reset the contributions, as the campaign has ended + // and the data can be used as a testament to how much was raised + + let owner = get_caller_address(); + let success = token.transfer(owner, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Claimed(Claimed { amount })); + } + + fn close(ref self: ContractState, reason: ByteArray) { + self._assert_only_creator(); + assert(self._is_active(), Errors::ENDED); + + if !self._is_target_reached() && self._is_expired() { + self.status.write(Status::FAILED); + } else { + self.status.write(Status::CLOSED); + } + + self._withdraw_all(reason.clone()); + + self.emit(Event::Closed(Closed { reason })); + } + + fn contribute(ref self: ContractState, amount: u256) { + assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self._is_active() && !self._is_expired(), Errors::ENDED); + assert(amount > 0, Errors::ZERO_DONATION); + + let contributor = get_caller_address(); + let this = get_contract_address(); + let success = self.token.read().transfer_from(contributor, this, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.contributions.add(contributor, amount); + self.total_contributions.write(self.total_contributions.read() + amount); + + self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + } + + fn get_contribution(self: @ContractState, contributor: ContractAddress) -> u256 { + self.contributions.get(contributor) + } + + fn get_contributions(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.contributions.get_contributions_as_arr() + } + + fn get_details(self: @ContractState) -> Details { + Details { + creator: self.creator.read(), + title: self.title.read(), + description: self.description.read(), + target: self.target.read(), + end_time: self.end_time.read(), + status: self.status.read(), + token: self.token.read().contract_address, + total_contributions: self.total_contributions.read(), + } + } + + fn start(ref self: ContractState, duration: u64) { + self._assert_only_creator(); + assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); + assert(duration > 0, Errors::ZERO_DURATION); + + self.end_time.write(get_block_timestamp() + duration); + self.status.write(Status::ACTIVE); + + self.emit(Event::Activated(Activated {})); + } + + /// 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 contributors, + /// and there's nothing stopping them from implementing a malicious upgrade. + /// 2. Trust the campaign creator -> the contributors 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 contributor trust, contract upgrades refund all of contributor funds, so that on the off chance that the creator is in cahoots + /// with factory owners to implement a malicious upgrade, the contributor funds would be returned. + /// There are some problems with this though: + /// - contributors 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 close to ending? + /// We just took all of their contributions 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) { + self.ownable._assert_only_owner(); + assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + assert( + self.status.read() == Status::ACTIVE || self.status.read() == Status::PENDING, + 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, + }; + assert(duration > 0, Errors::ZERO_DURATION); + self._withdraw_all("contract upgraded"); + self.total_contributions.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 withdraw(ref self: ContractState) { + assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); + assert(self.status.read() != Status::CLOSED, Errors::CLOSED); + assert(!self._is_target_reached(), Errors::TARGET_ALREADY_REACHED); + assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); + + let contributor = get_caller_address(); + let amount = self.contributions.remove(contributor); + + // no need to set total_contributions to 0, as the campaign has ended + // and the field can be used as a testament to how much was raised + + let success = self.token.read().transfer(contributor, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); + } + } + + #[generate_trait] + impl MockUpgradeInternalImpl of CampaignInternalTrait { + 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 _is_active(self: @ContractState) -> bool { + self.status.read() == Status::ACTIVE + } + + fn _is_target_reached(self: @ContractState) -> bool { + self.total_contributions.read() >= self.target.read() + } + + fn _withdraw_all(ref self: ContractState, reason: ByteArray) { + let mut contributions = self.contributions.get_contributions_as_arr(); + while let Option::Some((contributor, amt)) = contributions + .pop_front() { + self.contributions.remove(contributor); + let success = self.token.read().transfer(contributor, amt); + assert(success, Errors::TRANSFER_FAILED); + }; + + self.emit(Event::WithdrawnAll(WithdrawnAll { reason })); + } + } } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index c60baf0f..79c07889 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -6,7 +6,8 @@ use starknet::{ }; use snforge_std::{ declare, ContractClass, ContractClassTrait, start_cheat_caller_address, - stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash + stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash, + cheat_block_timestamp_global }; use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; @@ -17,14 +18,17 @@ use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTr const ERC20_SUPPLY: u256 = 10000; /// Deploy a campaign contract with the provided data -fn deploy_with( - title: ByteArray, description: ByteArray, target: u256, duration: u64, token: ContractAddress +fn deploy( + contract: ContractClass, + title: ByteArray, + description: ByteArray, + target: u256, + token: ContractAddress ) -> ICampaignDispatcher { let creator = contract_address_const::<'creator'>(); let mut calldata: Array:: = array![]; - ((creator, title, description, target), duration, token).serialize(ref calldata); + ((creator, title, description, target), token).serialize(ref calldata); - let contract = declare("Campaign").unwrap(); let contract_address = contract.precalculate_address(@calldata); let owner = contract_address_const::<'owner'>(); start_cheat_caller_address(contract_address, owner); @@ -36,68 +40,62 @@ fn deploy_with( ICampaignDispatcher { contract_address } } -/// Deploy a campaign contract with default data -fn deploy() -> ICampaignDispatcher { - deploy_with("title 1", "description 1", 10000, 60, contract_address_const::<'token'>()) -} - -fn deploy_with_token() -> ICampaignDispatcher { +fn deploy_with_token( + contract: ContractClass, token: ContractClass +) -> (ICampaignDispatcher, IERC20Dispatcher) { // define ERC20 data - let erc20_name: ByteArray = "My Token"; - let erc20_symbol: ByteArray = "MTKN"; - let erc20_supply: u256 = 100000; - let erc20_owner = contract_address_const::<'erc20_owner'>(); + let token_name: ByteArray = "My Token"; + let token_symbol: ByteArray = "MTKN"; + let token_supply: u256 = 100000; + let token_owner = contract_address_const::<'token_owner'>(); // deploy ERC20 token - let erc20 = declare("ERC20").unwrap(); - let mut erc20_constructor_calldata = array![]; - (erc20_name, erc20_symbol, erc20_supply, erc20_owner).serialize(ref erc20_constructor_calldata); - let (erc20_address, _) = erc20.deploy(@erc20_constructor_calldata).unwrap(); + let mut token_constructor_calldata = array![]; + (token_name, token_symbol, token_supply, token_owner).serialize(ref token_constructor_calldata); + let (token_address, _) = token.deploy(@token_constructor_calldata).unwrap(); // transfer amounts to some contributors let contributor_1 = contract_address_const::<'contributor_1'>(); let contributor_2 = contract_address_const::<'contributor_2'>(); let contributor_3 = contract_address_const::<'contributor_3'>(); - start_cheat_caller_address(erc20_address, erc20_owner); - let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_address }; - erc20_dispatcher.transfer(contributor_1, 10000); - erc20_dispatcher.transfer(contributor_2, 10000); - erc20_dispatcher.transfer(contributor_3, 10000); + start_cheat_caller_address(token_address, token_owner); + let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + token_dispatcher.transfer(contributor_1, 10000); + token_dispatcher.transfer(contributor_2, 10000); + token_dispatcher.transfer(contributor_3, 10000); // deploy the actual Campaign contract - let campaign_dispatcher = deploy_with("title 1", "description 1", 10000, 60, erc20_address); + let campaign_dispatcher = deploy(contract, "title 1", "description 1", 10000, token_address); // approve the contributions for each contributor - start_cheat_caller_address(erc20_address, contributor_1); - erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000); - start_cheat_caller_address(erc20_address, contributor_2); - erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000); - start_cheat_caller_address(erc20_address, contributor_3); - erc20_dispatcher.approve(campaign_dispatcher.contract_address, 10000); + start_cheat_caller_address(token_address, contributor_1); + token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); + start_cheat_caller_address(token_address, contributor_2); + token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); + start_cheat_caller_address(token_address, contributor_3); + token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); // NOTE: don't forget to stop the caller address cheat on the ERC20 contract!! // Otherwise, any call to this contract from any source will have the cheated // address as the caller - stop_cheat_caller_address(erc20_address); - - campaign_dispatcher -} + stop_cheat_caller_address(token_address); -fn _get_token_dispatcher(campaign: ICampaignDispatcher) -> IERC20Dispatcher { - let token_address = campaign.get_details().token; - IERC20Dispatcher { contract_address: token_address } + (campaign_dispatcher, token_dispatcher) } #[test] fn test_deploy() { - let campaign = deploy(); + let contract = declare("Campaign").unwrap(); + let campaign = deploy( + contract, "title 1", "description 1", 10000, contract_address_const::<'token'>() + ); let details = campaign.get_details(); assert_eq!(details.title, "title 1"); assert_eq!(details.description, "description 1"); assert_eq!(details.target, 10000); - assert_eq!(details.end_time, get_block_timestamp() + 60); + assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_contributions, 0); @@ -110,18 +108,30 @@ fn test_deploy() { #[test] fn test_successful_campaign() { - let campaign = deploy_with_token(); - let token = _get_token_dispatcher(campaign); + let token_class = declare("ERC20").unwrap(); + let contract_class = declare("Campaign").unwrap(); + let (campaign, token) = deploy_with_token(contract_class, token_class); + let duration: u64 = 60; let creator = contract_address_const::<'creator'>(); let contributor_1 = contract_address_const::<'contributor_1'>(); let contributor_2 = contract_address_const::<'contributor_2'>(); let contributor_3 = contract_address_const::<'contributor_3'>(); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + + // start campaign start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(); + campaign.start(duration); assert_eq!(campaign.get_details().status, Status::ACTIVE); + assert_eq!(campaign.get_details().end_time, get_block_timestamp() + duration); + + spy + .assert_emitted( + @array![(campaign.contract_address, Campaign::Event::Activated(Campaign::Activated {}))] + ); + // 1st donation start_cheat_caller_address(campaign.contract_address, contributor_1); let mut prev_balance = token.balance_of(contributor_1); campaign.contribute(3000); @@ -129,6 +139,19 @@ fn test_successful_campaign() { assert_eq!(campaign.get_contribution(contributor_1), 3000); assert_eq!(token.balance_of(contributor_1), prev_balance - 3000); + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::ContributionMade( + Campaign::ContributionMade { contributor: contributor_1, amount: 3000 } + ) + ) + ] + ); + + // 2nd donation start_cheat_caller_address(campaign.contract_address, contributor_2); prev_balance = token.balance_of(contributor_2); campaign.contribute(500); @@ -136,6 +159,19 @@ fn test_successful_campaign() { assert_eq!(campaign.get_contribution(contributor_2), 500); assert_eq!(token.balance_of(contributor_2), prev_balance - 500); + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::ContributionMade( + Campaign::ContributionMade { contributor: contributor_2, amount: 500 } + ) + ) + ] + ); + + // 3rd donation start_cheat_caller_address(campaign.contract_address, contributor_3); prev_balance = token.balance_of(contributor_3); campaign.contribute(7000); @@ -143,27 +179,51 @@ fn test_successful_campaign() { assert_eq!(campaign.get_contribution(contributor_3), 7000); assert_eq!(token.balance_of(contributor_3), prev_balance - 7000); + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::ContributionMade( + Campaign::ContributionMade { contributor: contributor_3, amount: 7000 } + ) + ) + ] + ); + + // claim + cheat_block_timestamp_global(get_block_timestamp() + duration); start_cheat_caller_address(campaign.contract_address, creator); prev_balance = token.balance_of(creator); campaign.claim(); assert_eq!(token.balance_of(creator), prev_balance + 10500); assert_eq!(campaign.get_details().status, Status::SUCCESSFUL); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Claimed(Campaign::Claimed { amount: 10500 }) + ) + ] + ); } #[test] fn test_upgrade_class_hash() { - let campaign = deploy(); + let new_class_hash = declare("MockUpgrade").unwrap().class_hash; + let owner = contract_address_const::<'owner'>(); + // test pending campaign + let contract_class = declare("Campaign").unwrap(); + let token_class = declare("ERC20").unwrap(); + let (campaign, _) = deploy_with_token(contract_class, token_class); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); - let new_class_hash = declare("MockContract").unwrap().class_hash; - - let owner = contract_address_const::<'owner'>(); start_cheat_caller_address(campaign.contract_address, owner); - - if let Result::Err(errs) = campaign.upgrade(new_class_hash) { - panic(errs) - } + campaign.upgrade(new_class_hash, Option::None); + stop_cheat_caller_address(campaign.contract_address); assert_eq!(get_class_hash(campaign.contract_address), new_class_hash); @@ -176,20 +236,52 @@ fn test_upgrade_class_hash() { ) ] ); -} -#[test] -#[should_panic(expected: 'Not owner')] -fn test_upgrade_class_hash_fail() { - let campaign = deploy(); + // test active campaign + let (campaign, token) = deploy_with_token(contract_class, token_class); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + let duration: u64 = 60; + let creator = contract_address_const::<'creator'>(); + let contributor_1 = contract_address_const::<'contributor_1'>(); + let contributor_2 = contract_address_const::<'contributor_2'>(); + let contributor_3 = contract_address_const::<'contributor_3'>(); + let prev_balance_contributor_1 = token.balance_of(contributor_1); + let prev_balance_contributor_2 = token.balance_of(contributor_2); + let prev_balance_contributor_3 = token.balance_of(contributor_3); + + start_cheat_caller_address(campaign.contract_address, creator); + campaign.start(duration); + start_cheat_caller_address(campaign.contract_address, contributor_1); + campaign.contribute(3000); + start_cheat_caller_address(campaign.contract_address, contributor_2); + campaign.contribute(1000); + start_cheat_caller_address(campaign.contract_address, contributor_3); + campaign.contribute(2000); - let new_class_hash = declare("MockContract").unwrap().class_hash; + start_cheat_caller_address(campaign.contract_address, owner); + campaign.upgrade(new_class_hash, Option::Some(duration)); + stop_cheat_caller_address(campaign.contract_address); - let random_address = contract_address_const::<'random_address'>(); - start_cheat_caller_address(campaign.contract_address, random_address); + assert_eq!(prev_balance_contributor_1, token.balance_of(contributor_1)); + assert_eq!(prev_balance_contributor_2, token.balance_of(contributor_2)); + assert_eq!(prev_balance_contributor_3, token.balance_of(contributor_3)); + assert_eq!(campaign.get_details().total_contributions, 0); + assert_eq!(campaign.get_details().end_time, get_block_timestamp() + duration); - if let Result::Err(errs) = campaign.upgrade(new_class_hash) { - panic(errs) - } + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ), + ( + campaign.contract_address, + Campaign::Event::WithdrawnAll( + Campaign::WithdrawnAll { reason: "contract upgraded" } + ) + ) + ] + ); }