From c3c178131cdf34311df11e21266bb01b4f3d5f99 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 08:10:13 +0200 Subject: [PATCH 001/116] add initial factory --- Scarb.lock | 9 ++ .../applications/advanced_factory/.gitignore | 2 + .../applications/advanced_factory/Scarb.toml | 19 +++ .../advanced_factory/src/lib.cairo | 113 ++++++++++++++++++ .../advanced_factory/src/simple_counter.cairo | 39 ++++++ .../advanced_factory/src/tests.cairo | 64 ++++++++++ 6 files changed, 246 insertions(+) create mode 100644 listings/applications/advanced_factory/.gitignore create mode 100644 listings/applications/advanced_factory/Scarb.toml create mode 100644 listings/applications/advanced_factory/src/lib.cairo create mode 100644 listings/applications/advanced_factory/src/simple_counter.cairo create mode 100644 listings/applications/advanced_factory/src/tests.cairo diff --git a/Scarb.lock b/Scarb.lock index c5dc5433..bd475f38 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,6 +1,15 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "advanced_factory" +version = "0.1.0" +dependencies = [ + "components", + "openzeppelin", + "snforge_std", +] + [[package]] name = "alexandria_storage" version = "0.3.0" diff --git a/listings/applications/advanced_factory/.gitignore b/listings/applications/advanced_factory/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/listings/applications/advanced_factory/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml new file mode 100644 index 00000000..2f188542 --- /dev/null +++ b/listings/applications/advanced_factory/Scarb.toml @@ -0,0 +1,19 @@ +[package] +name = "advanced_factory" +version.workspace = true +edition = "2023_11" + +[dependencies] +starknet.workspace = true +# Starknet Foundry: +snforge_std.workspace = true +# OpenZeppelin: +openzeppelin.workspace = true +# StarknetByExample Components +components.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] +casm = true diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo new file mode 100644 index 00000000..354d32ae --- /dev/null +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -0,0 +1,113 @@ +// ANCHOR: contract +pub use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +pub trait ICounterFactory { + /// Create a new counter contract from stored arguments + fn create_counter(ref self: TContractState) -> ContractAddress; + + /// Create a new counter contract from the given arguments + fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress; + + /// Update the argument + fn update_init_value(ref self: TContractState, new_init_value: u128); + + /// Update the class hash of the Counter contract to deploy when creating a new counter + fn update_counter_class_hash(ref self: TContractState, new_class_hash: ClassHash); +} + +#[starknet::contract] +pub mod CounterFactory { + use core::starknet::event::EventEmitter; + use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall}; + + #[storage] + struct Storage { + /// Store the constructor arguments of the contract to deploy + init_value: u128, + /// Store the class hash of the contract to deploy + counter_class_hash: ClassHash, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + ClassHashUpdated: ClassHashUpdated, + CounterCreated: CounterCreated, + InitValueUpdated: InitValueUpdated, + } + + #[derive(Drop, starknet::Event)] + struct ClassHashUpdated { + previous_class_hash: ClassHash, + new_class_hash: ClassHash, + } + + #[derive(Drop, starknet::Event)] + struct CounterCreated { + deployed_address: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct InitValueUpdated { + previous_init_value: u128, + new_init_value: u128, + } + + #[constructor] + fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) { + self.init_value.write(init_value); + self.counter_class_hash.write(class_hash); + } + + #[abi(embed_v0)] + impl Factory of super::ICounterFactory { + // ANCHOR: deploy + fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress { + // Contructor arguments + let mut constructor_calldata: Array:: = array![init_value.into()]; + + // Contract deployment + let (deployed_address, _) = deploy_syscall( + self.counter_class_hash.read(), 0, constructor_calldata.span(), false + ) + .unwrap_syscall(); + + self.emit(Event::CounterCreated(CounterCreated { deployed_address })); + + deployed_address + } + // ANCHOR_END: deploy + + fn create_counter(ref self: ContractState) -> ContractAddress { + self.create_counter_at(self.init_value.read()) + } + + fn update_init_value(ref self: ContractState, new_init_value: u128) { + let previous_init_value = self.init_value.read(); + self.init_value.write(new_init_value); + self + .emit( + Event::InitValueUpdated( + InitValueUpdated { previous_init_value, new_init_value } + ) + ); + } + + fn update_counter_class_hash(ref self: ContractState, new_class_hash: ClassHash) { + let previous_class_hash = self.counter_class_hash.read(); + self.counter_class_hash.write(new_class_hash); + self + .emit( + Event::ClassHashUpdated( + ClassHashUpdated { previous_class_hash, new_class_hash } + ) + ); + } + } +} +// ANCHOR_END: contract + +#[cfg(test)] +mod tests; +mod simple_counter; diff --git a/listings/applications/advanced_factory/src/simple_counter.cairo b/listings/applications/advanced_factory/src/simple_counter.cairo new file mode 100644 index 00000000..0f139272 --- /dev/null +++ b/listings/applications/advanced_factory/src/simple_counter.cairo @@ -0,0 +1,39 @@ +#[starknet::interface] +pub trait ISimpleCounter { + fn get_current_count(self: @TContractState) -> u128; + fn increment(ref self: TContractState); + fn decrement(ref self: TContractState); +} + +#[starknet::contract] +pub mod SimpleCounter { + #[storage] + struct Storage { + // Counter variable + counter: u128, + } + + #[constructor] + fn constructor(ref self: ContractState, init_value: u128) { + // Store initial value + self.counter.write(init_value); + } + + #[abi(embed_v0)] + impl SimpleCounter of super::ISimpleCounter { + fn get_current_count(self: @ContractState) -> u128 { + self.counter.read() + } + + fn increment(ref self: ContractState) { + // Store counter value + 1 + let mut counter: u128 = self.counter.read() + 1; + self.counter.write(counter); + } + fn decrement(ref self: ContractState) { + // Store counter value - 1 + let mut counter: u128 = self.counter.read() - 1; + self.counter.write(counter); + } + } +} diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo new file mode 100644 index 00000000..6d683f06 --- /dev/null +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -0,0 +1,64 @@ +use core::result::ResultTrait; +use advanced_factory::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait}; +use starknet::{ContractAddress, ClassHash,}; +use snforge_std::{declare, ContractClass, ContractClassTrait}; + +// Define a target contract to deploy +use advanced_factory::simple_counter::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait}; + +/// Deploy a counter factory contract +fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFactoryDispatcher { + let mut constructor_calldata: @Array:: = @array![ + init_value.into(), counter_class_hash.into() + ]; + + let contract = declare("CounterFactory").unwrap(); + let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); + + ICounterFactoryDispatcher { contract_address } +} + +#[test] +fn test_deploy_counter_constructor() { + let init_value = 10; + + let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; + let factory = deploy_factory(counter_class_hash, init_value); + + let counter_address = factory.create_counter(); + let counter = ISimpleCounterDispatcher { contract_address: counter_address }; + + assert_eq!(counter.get_current_count(), init_value); +} + +#[test] +fn test_deploy_counter_argument() { + let init_value = 10; + let argument_value = 20; + + let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; + let factory = deploy_factory(counter_class_hash, init_value); + + let counter_address = factory.create_counter_at(argument_value); + let counter = ISimpleCounterDispatcher { contract_address: counter_address }; + + assert_eq!(counter.get_current_count(), argument_value); +} + +#[test] +fn test_deploy_multiple() { + let init_value = 10; + let argument_value = 20; + + let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; + let factory = deploy_factory(counter_class_hash, init_value); + + let mut counter_address = factory.create_counter(); + let counter_1 = ISimpleCounterDispatcher { contract_address: counter_address }; + + counter_address = factory.create_counter_at(argument_value); + let counter_2 = ISimpleCounterDispatcher { contract_address: counter_address }; + + assert_eq!(counter_1.get_current_count(), init_value); + assert_eq!(counter_2.get_current_count(), argument_value); +} From 77e0f4c8dd63ed54e0f9c306c88749f08882f4cf Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 09:08:47 +0200 Subject: [PATCH 002/116] add ownable component --- .../advanced_factory/src/lib.cairo | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index 354d32ae..4e1eb1b2 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -18,11 +18,23 @@ pub trait ICounterFactory { #[starknet::contract] pub mod CounterFactory { + use components::ownable::ownable_component::OwnableInternalTrait; use core::starknet::event::EventEmitter; - use starknet::{ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall}; + use starknet::{ + ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall, get_caller_address + }; + use components::ownable::ownable_component; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = ownable_component::Ownable; + impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; #[storage] struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, /// Store the constructor arguments of the contract to deploy init_value: u128, /// Store the class hash of the contract to deploy @@ -32,6 +44,8 @@ pub mod CounterFactory { #[event] #[derive(Drop, starknet::Event)] pub enum Event { + #[flat] + OwnableEvent: ownable_component::Event, ClassHashUpdated: ClassHashUpdated, CounterCreated: CounterCreated, InitValueUpdated: InitValueUpdated, @@ -58,6 +72,7 @@ pub mod CounterFactory { fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) { self.init_value.write(init_value); self.counter_class_hash.write(class_hash); + self.ownable._init(get_caller_address()); } #[abi(embed_v0)] @@ -84,6 +99,7 @@ pub mod CounterFactory { } fn update_init_value(ref self: ContractState, new_init_value: u128) { + self.ownable._assert_only_owner(); let previous_init_value = self.init_value.read(); self.init_value.write(new_init_value); self @@ -95,6 +111,7 @@ pub mod CounterFactory { } fn update_counter_class_hash(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable._assert_only_owner(); let previous_class_hash = self.counter_class_hash.read(); self.counter_class_hash.write(new_class_hash); self From 44826995991761f8578ad948310fcb4c614b2ac8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 09:34:30 +0200 Subject: [PATCH 003/116] add caller to CounterCreated event --- listings/applications/advanced_factory/src/lib.cairo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index 4e1eb1b2..7fa949b8 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -59,6 +59,7 @@ pub mod CounterFactory { #[derive(Drop, starknet::Event)] struct CounterCreated { + caller: ContractAddress, deployed_address: ContractAddress } @@ -88,7 +89,8 @@ pub mod CounterFactory { ) .unwrap_syscall(); - self.emit(Event::CounterCreated(CounterCreated { deployed_address })); + let caller = get_caller_address(); + self.emit(Event::CounterCreated(CounterCreated { caller, deployed_address })); deployed_address } From 8c4041d9432b05b7db14b0e5feecc7fbff434fc6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 11:12:25 +0200 Subject: [PATCH 004/116] turn counter into campaign --- .../advanced_factory/src/campaign.cairo | 102 ++++++++++++++++++ .../advanced_factory/src/lib.cairo | 2 +- .../advanced_factory/src/simple_counter.cairo | 39 ------- .../advanced_factory/src/tests.cairo | 64 +++++------ 4 files changed, 135 insertions(+), 72 deletions(-) create mode 100644 listings/applications/advanced_factory/src/campaign.cairo delete mode 100644 listings/applications/advanced_factory/src/simple_counter.cairo diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo new file mode 100644 index 00000000..40135d47 --- /dev/null +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -0,0 +1,102 @@ +#[starknet::interface] +pub trait ICampaign { + fn get_current_count(self: @TContractState) -> u128; + fn increment(ref self: TContractState); + fn decrement(ref self: TContractState); +} + + +#[starknet::contract] +pub mod Campaign { + use components::ownable::ownable_component::OwnableInternalTrait; + use core::num::traits::zero::Zero; + use starknet::{ + ClassHash, ContractAddress, get_block_timestamp, syscalls::replace_class_syscall, + get_caller_address + }; + use components::ownable::ownable_component; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = ownable_component::Ownable; + impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, + donations: LegacyMap, + end_time: u64, + factory: ContractAddress, + target: u128, + total_donations: u128, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: ownable_component::Event, + Donated: Donated, + Withdrawn: Withdrawn, + Upgraded: Upgraded, + } + + #[derive(Drop, starknet::Event)] + pub struct Donated { + #[key] + pub donor: ContractAddress, + pub amount: u128, + } + + #[derive(Drop, starknet::Event)] + pub struct Withdrawn { + pub amount: u128, + } + + #[derive(Drop, starknet::Event)] + pub struct Upgraded { + pub implementation: ClassHash + } + + pub mod Errors { + pub const NOT_FACTORY: felt252 = 'Not factory'; + pub const INACTIVE: felt252 = 'Campaign no longer active'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + creator: ContractAddress, + target: u128, + duration: u64, + factory: ContractAddress + ) { + assert(factory.is_non_zero(), 'factory address zero'); + assert(creator.is_non_zero(), 'creator address zero'); + assert(target > 0, 'target == 0'); + assert(duration > 0, 'duration == 0'); + + self.target.write(target); + self.end_time.write(get_block_timestamp() + duration); + self.factory.write(factory); + self.ownable._init(creator); + } + // #[abi(embed_v0)] + // impl Campaign of super::ICampaign { + + // } + + #[generate_trait] + impl CampaignInternal of CampaignInternalTrait { + fn _assert_only_factory(self: @ContractState) { + let caller = get_caller_address(); + assert(caller == self.factory.read(), Errors::NOT_FACTORY); + } + + fn _assert_only_active(self: @ContractState) { + assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); + } + } +} diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index 7fa949b8..356d1574 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -129,4 +129,4 @@ pub mod CounterFactory { #[cfg(test)] mod tests; -mod simple_counter; +mod campaign; diff --git a/listings/applications/advanced_factory/src/simple_counter.cairo b/listings/applications/advanced_factory/src/simple_counter.cairo deleted file mode 100644 index 0f139272..00000000 --- a/listings/applications/advanced_factory/src/simple_counter.cairo +++ /dev/null @@ -1,39 +0,0 @@ -#[starknet::interface] -pub trait ISimpleCounter { - fn get_current_count(self: @TContractState) -> u128; - fn increment(ref self: TContractState); - fn decrement(ref self: TContractState); -} - -#[starknet::contract] -pub mod SimpleCounter { - #[storage] - struct Storage { - // Counter variable - counter: u128, - } - - #[constructor] - fn constructor(ref self: ContractState, init_value: u128) { - // Store initial value - self.counter.write(init_value); - } - - #[abi(embed_v0)] - impl SimpleCounter of super::ISimpleCounter { - fn get_current_count(self: @ContractState) -> u128 { - self.counter.read() - } - - fn increment(ref self: ContractState) { - // Store counter value + 1 - let mut counter: u128 = self.counter.read() + 1; - self.counter.write(counter); - } - fn decrement(ref self: ContractState) { - // Store counter value - 1 - let mut counter: u128 = self.counter.read() - 1; - self.counter.write(counter); - } - } -} diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 6d683f06..f569ae7d 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -17,48 +17,48 @@ fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFa ICounterFactoryDispatcher { contract_address } } +// #[test] +// fn test_deploy_counter_constructor() { +// let init_value = 10; -#[test] -fn test_deploy_counter_constructor() { - let init_value = 10; +// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; +// let factory = deploy_factory(counter_class_hash, init_value); - let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; - let factory = deploy_factory(counter_class_hash, init_value); +// let counter_address = factory.create_counter(); +// let counter = ISimpleCounterDispatcher { contract_address: counter_address }; - let counter_address = factory.create_counter(); - let counter = ISimpleCounterDispatcher { contract_address: counter_address }; +// assert_eq!(counter.get_current_count(), init_value); +// } - assert_eq!(counter.get_current_count(), init_value); -} +// #[test] +// fn test_deploy_counter_argument() { +// let init_value = 10; +// let argument_value = 20; -#[test] -fn test_deploy_counter_argument() { - let init_value = 10; - let argument_value = 20; +// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; +// let factory = deploy_factory(counter_class_hash, init_value); - let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; - let factory = deploy_factory(counter_class_hash, init_value); +// let counter_address = factory.create_counter_at(argument_value); +// let counter = ISimpleCounterDispatcher { contract_address: counter_address }; - let counter_address = factory.create_counter_at(argument_value); - let counter = ISimpleCounterDispatcher { contract_address: counter_address }; +// assert_eq!(counter.get_current_count(), argument_value); +// } - assert_eq!(counter.get_current_count(), argument_value); -} +// #[test] +// fn test_deploy_multiple() { +// let init_value = 10; +// let argument_value = 20; -#[test] -fn test_deploy_multiple() { - let init_value = 10; - let argument_value = 20; +// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; +// let factory = deploy_factory(counter_class_hash, init_value); - let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; - let factory = deploy_factory(counter_class_hash, init_value); +// let mut counter_address = factory.create_counter(); +// let counter_1 = ISimpleCounterDispatcher { contract_address: counter_address }; - let mut counter_address = factory.create_counter(); - let counter_1 = ISimpleCounterDispatcher { contract_address: counter_address }; +// counter_address = factory.create_counter_at(argument_value); +// let counter_2 = ISimpleCounterDispatcher { contract_address: counter_address }; - counter_address = factory.create_counter_at(argument_value); - let counter_2 = ISimpleCounterDispatcher { contract_address: counter_address }; +// assert_eq!(counter_1.get_current_count(), init_value); +// assert_eq!(counter_2.get_current_count(), argument_value); +// } - assert_eq!(counter_1.get_current_count(), init_value); - assert_eq!(counter_2.get_current_count(), argument_value); -} From a444ead9e95a32fb3094195517a9fe4c87b85946 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 12:36:35 +0200 Subject: [PATCH 005/116] fix Campaign interfaced funcs + implement donate --- .../advanced_factory/src/campaign.cairo | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 40135d47..9ad74313 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -1,18 +1,20 @@ +use starknet::ClassHash; + #[starknet::interface] pub trait ICampaign { - fn get_current_count(self: @TContractState) -> u128; - fn increment(ref self: TContractState); - fn decrement(ref self: TContractState); + fn donate(ref self: TContractState, amount: u256); + fn withdraw(ref self: TContractState); + fn upgrade(ref self: TContractState, impl_hash: ClassHash); } - #[starknet::contract] pub mod Campaign { use components::ownable::ownable_component::OwnableInternalTrait; use core::num::traits::zero::Zero; + use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ - ClassHash, ContractAddress, get_block_timestamp, syscalls::replace_class_syscall, - get_caller_address + ClassHash, ContractAddress, get_block_timestamp, contract_address_const, + syscalls::replace_class_syscall, get_caller_address, get_contract_address }; use components::ownable::ownable_component; @@ -26,11 +28,12 @@ pub mod Campaign { struct Storage { #[substorage(v0)] ownable: ownable_component::Storage, - donations: LegacyMap, + donations: LegacyMap, end_time: u64, + eth_token: IERC20Dispatcher, factory: ContractAddress, - target: u128, - total_donations: u128, + target: u256, + total_donations: u256, } #[event] @@ -47,12 +50,12 @@ pub mod Campaign { pub struct Donated { #[key] pub donor: ContractAddress, - pub amount: u128, + pub amount: u256, } #[derive(Drop, starknet::Event)] pub struct Withdrawn { - pub amount: u128, + pub amount: u256, } #[derive(Drop, starknet::Event)] @@ -63,13 +66,14 @@ pub mod Campaign { pub mod Errors { pub const NOT_FACTORY: felt252 = 'Not factory'; pub const INACTIVE: felt252 = 'Campaign no longer active'; + pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; } #[constructor] fn constructor( ref self: ContractState, creator: ContractAddress, - target: u128, + target: u256, duration: u64, factory: ContractAddress ) { @@ -78,15 +82,36 @@ pub mod Campaign { assert(target > 0, 'target == 0'); assert(duration > 0, 'duration == 0'); + let eth_address = contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >(); + self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); + self.target.write(target); self.end_time.write(get_block_timestamp() + duration); self.factory.write(factory); self.ownable._init(creator); } - // #[abi(embed_v0)] - // impl Campaign of super::ICampaign { - // } + #[abi(embed_v0)] + impl Campaign of super::ICampaign { + fn donate(ref self: ContractState, amount: u256) { + assert(amount > 0, Errors::ZERO_DONATION); + + let donor = get_caller_address(); + let this = get_contract_address(); + self.eth_token.read().transfer_from(donor, this, amount); + + self.donations.write(donor, self.donations.read(donor) + amount); + self.total_donations.write(self.total_donations.read() + amount); + + self.emit(Event::Donated(Donated { donor, amount })); + } + + fn withdraw(ref self: ContractState) {} + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) {} + } #[generate_trait] impl CampaignInternal of CampaignInternalTrait { From a29d0772c7bc8b6e9f61d739407c7937532dd156 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 12:50:19 +0200 Subject: [PATCH 006/116] add _assert_is_ended + update error messages --- .../applications/advanced_factory/src/campaign.cairo | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 9ad74313..06092aca 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -64,8 +64,9 @@ pub mod Campaign { } pub mod Errors { - pub const NOT_FACTORY: felt252 = 'Not factory'; - pub const INACTIVE: felt252 = 'Campaign no longer active'; + pub const NOT_FACTORY: felt252 = 'Caller not factory'; + pub const INACTIVE: felt252 = 'Campaign already ended'; + pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; } @@ -96,6 +97,7 @@ pub mod Campaign { #[abi(embed_v0)] impl Campaign of super::ICampaign { fn donate(ref self: ContractState, amount: u256) { + self._assert_only_active(); assert(amount > 0, Errors::ZERO_DONATION); let donor = get_caller_address(); @@ -123,5 +125,9 @@ pub mod Campaign { fn _assert_only_active(self: @ContractState) { assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); } + + fn _assert_is_ended(self: @ContractState) { + assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); + } } } From d9facfe2ce6d1b2bd2b10e225b9ce12a20b76454 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 12:51:26 +0200 Subject: [PATCH 007/116] _assert_active->_assert_campaign_active --- listings/applications/advanced_factory/src/campaign.cairo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 06092aca..19fe5921 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -97,7 +97,7 @@ pub mod Campaign { #[abi(embed_v0)] impl Campaign of super::ICampaign { fn donate(ref self: ContractState, amount: u256) { - self._assert_only_active(); + self._assert_campaign_active(); assert(amount > 0, Errors::ZERO_DONATION); let donor = get_caller_address(); @@ -122,7 +122,7 @@ pub mod Campaign { assert(caller == self.factory.read(), Errors::NOT_FACTORY); } - fn _assert_only_active(self: @ContractState) { + fn _assert_campaign_active(self: @ContractState) { assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); } From 9436c7721df8a647186364550e638f874638088d Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 12:51:46 +0200 Subject: [PATCH 008/116] _assert_is_ended->_assert_campaign_ended --- listings/applications/advanced_factory/src/campaign.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 19fe5921..c76b60c8 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -126,7 +126,7 @@ pub mod Campaign { assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); } - fn _assert_is_ended(self: @ContractState) { + fn _assert_campaign_ended(self: @ContractState) { assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); } } From e3ff8de2adb0ce8df6b1b3dc9813236e3d05f185 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:02:32 +0200 Subject: [PATCH 009/116] implement withdraw --- .../advanced_factory/src/campaign.cairo | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index c76b60c8..4911d243 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -68,6 +68,8 @@ pub mod Campaign { pub const INACTIVE: felt252 = 'Campaign already ended'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; + pub const ZERO_FUNDS: felt252 = 'No funds to withdraw'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; } #[constructor] @@ -110,7 +112,24 @@ pub mod Campaign { self.emit(Event::Donated(Donated { donor, amount })); } - fn withdraw(ref self: ContractState) {} + fn withdraw(ref self: ContractState) { + self.ownable._assert_only_owner(); + self._assert_campaign_ended(); + + let this = get_contract_address(); + let eth_token = self.eth_token.read(); + + let amount = eth_token.balance_of(this); + assert(amount > 0, Errors::ZERO_FUNDS); + + // no need to set total_donations to 0, as the campaign has ended + // and the field can be left as a testament to how much was raised + + let success = eth_token.transfer(get_caller_address(), amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Withdrawn(Withdrawn { amount })); + } fn upgrade(ref self: ContractState, impl_hash: ClassHash) {} } From 2ea6f8f755ea3f29d203e6eb5334560b2ffb51c1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:03:52 +0200 Subject: [PATCH 010/116] add missing assert success in donate --- listings/applications/advanced_factory/src/campaign.cairo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 4911d243..f4b48531 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -104,7 +104,8 @@ pub mod Campaign { let donor = get_caller_address(); let this = get_contract_address(); - self.eth_token.read().transfer_from(donor, this, amount); + let success = self.eth_token.read().transfer_from(donor, this, amount); + assert(success, Errors::TRANSFER_FAILED); self.donations.write(donor, self.donations.read(donor) + amount); self.total_donations.write(self.total_donations.read() + amount); From abd561a08b7f92f7a454d0275bbf5a03858a8946 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:08:49 +0200 Subject: [PATCH 011/116] add title & description --- .../applications/advanced_factory/src/campaign.cairo | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index f4b48531..810620b6 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -9,6 +9,7 @@ pub trait ICampaign { #[starknet::contract] pub mod Campaign { + use core::byte_array::ByteArrayTrait; use components::ownable::ownable_component::OwnableInternalTrait; use core::num::traits::zero::Zero; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -33,6 +34,8 @@ pub mod Campaign { eth_token: IERC20Dispatcher, factory: ContractAddress, target: u256, + title: ByteArray, + description: ByteArray, total_donations: u256, } @@ -70,12 +73,15 @@ pub mod Campaign { pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; pub const ZERO_FUNDS: felt252 = 'No funds to withdraw'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + pub const TITLE_EMPTY: felt252 = 'Title empty'; } #[constructor] fn constructor( ref self: ContractState, creator: ContractAddress, + title: ByteArray, + description: ByteArray, target: u256, duration: u64, factory: ContractAddress @@ -84,12 +90,15 @@ pub mod Campaign { assert(creator.is_non_zero(), 'creator address zero'); assert(target > 0, 'target == 0'); assert(duration > 0, 'duration == 0'); + assert(title.len() > 0, Errors::TITLE_EMPTY); let eth_address = contract_address_const::< 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 >(); self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); + self.title.write(title); + self.description.write(description); self.target.write(target); self.end_time.write(get_block_timestamp() + duration); self.factory.write(factory); From 8c255fb148b44d53369386a025011e6db1d3ac4a Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:14:02 +0200 Subject: [PATCH 012/116] update comment --- listings/applications/advanced_factory/src/campaign.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 810620b6..01c42b3d 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -133,7 +133,7 @@ pub mod Campaign { assert(amount > 0, Errors::ZERO_FUNDS); // no need to set total_donations to 0, as the campaign has ended - // and the field can be left as a testament to how much was raised + // and the field can be used as a testament to how much was raised let success = eth_token.transfer(get_caller_address(), amount); assert(success, Errors::TRANSFER_FAILED); From 31b84f363f0eb1866d63cdec8cdeb7185747f1e7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:18:34 +0200 Subject: [PATCH 013/116] implement upgrade --- .../advanced_factory/src/campaign.cairo | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 01c42b3d..d671ef94 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -9,13 +9,14 @@ pub trait ICampaign { #[starknet::contract] pub mod Campaign { + use core::starknet::SyscallResultTrait; use core::byte_array::ByteArrayTrait; use components::ownable::ownable_component::OwnableInternalTrait; use core::num::traits::zero::Zero; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ - ClassHash, ContractAddress, get_block_timestamp, contract_address_const, - syscalls::replace_class_syscall, get_caller_address, get_contract_address + ClassHash, ContractAddress, get_block_timestamp, contract_address_const, get_caller_address, + get_contract_address }; use components::ownable::ownable_component; @@ -74,6 +75,7 @@ pub mod Campaign { pub const ZERO_FUNDS: felt252 = 'No funds to withdraw'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const TITLE_EMPTY: felt252 = 'Title empty'; + pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; } #[constructor] @@ -141,16 +143,18 @@ pub mod Campaign { self.emit(Event::Withdrawn(Withdrawn { amount })); } - fn upgrade(ref self: ContractState, impl_hash: ClassHash) {} + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); + assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + + starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); + + self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); + } } #[generate_trait] impl CampaignInternal of CampaignInternalTrait { - fn _assert_only_factory(self: @ContractState) { - let caller = get_caller_address(); - assert(caller == self.factory.read(), Errors::NOT_FACTORY); - } - fn _assert_campaign_active(self: @ContractState) { assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); } From cae84c6a9a718c18de00080d8381c39abc6493f5 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:20:44 +0200 Subject: [PATCH 014/116] clean up internal funcs and imports --- .../advanced_factory/src/campaign.cairo | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index d671ef94..3aebeb6b 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -9,14 +9,12 @@ pub trait ICampaign { #[starknet::contract] pub mod Campaign { - use core::starknet::SyscallResultTrait; - use core::byte_array::ByteArrayTrait; use components::ownable::ownable_component::OwnableInternalTrait; use core::num::traits::zero::Zero; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ - ClassHash, ContractAddress, get_block_timestamp, contract_address_const, get_caller_address, - get_contract_address + ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, contract_address_const, + get_caller_address, get_contract_address }; use components::ownable::ownable_component; @@ -110,7 +108,7 @@ pub mod Campaign { #[abi(embed_v0)] impl Campaign of super::ICampaign { fn donate(ref self: ContractState, amount: u256) { - self._assert_campaign_active(); + assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); assert(amount > 0, Errors::ZERO_DONATION); let donor = get_caller_address(); @@ -126,7 +124,7 @@ pub mod Campaign { fn withdraw(ref self: ContractState) { self.ownable._assert_only_owner(); - self._assert_campaign_ended(); + assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); let this = get_contract_address(); let eth_token = self.eth_token.read(); @@ -152,15 +150,4 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } } - - #[generate_trait] - impl CampaignInternal of CampaignInternalTrait { - fn _assert_campaign_active(self: @ContractState) { - assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); - } - - fn _assert_campaign_ended(self: @ContractState) { - assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); - } - } } From c45855b7c9b76fb68d2012c8cc342a627b84a1b6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:24:22 +0200 Subject: [PATCH 015/116] move hardcoded errors in Errors mod --- .../applications/advanced_factory/src/campaign.cairo | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 3aebeb6b..1daa6f2e 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -70,10 +70,14 @@ pub mod Campaign { pub const INACTIVE: felt252 = 'Campaign already ended'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; 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 withdraw'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const TITLE_EMPTY: felt252 = 'Title empty'; pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; + pub const FACTORY_ZERO: felt252 = 'Factory address cannot be zero'; + pub const CREATOR_ZERO: felt252 = 'Creator address cannot be zero'; } #[constructor] @@ -86,11 +90,11 @@ pub mod Campaign { duration: u64, factory: ContractAddress ) { - assert(factory.is_non_zero(), 'factory address zero'); - assert(creator.is_non_zero(), 'creator address zero'); - assert(target > 0, 'target == 0'); - assert(duration > 0, 'duration == 0'); + assert(factory.is_non_zero(), Errors::FACTORY_ZERO); + assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); + assert(target > 0, Errors::ZERO_TARGET); + assert(duration > 0, Errors::ZERO_DURATION); let eth_address = contract_address_const::< 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 From 2008ed3358d664cd58291e21902aadf8c28439ec Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 13:27:56 +0200 Subject: [PATCH 016/116] donate -> contribute + event rename --- .../advanced_factory/src/campaign.cairo | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 1daa6f2e..14754a91 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -2,7 +2,7 @@ use starknet::ClassHash; #[starknet::interface] pub trait ICampaign { - fn donate(ref self: TContractState, amount: u256); + fn contribute(ref self: TContractState, amount: u256); fn withdraw(ref self: TContractState); fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -28,14 +28,14 @@ pub mod Campaign { struct Storage { #[substorage(v0)] ownable: ownable_component::Storage, - donations: LegacyMap, + contributions: LegacyMap, end_time: u64, eth_token: IERC20Dispatcher, factory: ContractAddress, target: u256, title: ByteArray, description: ByteArray, - total_donations: u256, + total_contributions: u256, } #[event] @@ -43,15 +43,15 @@ pub mod Campaign { pub enum Event { #[flat] OwnableEvent: ownable_component::Event, - Donated: Donated, + ContributionMade: ContributionMade, Withdrawn: Withdrawn, Upgraded: Upgraded, } #[derive(Drop, starknet::Event)] - pub struct Donated { + pub struct ContributionMade { #[key] - pub donor: ContractAddress, + pub contributor: ContractAddress, pub amount: u256, } @@ -111,19 +111,19 @@ pub mod Campaign { #[abi(embed_v0)] impl Campaign of super::ICampaign { - fn donate(ref self: ContractState, amount: u256) { + fn contribute(ref self: ContractState, amount: u256) { assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); assert(amount > 0, Errors::ZERO_DONATION); - let donor = get_caller_address(); + let contributor = get_caller_address(); let this = get_contract_address(); - let success = self.eth_token.read().transfer_from(donor, this, amount); + let success = self.eth_token.read().transfer_from(contributor, this, amount); assert(success, Errors::TRANSFER_FAILED); - self.donations.write(donor, self.donations.read(donor) + amount); - self.total_donations.write(self.total_donations.read() + amount); + self.contributions.write(contributor, self.contributions.read(contributor) + amount); + self.total_contributions.write(self.total_contributions.read() + amount); - self.emit(Event::Donated(Donated { donor, amount })); + self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); } fn withdraw(ref self: ContractState) { @@ -136,7 +136,7 @@ pub mod Campaign { let amount = eth_token.balance_of(this); assert(amount > 0, Errors::ZERO_FUNDS); - // no need to set total_donations to 0, as the campaign has ended + // 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 = eth_token.transfer(get_caller_address(), amount); From 662a4f3ce7ef2d460bf855f779c6205325822fce Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 6 Jun 2024 14:01:59 +0200 Subject: [PATCH 017/116] withdraw -> claim --- .../applications/advanced_factory/src/campaign.cairo | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 14754a91..09e03b25 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -3,7 +3,7 @@ use starknet::ClassHash; #[starknet::interface] pub trait ICampaign { fn contribute(ref self: TContractState, amount: u256); - fn withdraw(ref self: TContractState); + fn claim(ref self: TContractState); fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -44,7 +44,7 @@ pub mod Campaign { #[flat] OwnableEvent: ownable_component::Event, ContributionMade: ContributionMade, - Withdrawn: Withdrawn, + Claimed: Claimed, Upgraded: Upgraded, } @@ -56,7 +56,7 @@ pub mod Campaign { } #[derive(Drop, starknet::Event)] - pub struct Withdrawn { + pub struct Claimed { pub amount: u256, } @@ -72,7 +72,7 @@ pub mod Campaign { 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 withdraw'; + 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 CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; @@ -126,7 +126,7 @@ pub mod Campaign { self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); } - fn withdraw(ref self: ContractState) { + fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); @@ -142,7 +142,7 @@ pub mod Campaign { let success = eth_token.transfer(get_caller_address(), amount); assert(success, Errors::TRANSFER_FAILED); - self.emit(Event::Withdrawn(Withdrawn { amount })); + self.emit(Event::Claimed(Claimed { amount })); } fn upgrade(ref self: ContractState, impl_hash: ClassHash) { From 7e137073295bf59a01211b982583778bc0ed77be Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 08:18:26 +0200 Subject: [PATCH 018/116] add store impl for contract addr. array --- .../src/store_contr_addr_array.cairo | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 listings/applications/advanced_factory/src/store_contr_addr_array.cairo diff --git a/listings/applications/advanced_factory/src/store_contr_addr_array.cairo b/listings/applications/advanced_factory/src/store_contr_addr_array.cairo new file mode 100644 index 00000000..91de4644 --- /dev/null +++ b/listings/applications/advanced_factory/src/store_contr_addr_array.cairo @@ -0,0 +1,66 @@ +use core::result::ResultTrait; +use core::array::ArrayTrait; +use starknet::{ContractAddress, Store, SyscallResult}; +use starknet::storage_access::{StorageBaseAddress, storage_address_from_base_and_offset}; +use starknet::syscalls::{storage_read_syscall, storage_write_syscall}; + +impl StoreContractAddressArray of Store> { + fn read( + address_domain: u32, base: StorageBaseAddress + ) -> SyscallResult> { + StoreContractAddressArray::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreContractAddressArray::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = array![]; + + let len: u8 = Store::::read_at_offset(address_domain, base, offset) + .expect('Storage Span too large'); + offset += 1; + + let exit = len + offset; + loop { + if offset >= exit { + break; + } + + let value = Store::::read_at_offset(address_domain, base, offset) + .unwrap(); + arr.append(value); + offset += Store::::size(); + }; + + Result::Ok(arr) + } + + fn write_at_offset( + address_domain: u32, + base: StorageBaseAddress, + mut offset: u8, + mut value: Array + ) -> SyscallResult<()> { + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len).unwrap(); + offset += 1; + + while let Option::Some(element) = value.pop_front() { + Store::::write_at_offset(address_domain, base, offset, element) + .unwrap(); + offset += Store::::size(); + }; + + Result::Ok(()) + } + + fn size() -> u8 { + 255 * Store::::size() + } +} From a0932c82aa7f9de18ef0417cf75ac4b4083969b6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 08:50:33 +0200 Subject: [PATCH 019/116] remove store impl --- .../src/store_contr_addr_array.cairo | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 listings/applications/advanced_factory/src/store_contr_addr_array.cairo diff --git a/listings/applications/advanced_factory/src/store_contr_addr_array.cairo b/listings/applications/advanced_factory/src/store_contr_addr_array.cairo deleted file mode 100644 index 91de4644..00000000 --- a/listings/applications/advanced_factory/src/store_contr_addr_array.cairo +++ /dev/null @@ -1,66 +0,0 @@ -use core::result::ResultTrait; -use core::array::ArrayTrait; -use starknet::{ContractAddress, Store, SyscallResult}; -use starknet::storage_access::{StorageBaseAddress, storage_address_from_base_and_offset}; -use starknet::syscalls::{storage_read_syscall, storage_write_syscall}; - -impl StoreContractAddressArray of Store> { - fn read( - address_domain: u32, base: StorageBaseAddress - ) -> SyscallResult> { - StoreContractAddressArray::read_at_offset(address_domain, base, 0) - } - - fn write( - address_domain: u32, base: StorageBaseAddress, value: Array - ) -> SyscallResult<()> { - StoreContractAddressArray::write_at_offset(address_domain, base, 0, value) - } - - fn read_at_offset( - address_domain: u32, base: StorageBaseAddress, mut offset: u8 - ) -> SyscallResult> { - let mut arr: Array = array![]; - - let len: u8 = Store::::read_at_offset(address_domain, base, offset) - .expect('Storage Span too large'); - offset += 1; - - let exit = len + offset; - loop { - if offset >= exit { - break; - } - - let value = Store::::read_at_offset(address_domain, base, offset) - .unwrap(); - arr.append(value); - offset += Store::::size(); - }; - - Result::Ok(arr) - } - - fn write_at_offset( - address_domain: u32, - base: StorageBaseAddress, - mut offset: u8, - mut value: Array - ) -> SyscallResult<()> { - let len: u8 = value.len().try_into().expect('Storage - Span too large'); - Store::::write_at_offset(address_domain, base, offset, len).unwrap(); - offset += 1; - - while let Option::Some(element) = value.pop_front() { - Store::::write_at_offset(address_domain, base, offset, element) - .unwrap(); - offset += Store::::size(); - }; - - Result::Ok(()) - } - - fn size() -> u8 { - 255 * Store::::size() - } -} From 52aa53405a628e2f2c0f0dabc575e6ee36c213b6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 08:50:45 +0200 Subject: [PATCH 020/116] add dynamic array impl --- .../src/contract_address_array.cairo | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 listings/applications/advanced_factory/src/contract_address_array.cairo diff --git a/listings/applications/advanced_factory/src/contract_address_array.cairo b/listings/applications/advanced_factory/src/contract_address_array.cairo new file mode 100644 index 00000000..a92c5701 --- /dev/null +++ b/listings/applications/advanced_factory/src/contract_address_array.cairo @@ -0,0 +1,33 @@ +use starknet::ContractAddress; + +pub struct ContractAddressArray { + data: Felt252Dict>, + len: usize +} + +impl DestructContractAddressArray of Destruct { + fn destruct(self: ContractAddressArray) nopanic { + self.data.squash(); + } +} + +#[generate_trait] +impl ContractAddressArrayImpl of ContractAddressArrayTrait { + fn new() -> ContractAddressArray { + ContractAddressArray { data: Default::default(), len: 0 } + } + + fn push(ref self: ContractAddressArray, value: ContractAddress) { + self.data.insert(self.len.into(), NullableTrait::new(value)); + self.len += 1; + } + + fn get(ref self: ContractAddressArray, index: usize) -> ContractAddress { + assert!(index < self.len(), "Index out of bounds"); + self.data.get(index.into()).deref() + } + + fn len(self: @ContractAddressArray) -> usize { + *self.len + } +} From 843bb2ed75a01c2a58bf7a8d6f2f07c4421c01ee Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 09:02:35 +0200 Subject: [PATCH 021/116] remove dyn. array --- .../src/store_address_array.cairo | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 listings/applications/advanced_factory/src/store_address_array.cairo diff --git a/listings/applications/advanced_factory/src/store_address_array.cairo b/listings/applications/advanced_factory/src/store_address_array.cairo new file mode 100644 index 00000000..91de4644 --- /dev/null +++ b/listings/applications/advanced_factory/src/store_address_array.cairo @@ -0,0 +1,66 @@ +use core::result::ResultTrait; +use core::array::ArrayTrait; +use starknet::{ContractAddress, Store, SyscallResult}; +use starknet::storage_access::{StorageBaseAddress, storage_address_from_base_and_offset}; +use starknet::syscalls::{storage_read_syscall, storage_write_syscall}; + +impl StoreContractAddressArray of Store> { + fn read( + address_domain: u32, base: StorageBaseAddress + ) -> SyscallResult> { + StoreContractAddressArray::read_at_offset(address_domain, base, 0) + } + + fn write( + address_domain: u32, base: StorageBaseAddress, value: Array + ) -> SyscallResult<()> { + StoreContractAddressArray::write_at_offset(address_domain, base, 0, value) + } + + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, mut offset: u8 + ) -> SyscallResult> { + let mut arr: Array = array![]; + + let len: u8 = Store::::read_at_offset(address_domain, base, offset) + .expect('Storage Span too large'); + offset += 1; + + let exit = len + offset; + loop { + if offset >= exit { + break; + } + + let value = Store::::read_at_offset(address_domain, base, offset) + .unwrap(); + arr.append(value); + offset += Store::::size(); + }; + + Result::Ok(arr) + } + + fn write_at_offset( + address_domain: u32, + base: StorageBaseAddress, + mut offset: u8, + mut value: Array + ) -> SyscallResult<()> { + let len: u8 = value.len().try_into().expect('Storage - Span too large'); + Store::::write_at_offset(address_domain, base, offset, len).unwrap(); + offset += 1; + + while let Option::Some(element) = value.pop_front() { + Store::::write_at_offset(address_domain, base, offset, element) + .unwrap(); + offset += Store::::size(); + }; + + Result::Ok(()) + } + + fn size() -> u8 { + 255 * Store::::size() + } +} From 5c6b5c1fd3813f46e3fcdf71b919ee51be7b260c Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 10:07:20 +0200 Subject: [PATCH 022/116] remove descr + convert title to felt + convert target to u128 --- Scarb.lock | 53 +++++++ .../applications/advanced_factory/Scarb.toml | 1 + .../advanced_factory/src/campaign.cairo | 15 +- .../advanced_factory/src/factory.cairo | 112 +++++++++++++++ .../advanced_factory/src/lib.cairo | 132 +----------------- 5 files changed, 175 insertions(+), 138 deletions(-) create mode 100644 listings/applications/advanced_factory/src/factory.cairo diff --git a/Scarb.lock b/Scarb.lock index bd475f38..61b413d4 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -5,11 +5,64 @@ version = 1 name = "advanced_factory" version = "0.1.0" dependencies = [ + "alexandria_math", "components", "openzeppelin", "snforge_std", ] +[[package]] +name = "alexandria_bytes" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_data_structures", + "alexandria_math", +] + +[[package]] +name = "alexandria_data_structures" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_encoding", +] + +[[package]] +name = "alexandria_encoding" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_bytes", + "alexandria_math", + "alexandria_numeric", +] + +[[package]] +name = "alexandria_math" +version = "0.2.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_data_structures", +] + +[[package]] +name = "alexandria_numeric" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_math", + "alexandria_searching", +] + +[[package]] +name = "alexandria_searching" +version = "0.1.0" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +dependencies = [ + "alexandria_data_structures", +] + [[package]] name = "alexandria_storage" version = "0.3.0" diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml index 2f188542..d1f18234 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -11,6 +11,7 @@ snforge_std.workspace = true openzeppelin.workspace = true # StarknetByExample Components components.workspace = true +alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git" } [scripts] test.workspace = true diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 09e03b25..8b6cc504 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -33,8 +33,7 @@ pub mod Campaign { eth_token: IERC20Dispatcher, factory: ContractAddress, target: u256, - title: ByteArray, - description: ByteArray, + title: felt252, total_contributions: u256, } @@ -84,15 +83,14 @@ pub mod Campaign { fn constructor( ref self: ContractState, creator: ContractAddress, - title: ByteArray, - description: ByteArray, - target: u256, + title: felt252, + target: u128, duration: u64, factory: ContractAddress ) { assert(factory.is_non_zero(), Errors::FACTORY_ZERO); assert(creator.is_non_zero(), Errors::CREATOR_ZERO); - assert(title.len() > 0, Errors::TITLE_EMPTY); + assert(title != 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); assert(duration > 0, Errors::ZERO_DURATION); @@ -102,8 +100,7 @@ pub mod Campaign { self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); self.title.write(title); - self.description.write(description); - self.target.write(target); + self.target.write(target.into()); self.end_time.write(get_block_timestamp() + duration); self.factory.write(factory); self.ownable._init(creator); @@ -117,7 +114,7 @@ pub mod Campaign { let contributor = get_caller_address(); let this = get_contract_address(); - let success = self.eth_token.read().transfer_from(contributor, this, amount); + let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); assert(success, Errors::TRANSFER_FAILED); self.contributions.write(contributor, self.contributions.read(contributor) + amount); diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo new file mode 100644 index 00000000..eb061af4 --- /dev/null +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -0,0 +1,112 @@ +// ANCHOR: contract +pub use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +pub trait ICrowdfundingFactory { + fn create_campaign( + ref self: TContractState, title: felt252, target: u128, duration: u64 + ) -> ContractAddress; + fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash); +} + +#[starknet::contract] +pub mod CrowdfundingFactory { + use core::num::traits::zero::Zero; + use starknet::{ + ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall, + get_caller_address, get_contract_address + }; + use alexandria_storage::list::{List, ListTrait}; + use components::ownable::ownable_component; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = ownable_component::Ownable; + impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, + /// Store all of the created campaign instances' addresses + campaigns: List, + /// Store the class hash of the contract to deploy + campaign_class_hash: ClassHash, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: ownable_component::Event, + ClassHashUpdated: ClassHashUpdated, + CampaignCreated: CampaignCreated, + } + + #[derive(Drop, starknet::Event)] + struct ClassHashUpdated { + new_class_hash: ClassHash, + } + + #[derive(Drop, starknet::Event)] + struct CampaignCreated { + caller: ContractAddress, + deployed_address: ContractAddress + } + + pub mod Errors { + pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; + } + + #[constructor] + fn constructor(ref self: ContractState, class_hash: ClassHash) { + assert(class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + self.campaign_class_hash.write(class_hash); + self.ownable._init(get_caller_address()); + } + + #[abi(embed_v0)] + impl CrowdfundingFactory of super::ICrowdfundingFactory { + // ANCHOR: deploy + fn create_campaign( + // TODO: how to cast target: u256 into a felt??? + ref self: ContractState, title: felt252, target: u128, duration: u64 + ) -> ContractAddress { + let caller = get_caller_address(); + let this = get_contract_address(); + + // Create contructor arguments + let mut constructor_calldata: Array:: = array![]; + constructor_calldata.append(caller.into()); + constructor_calldata.append(title); + constructor_calldata.append(target.into()); + constructor_calldata.append(duration.into()); + constructor_calldata.append(this.into()); + + // Contract deployment + let (deployed_address, _) = deploy_syscall( + self.campaign_class_hash.read(), 0, constructor_calldata.span(), false + ) + .unwrap_syscall(); + + // track new campaign instance + let mut campaigns = self.campaigns.read(); + campaigns.append(deployed_address).unwrap(); + + self.emit(Event::CampaignCreated(CampaignCreated { caller, deployed_address })); + + deployed_address + } + // ANCHOR_END: deploy + + fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) { + self.ownable._assert_only_owner(); + self.campaign_class_hash.write(new_class_hash); + self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); + } + } +} +// ANCHOR_END: contract + + diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index 356d1574..f96a057d 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -1,132 +1,6 @@ -// ANCHOR: contract -pub use starknet::{ContractAddress, ClassHash}; - -#[starknet::interface] -pub trait ICounterFactory { - /// Create a new counter contract from stored arguments - fn create_counter(ref self: TContractState) -> ContractAddress; - - /// Create a new counter contract from the given arguments - fn create_counter_at(ref self: TContractState, init_value: u128) -> ContractAddress; - - /// Update the argument - fn update_init_value(ref self: TContractState, new_init_value: u128); - - /// Update the class hash of the Counter contract to deploy when creating a new counter - fn update_counter_class_hash(ref self: TContractState, new_class_hash: ClassHash); -} - -#[starknet::contract] -pub mod CounterFactory { - use components::ownable::ownable_component::OwnableInternalTrait; - use core::starknet::event::EventEmitter; - use starknet::{ - ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall, get_caller_address - }; - use components::ownable::ownable_component; - - component!(path: ownable_component, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - impl OwnableImpl = ownable_component::Ownable; - impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - ownable: ownable_component::Storage, - /// Store the constructor arguments of the contract to deploy - init_value: u128, - /// Store the class hash of the contract to deploy - counter_class_hash: ClassHash, - } - - #[event] - #[derive(Drop, starknet::Event)] - pub enum Event { - #[flat] - OwnableEvent: ownable_component::Event, - ClassHashUpdated: ClassHashUpdated, - CounterCreated: CounterCreated, - InitValueUpdated: InitValueUpdated, - } - - #[derive(Drop, starknet::Event)] - struct ClassHashUpdated { - previous_class_hash: ClassHash, - new_class_hash: ClassHash, - } - - #[derive(Drop, starknet::Event)] - struct CounterCreated { - caller: ContractAddress, - deployed_address: ContractAddress - } - - #[derive(Drop, starknet::Event)] - struct InitValueUpdated { - previous_init_value: u128, - new_init_value: u128, - } - - #[constructor] - fn constructor(ref self: ContractState, init_value: u128, class_hash: ClassHash) { - self.init_value.write(init_value); - self.counter_class_hash.write(class_hash); - self.ownable._init(get_caller_address()); - } - - #[abi(embed_v0)] - impl Factory of super::ICounterFactory { - // ANCHOR: deploy - fn create_counter_at(ref self: ContractState, init_value: u128) -> ContractAddress { - // Contructor arguments - let mut constructor_calldata: Array:: = array![init_value.into()]; - - // Contract deployment - let (deployed_address, _) = deploy_syscall( - self.counter_class_hash.read(), 0, constructor_calldata.span(), false - ) - .unwrap_syscall(); - - let caller = get_caller_address(); - self.emit(Event::CounterCreated(CounterCreated { caller, deployed_address })); - - deployed_address - } - // ANCHOR_END: deploy - - fn create_counter(ref self: ContractState) -> ContractAddress { - self.create_counter_at(self.init_value.read()) - } - - fn update_init_value(ref self: ContractState, new_init_value: u128) { - self.ownable._assert_only_owner(); - let previous_init_value = self.init_value.read(); - self.init_value.write(new_init_value); - self - .emit( - Event::InitValueUpdated( - InitValueUpdated { previous_init_value, new_init_value } - ) - ); - } - - fn update_counter_class_hash(ref self: ContractState, new_class_hash: ClassHash) { - self.ownable._assert_only_owner(); - let previous_class_hash = self.counter_class_hash.read(); - self.counter_class_hash.write(new_class_hash); - self - .emit( - Event::ClassHashUpdated( - ClassHashUpdated { previous_class_hash, new_class_hash } - ) - ); - } - } -} -// ANCHOR_END: contract +mod factory; +mod campaign; +mod contract_address_array; #[cfg(test)] mod tests; -mod campaign; From dabb2c1ee124f57b864d08b5837f8b934cc4c689 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 10:21:30 +0200 Subject: [PATCH 023/116] implement updating class hashes --- .../advanced_factory/src/factory.cairo | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index eb061af4..353465be 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -17,6 +17,7 @@ pub mod CrowdfundingFactory { get_caller_address, get_contract_address }; use alexandria_storage::list::{List, ListTrait}; + use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; use components::ownable::ownable_component; component!(path: ownable_component, storage: ownable, event: OwnableEvent); @@ -30,7 +31,7 @@ pub mod CrowdfundingFactory { #[substorage(v0)] ownable: ownable_component::Storage, /// Store all of the created campaign instances' addresses - campaigns: List, + campaigns: List, /// Store the class hash of the contract to deploy campaign_class_hash: ClassHash, } @@ -52,7 +53,7 @@ pub mod CrowdfundingFactory { #[derive(Drop, starknet::Event)] struct CampaignCreated { caller: ContractAddress, - deployed_address: ContractAddress + contract_address: ContractAddress } pub mod Errors { @@ -85,24 +86,35 @@ pub mod CrowdfundingFactory { constructor_calldata.append(this.into()); // Contract deployment - let (deployed_address, _) = deploy_syscall( + let (contract_address, _) = deploy_syscall( self.campaign_class_hash.read(), 0, constructor_calldata.span(), false ) .unwrap_syscall(); // track new campaign instance let mut campaigns = self.campaigns.read(); - campaigns.append(deployed_address).unwrap(); + campaigns.append(ICampaignDispatcher { contract_address }).unwrap(); - self.emit(Event::CampaignCreated(CampaignCreated { caller, deployed_address })); + self.emit(Event::CampaignCreated(CampaignCreated { caller, contract_address })); - deployed_address + contract_address } // ANCHOR_END: deploy fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) { self.ownable._assert_only_owner(); + + // update own campaign class hash value self.campaign_class_hash.write(new_class_hash); + + // upgrade each campaign with the new class hash + let campaigns = self.campaigns.read(); + let mut i = 0; + while let Option::Some(campaign) = campaigns.get(i).unwrap_syscall() { + campaign.upgrade(new_class_hash); + i += 1; + }; + self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); } } From 7f7c7fceea47a47d45eadb19548ebbb48df53394 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 11:38:30 +0200 Subject: [PATCH 024/116] Make title ByteArray again + target into u256 + update ctor arg serialization --- .../advanced_factory/src/campaign.cairo | 10 +++++----- .../applications/advanced_factory/src/factory.cairo | 13 +++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 8b6cc504..c7072313 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -33,7 +33,7 @@ pub mod Campaign { eth_token: IERC20Dispatcher, factory: ContractAddress, target: u256, - title: felt252, + title: ByteArray, total_contributions: u256, } @@ -83,14 +83,14 @@ pub mod Campaign { fn constructor( ref self: ContractState, creator: ContractAddress, - title: felt252, - target: u128, + title: ByteArray, + target: u256, duration: u64, factory: ContractAddress ) { assert(factory.is_non_zero(), Errors::FACTORY_ZERO); assert(creator.is_non_zero(), Errors::CREATOR_ZERO); - assert(title != 0, Errors::TITLE_EMPTY); + assert(title.len() > 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); assert(duration > 0, Errors::ZERO_DURATION); @@ -100,7 +100,7 @@ pub mod Campaign { self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); self.title.write(title); - self.target.write(target.into()); + self.target.write(target); self.end_time.write(get_block_timestamp() + duration); self.factory.write(factory); self.ownable._init(creator); diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 353465be..8f1e62fc 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -4,7 +4,7 @@ pub use starknet::{ContractAddress, ClassHash}; #[starknet::interface] pub trait ICrowdfundingFactory { fn create_campaign( - ref self: TContractState, title: felt252, target: u128, duration: u64 + ref self: TContractState, title: ByteArray, target: u256, duration: u64 ) -> ContractAddress; fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash); } @@ -67,23 +67,20 @@ pub mod CrowdfundingFactory { self.ownable._init(get_caller_address()); } + #[abi(embed_v0)] impl CrowdfundingFactory of super::ICrowdfundingFactory { // ANCHOR: deploy fn create_campaign( - // TODO: how to cast target: u256 into a felt??? - ref self: ContractState, title: felt252, target: u128, duration: u64 + ref self: ContractState, title: ByteArray, target: u256, duration: u64 ) -> ContractAddress { let caller = get_caller_address(); let this = get_contract_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - constructor_calldata.append(caller.into()); - constructor_calldata.append(title); - constructor_calldata.append(target.into()); - constructor_calldata.append(duration.into()); - constructor_calldata.append(this.into()); + (caller, title, target, duration).serialize(ref constructor_calldata); + this.serialize(ref constructor_calldata); // Contract deployment let (contract_address, _) = deploy_syscall( From a39ced99408bc441b890f3b8e4cd576afb75fc8a Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 7 Jun 2024 11:51:57 +0200 Subject: [PATCH 025/116] refactor serialization + add back description --- .../advanced_factory/src/campaign.cairo | 3 +++ .../advanced_factory/src/factory.cairo | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index c7072313..a34d3064 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -34,6 +34,7 @@ pub mod Campaign { factory: ContractAddress, target: u256, title: ByteArray, + description: ByteArray, total_contributions: u256, } @@ -84,6 +85,7 @@ pub mod Campaign { ref self: ContractState, creator: ContractAddress, title: ByteArray, + description: ByteArray, target: u256, duration: u64, factory: ContractAddress @@ -101,6 +103,7 @@ pub mod Campaign { self.title.write(title); self.target.write(target); + self.description.write(description); self.end_time.write(get_block_timestamp() + duration); self.factory.write(factory); self.ownable._init(creator); diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 8f1e62fc..d7d180d7 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -4,7 +4,11 @@ pub use starknet::{ContractAddress, ClassHash}; #[starknet::interface] pub trait ICrowdfundingFactory { fn create_campaign( - ref self: TContractState, title: ByteArray, target: u256, duration: u64 + ref self: TContractState, + title: ByteArray, + description: ByteArray, + target: u256, + duration: u64 ) -> ContractAddress; fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash); } @@ -72,15 +76,19 @@ pub mod CrowdfundingFactory { impl CrowdfundingFactory of super::ICrowdfundingFactory { // ANCHOR: deploy fn create_campaign( - ref self: ContractState, title: ByteArray, target: u256, duration: u64 + ref self: ContractState, + title: ByteArray, + description: ByteArray, + target: u256, + duration: u64 ) -> ContractAddress { let caller = get_caller_address(); let this = get_contract_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - (caller, title, target, duration).serialize(ref constructor_calldata); - this.serialize(ref constructor_calldata); + ((caller, title, description, target), duration, this) + .serialize(ref constructor_calldata); // Contract deployment let (contract_address, _) = deploy_syscall( From 0fcfd1721bf76d59b4a85ff63ec160a0bc249bce Mon Sep 17 00:00:00 2001 From: Nenad Date: Sat, 8 Jun 2024 12:16:38 +0200 Subject: [PATCH 026/116] remove unused contracts --- .../src/contract_address_array.cairo | 33 ---------- .../advanced_factory/src/lib.cairo | 1 - .../src/store_address_array.cairo | 66 ------------------- .../advanced_factory/src/tests.cairo | 32 ++++----- 4 files changed, 17 insertions(+), 115 deletions(-) delete mode 100644 listings/applications/advanced_factory/src/contract_address_array.cairo delete mode 100644 listings/applications/advanced_factory/src/store_address_array.cairo diff --git a/listings/applications/advanced_factory/src/contract_address_array.cairo b/listings/applications/advanced_factory/src/contract_address_array.cairo deleted file mode 100644 index a92c5701..00000000 --- a/listings/applications/advanced_factory/src/contract_address_array.cairo +++ /dev/null @@ -1,33 +0,0 @@ -use starknet::ContractAddress; - -pub struct ContractAddressArray { - data: Felt252Dict>, - len: usize -} - -impl DestructContractAddressArray of Destruct { - fn destruct(self: ContractAddressArray) nopanic { - self.data.squash(); - } -} - -#[generate_trait] -impl ContractAddressArrayImpl of ContractAddressArrayTrait { - fn new() -> ContractAddressArray { - ContractAddressArray { data: Default::default(), len: 0 } - } - - fn push(ref self: ContractAddressArray, value: ContractAddress) { - self.data.insert(self.len.into(), NullableTrait::new(value)); - self.len += 1; - } - - fn get(ref self: ContractAddressArray, index: usize) -> ContractAddress { - assert!(index < self.len(), "Index out of bounds"); - self.data.get(index.into()).deref() - } - - fn len(self: @ContractAddressArray) -> usize { - *self.len - } -} diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index f96a057d..6d4daea8 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -1,6 +1,5 @@ mod factory; mod campaign; -mod contract_address_array; #[cfg(test)] mod tests; diff --git a/listings/applications/advanced_factory/src/store_address_array.cairo b/listings/applications/advanced_factory/src/store_address_array.cairo deleted file mode 100644 index 91de4644..00000000 --- a/listings/applications/advanced_factory/src/store_address_array.cairo +++ /dev/null @@ -1,66 +0,0 @@ -use core::result::ResultTrait; -use core::array::ArrayTrait; -use starknet::{ContractAddress, Store, SyscallResult}; -use starknet::storage_access::{StorageBaseAddress, storage_address_from_base_and_offset}; -use starknet::syscalls::{storage_read_syscall, storage_write_syscall}; - -impl StoreContractAddressArray of Store> { - fn read( - address_domain: u32, base: StorageBaseAddress - ) -> SyscallResult> { - StoreContractAddressArray::read_at_offset(address_domain, base, 0) - } - - fn write( - address_domain: u32, base: StorageBaseAddress, value: Array - ) -> SyscallResult<()> { - StoreContractAddressArray::write_at_offset(address_domain, base, 0, value) - } - - fn read_at_offset( - address_domain: u32, base: StorageBaseAddress, mut offset: u8 - ) -> SyscallResult> { - let mut arr: Array = array![]; - - let len: u8 = Store::::read_at_offset(address_domain, base, offset) - .expect('Storage Span too large'); - offset += 1; - - let exit = len + offset; - loop { - if offset >= exit { - break; - } - - let value = Store::::read_at_offset(address_domain, base, offset) - .unwrap(); - arr.append(value); - offset += Store::::size(); - }; - - Result::Ok(arr) - } - - fn write_at_offset( - address_domain: u32, - base: StorageBaseAddress, - mut offset: u8, - mut value: Array - ) -> SyscallResult<()> { - let len: u8 = value.len().try_into().expect('Storage - Span too large'); - Store::::write_at_offset(address_domain, base, offset, len).unwrap(); - offset += 1; - - while let Option::Some(element) = value.pop_front() { - Store::::write_at_offset(address_domain, base, offset, element) - .unwrap(); - offset += Store::::size(); - }; - - Result::Ok(()) - } - - fn size() -> u8 { - 255 * Store::::size() - } -} diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index f569ae7d..4b29a896 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,22 +1,23 @@ -use core::result::ResultTrait; -use advanced_factory::{CounterFactory, ICounterFactoryDispatcher, ICounterFactoryDispatcherTrait}; -use starknet::{ContractAddress, ClassHash,}; -use snforge_std::{declare, ContractClass, ContractClassTrait}; +// use core::result::ResultTrait; +// use advanced_factory::{CounterFactory, ICounterFactoryDispatcher, +// ICounterFactoryDispatcherTrait}; +// use starknet::{ContractAddress, ClassHash,}; +// use snforge_std::{declare, ContractClass, ContractClassTrait}; -// Define a target contract to deploy -use advanced_factory::simple_counter::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait}; +// // Define a target contract to deploy +// use advanced_factory::simple_counter::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait}; -/// Deploy a counter factory contract -fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFactoryDispatcher { - let mut constructor_calldata: @Array:: = @array![ - init_value.into(), counter_class_hash.into() - ]; +// /// Deploy a counter factory contract +// fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFactoryDispatcher { +// let mut constructor_calldata: @Array:: = @array![ +// init_value.into(), counter_class_hash.into() +// ]; - let contract = declare("CounterFactory").unwrap(); - let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); +// let contract = declare("CounterFactory").unwrap(); +// let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); - ICounterFactoryDispatcher { contract_address } -} +// ICounterFactoryDispatcher { contract_address } +// } // #[test] // fn test_deploy_counter_constructor() { // let init_value = 10; @@ -62,3 +63,4 @@ fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFa // assert_eq!(counter_2.get_current_count(), argument_value); // } + From 5f766fd7d075d831c420e47afc9c4dbb10cbac6e Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 11:02:31 +0200 Subject: [PATCH 027/116] add 1 test --- .../applications/advanced_factory/Scarb.toml | 2 +- .../advanced_factory/src/campaign.cairo | 37 ++++--- .../advanced_factory/src/tests.cairo | 97 ++++++++++--------- 3 files changed, 72 insertions(+), 64 deletions(-) diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml index d1f18234..6bea29d5 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -11,7 +11,7 @@ snforge_std.workspace = true openzeppelin.workspace = true # StarknetByExample Components components.workspace = true -alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" } [scripts] test.workspace = true diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index a34d3064..67144ea9 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -2,8 +2,9 @@ use starknet::ClassHash; #[starknet::interface] pub trait ICampaign { - fn contribute(ref self: TContractState, amount: u256); fn claim(ref self: TContractState); + fn contribute(ref self: TContractState, amount: u256); + fn get_title(self: @TContractState) -> ByteArray; fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -111,21 +112,6 @@ pub mod Campaign { #[abi(embed_v0)] impl Campaign of super::ICampaign { - fn contribute(ref self: ContractState, amount: u256) { - assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); - assert(amount > 0, Errors::ZERO_DONATION); - - let contributor = get_caller_address(); - let this = get_contract_address(); - let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); - assert(success, Errors::TRANSFER_FAILED); - - self.contributions.write(contributor, self.contributions.read(contributor) + amount); - self.total_contributions.write(self.total_contributions.read() + amount); - - self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); - } - fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); @@ -145,6 +131,25 @@ pub mod Campaign { self.emit(Event::Claimed(Claimed { amount })); } + fn contribute(ref self: ContractState, amount: u256) { + assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); + assert(amount > 0, Errors::ZERO_DONATION); + + let contributor = get_caller_address(); + let this = get_contract_address(); + let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); + assert(success, Errors::TRANSFER_FAILED); + + self.contributions.write(contributor, self.contributions.read(contributor) + amount); + self.total_contributions.write(self.total_contributions.read() + amount); + + self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + } + + fn get_title(self: @ContractState) -> ByteArray { + self.title.read() + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 4b29a896..2595d2dd 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,48 +1,51 @@ -// use core::result::ResultTrait; -// use advanced_factory::{CounterFactory, ICounterFactoryDispatcher, -// ICounterFactoryDispatcherTrait}; -// use starknet::{ContractAddress, ClassHash,}; -// use snforge_std::{declare, ContractClass, ContractClassTrait}; - -// // Define a target contract to deploy -// use advanced_factory::simple_counter::{ISimpleCounterDispatcher, ISimpleCounterDispatcherTrait}; - -// /// Deploy a counter factory contract -// fn deploy_factory(counter_class_hash: ClassHash, init_value: u128) -> ICounterFactoryDispatcher { -// let mut constructor_calldata: @Array:: = @array![ -// init_value.into(), counter_class_hash.into() -// ]; - -// let contract = declare("CounterFactory").unwrap(); -// let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); - -// ICounterFactoryDispatcher { contract_address } -// } -// #[test] -// fn test_deploy_counter_constructor() { -// let init_value = 10; - -// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; -// let factory = deploy_factory(counter_class_hash, init_value); - -// let counter_address = factory.create_counter(); -// let counter = ISimpleCounterDispatcher { contract_address: counter_address }; - -// assert_eq!(counter.get_current_count(), init_value); -// } - +use core::clone::Clone; +use core::result::ResultTrait; +use advanced_factory::factory::{ + CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait +}; +use starknet::{ContractAddress, ClassHash,}; +use snforge_std::{declare, ContractClass, ContractClassTrait}; + +// Define a target contract to deploy +use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; + +/// Deploy a campaign factory contract +fn deploy_factory(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { + let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; + + let contract = declare("CrowdfundingFactory").unwrap(); + let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); + + ICrowdfundingFactoryDispatcher { contract_address } +} + +#[test] +fn test_deploy_campaign_constructor() { + let campaign_class_hash = declare("Campaign").unwrap().class_hash; + let factory = deploy_factory(campaign_class_hash); + + let title: ByteArray = "New campaign"; + let description: ByteArray = "Some description"; + let target: u256 = 10000; + let duration: u64 = 60; + + let campaign_address = factory.create_campaign(title, description, target, duration); + let campaign = ICampaignDispatcher { contract_address: campaign_address }; + + assert_eq!(campaign.get_title(), title); +} // #[test] -// fn test_deploy_counter_argument() { +// fn test_deploy_campaign_argument() { // let init_value = 10; // let argument_value = 20; -// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; -// let factory = deploy_factory(counter_class_hash, init_value); +// let campaign_class_hash = declare("Campaign").unwrap().class_hash; +// let factory = deploy_factory(campaign_class_hash, init_value); -// let counter_address = factory.create_counter_at(argument_value); -// let counter = ISimpleCounterDispatcher { contract_address: counter_address }; +// let campaign_address = factory.create_campaign_at(argument_value); +// let campaign = ICampaignDispatcher { contract_address: campaign_address }; -// assert_eq!(counter.get_current_count(), argument_value); +// assert_eq!(campaign.get_current_count(), argument_value); // } // #[test] @@ -50,17 +53,17 @@ // let init_value = 10; // let argument_value = 20; -// let counter_class_hash = declare("SimpleCounter").unwrap().class_hash; -// let factory = deploy_factory(counter_class_hash, init_value); +// let campaign_class_hash = declare("Campaign").unwrap().class_hash; +// let factory = deploy_factory(campaign_class_hash, init_value); -// let mut counter_address = factory.create_counter(); -// let counter_1 = ISimpleCounterDispatcher { contract_address: counter_address }; +// let mut campaign_address = factory.create_campaign(); +// let campaign_1 = ICampaignDispatcher { contract_address: campaign_address }; -// counter_address = factory.create_counter_at(argument_value); -// let counter_2 = ISimpleCounterDispatcher { contract_address: counter_address }; +// campaign_address = factory.create_campaign_at(argument_value); +// let campaign_2 = ICampaignDispatcher { contract_address: campaign_address }; -// assert_eq!(counter_1.get_current_count(), init_value); -// assert_eq!(counter_2.get_current_count(), argument_value); +// assert_eq!(campaign_1.get_current_count(), init_value); +// assert_eq!(campaign_2.get_current_count(), argument_value); // } From 74fe0fd8bacf9cd3995b15155ce0291346ee0576 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:14:06 +0200 Subject: [PATCH 028/116] add get_description --- listings/applications/advanced_factory/src/campaign.cairo | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 67144ea9..fe743483 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -4,6 +4,7 @@ use starknet::ClassHash; pub trait ICampaign { fn claim(ref self: TContractState); fn contribute(ref self: TContractState, amount: u256); + fn get_description(self: @TContractState) -> ByteArray; fn get_title(self: @TContractState) -> ByteArray; fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -150,6 +151,10 @@ pub mod Campaign { self.title.read() } + fn get_description(self: @ContractState) -> ByteArray { + self.description.read() + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); From b3e1efbf2d9140f8b501dd6a934eca060f75c9d8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:14:25 +0200 Subject: [PATCH 029/116] add correct deps --- listings/applications/advanced_factory/Scarb.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml index 6bea29d5..0e0c0b95 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -5,16 +5,12 @@ edition = "2023_11" [dependencies] starknet.workspace = true -# Starknet Foundry: -snforge_std.workspace = true -# OpenZeppelin: openzeppelin.workspace = true -# StarknetByExample Components components.workspace = true -alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +alexandria_storage.workspace = true [scripts] test.workspace = true [[target.starknet-contract]] -casm = true +casm = true \ No newline at end of file From b327d80b93612984c0932ae7619d276b75255bda Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:16:11 +0200 Subject: [PATCH 030/116] add alexandria to toml --- Scarb.lock | 55 +----------------------------------------------------- Scarb.toml | 1 + 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index 61b413d4..d916f796 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -5,62 +5,9 @@ version = 1 name = "advanced_factory" version = "0.1.0" dependencies = [ - "alexandria_math", + "alexandria_storage", "components", "openzeppelin", - "snforge_std", -] - -[[package]] -name = "alexandria_bytes" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_data_structures", - "alexandria_math", -] - -[[package]] -name = "alexandria_data_structures" -version = "0.2.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_encoding", -] - -[[package]] -name = "alexandria_encoding" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_bytes", - "alexandria_math", - "alexandria_numeric", -] - -[[package]] -name = "alexandria_math" -version = "0.2.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_data_structures", -] - -[[package]] -name = "alexandria_numeric" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_math", - "alexandria_searching", -] - -[[package]] -name = "alexandria_searching" -version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" -dependencies = [ - "alexandria_data_structures", ] [[package]] diff --git a/Scarb.toml b/Scarb.toml index d15c540a..1f1df74f 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -16,6 +16,7 @@ starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } [workspace.package] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." From 109294664bc12b2d2860c3c796dfbf95a2cb0a64 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:17:23 +0200 Subject: [PATCH 031/116] format factory.cairo --- .../applications/advanced_factory/src/factory.cairo | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index d7d180d7..37136db2 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -115,10 +115,12 @@ pub mod CrowdfundingFactory { // upgrade each campaign with the new class hash let campaigns = self.campaigns.read(); let mut i = 0; - while let Option::Some(campaign) = campaigns.get(i).unwrap_syscall() { - campaign.upgrade(new_class_hash); - i += 1; - }; + while let Option::Some(campaign) = campaigns + .get(i) + .unwrap_syscall() { + campaign.upgrade(new_class_hash); + i += 1; + }; self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); } From 3bba851a34b4cdffa6f3e9e5d918b97f3fc6acbe Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:34:35 +0200 Subject: [PATCH 032/116] add missing snforge workspace --- listings/applications/advanced_factory/Scarb.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml index 0e0c0b95..c84b99dd 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -8,6 +8,7 @@ starknet.workspace = true openzeppelin.workspace = true components.workspace = true alexandria_storage.workspace = true +snforge_std.workspace = true [scripts] test.workspace = true From f1bd81e370265e3f02e7420825aee53cf60ec3dc Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 12:35:52 +0200 Subject: [PATCH 033/116] add missing getters + tests --- Scarb.lock | 1 + .../advanced_factory/src/campaign.cairo | 12 +++++++++++- .../advanced_factory/src/tests.cairo | 19 +++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index d916f796..ce5fb820 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -8,6 +8,7 @@ dependencies = [ "alexandria_storage", "components", "openzeppelin", + "snforge_std", ] [[package]] diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index fe743483..91be1788 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -6,6 +6,8 @@ pub trait ICampaign { fn contribute(ref self: TContractState, amount: u256); fn get_description(self: @TContractState) -> ByteArray; fn get_title(self: @TContractState) -> ByteArray; + fn get_target(self: @TContractState) -> u256; + fn get_end_time(self: @TContractState) -> u64; fn upgrade(ref self: TContractState, impl_hash: ClassHash); } @@ -23,7 +25,7 @@ pub mod Campaign { component!(path: ownable_component, storage: ownable, event: OwnableEvent); #[abi(embed_v0)] - impl OwnableImpl = ownable_component::Ownable; + pub impl OwnableImpl = ownable_component::Ownable; impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; #[storage] @@ -155,6 +157,14 @@ pub mod Campaign { self.description.read() } + fn get_target(self: @ContractState) -> u256 { + self.target.read() + } + + fn get_end_time(self: @ContractState) -> u64 { + self.end_time.read() + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 2595d2dd..9a28301c 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,13 +1,14 @@ use core::clone::Clone; use core::result::ResultTrait; use advanced_factory::factory::{ - CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait + ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait }; -use starknet::{ContractAddress, ClassHash,}; -use snforge_std::{declare, ContractClass, ContractClassTrait}; +use starknet::{ContractAddress, ClassHash, get_block_timestamp, contract_address_const}; +use snforge_std::{declare, ContractClass, ContractClassTrait, start_cheat_caller_address}; // Define a target contract to deploy use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; +use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign factory contract fn deploy_factory(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { @@ -24,15 +25,25 @@ fn test_deploy_campaign_constructor() { let campaign_class_hash = declare("Campaign").unwrap().class_hash; let factory = deploy_factory(campaign_class_hash); + let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); + start_cheat_caller_address(factory.contract_address, campaign_owner); + let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; let target: u256 = 10000; let duration: u64 = 60; - let campaign_address = factory.create_campaign(title, description, target, duration); + let campaign_address = factory + .create_campaign(title.clone(), description.clone(), target, duration); let campaign = ICampaignDispatcher { contract_address: campaign_address }; assert_eq!(campaign.get_title(), title); + assert_eq!(campaign.get_description(), description); + assert_eq!(campaign.get_target(), target); + assert_eq!(campaign.get_end_time(), get_block_timestamp() + duration); + + let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; + assert_eq!(campaign_ownable.owner(), campaign_owner); } // #[test] // fn test_deploy_campaign_argument() { From b5a7a3fbaf456be3c9edc052db79b0be9df78da2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 13:08:06 +0200 Subject: [PATCH 034/116] add factory deploy tests --- .../advanced_factory/src/factory.cairo | 5 +++ .../advanced_factory/src/tests.cairo | 36 +++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 37136db2..aa210e21 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -10,6 +10,7 @@ pub trait ICrowdfundingFactory { target: u256, duration: u64 ) -> ContractAddress; + fn get_campaign_class_hash(self: @TContractState) -> ClassHash; fn update_campaign_class_hash(ref self: TContractState, new_class_hash: ClassHash); } @@ -106,6 +107,10 @@ pub mod CrowdfundingFactory { } // ANCHOR_END: deploy + fn get_campaign_class_hash(self: @ContractState) -> ClassHash { + self.campaign_class_hash.read() + } + fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) { self.ownable._assert_only_owner(); diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 9a28301c..1955379b 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -3,27 +3,51 @@ use core::result::ResultTrait; use advanced_factory::factory::{ ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait }; -use starknet::{ContractAddress, ClassHash, get_block_timestamp, contract_address_const}; +use starknet::{ + ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address +}; use snforge_std::{declare, ContractClass, ContractClassTrait, start_cheat_caller_address}; // Define a target contract to deploy use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; -/// Deploy a campaign factory contract -fn deploy_factory(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { + +/// Deploy a campaign factory contract with the provided campaign class hash +fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; let contract = declare("CrowdfundingFactory").unwrap(); - let (contract_address, _) = contract.deploy(constructor_calldata).unwrap(); + let contract_address = contract.precalculate_address(constructor_calldata); + let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); + start_cheat_caller_address(contract_address, factory_owner); + + contract.deploy(constructor_calldata).unwrap(); ICrowdfundingFactoryDispatcher { contract_address } } +/// Deploy a campaign factory contract with default campaign class hash +fn deploy_factory() -> ICrowdfundingFactoryDispatcher { + let campaign_class_hash = declare("Campaign").unwrap().class_hash; + deploy_factory_with(campaign_class_hash) +} + #[test] -fn test_deploy_campaign_constructor() { +fn test_deploy_factory() { let campaign_class_hash = declare("Campaign").unwrap().class_hash; - let factory = deploy_factory(campaign_class_hash); + let factory = deploy_factory_with(campaign_class_hash); + + assert_eq!(factory.get_campaign_class_hash(), campaign_class_hash); + + let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); + let factory_ownable = IOwnableDispatcher { contract_address: factory.contract_address }; + assert_eq!(factory_ownable.owner(), factory_owner); +} + +#[test] +fn test_deploy_campaign() { + let factory = deploy_factory(); let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); start_cheat_caller_address(factory.contract_address, campaign_owner); From 36adaa5e2fe7d1de4fcb78e43dba056d09f86351 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 16:03:41 +0200 Subject: [PATCH 035/116] add class hash update test + event assertions --- .../src/campaign_updated.cairo | 177 ++++++++++++++++++ .../advanced_factory/src/factory.cairo | 10 +- .../advanced_factory/src/lib.cairo | 1 + .../advanced_factory/src/tests.cairo | 84 +++++++-- 4 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 listings/applications/advanced_factory/src/campaign_updated.cairo diff --git a/listings/applications/advanced_factory/src/campaign_updated.cairo b/listings/applications/advanced_factory/src/campaign_updated.cairo new file mode 100644 index 00000000..ed16adae --- /dev/null +++ b/listings/applications/advanced_factory/src/campaign_updated.cairo @@ -0,0 +1,177 @@ +use starknet::ClassHash; + +#[starknet::interface] +pub trait ICampaign { + fn claim(ref self: TContractState); + fn contribute(ref self: TContractState, amount: u256); + fn get_description(self: @TContractState) -> ByteArray; + fn get_title(self: @TContractState) -> ByteArray; + fn get_target(self: @TContractState) -> u256; + fn get_end_time(self: @TContractState) -> u64; + fn upgrade(ref self: TContractState, impl_hash: ClassHash); +} + +#[starknet::contract] +pub mod Campaign_Updated { + 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 + }; + use components::ownable::ownable_component; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + pub impl OwnableImpl = ownable_component::Ownable; + impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, + contributions: LegacyMap, + end_time: u64, + eth_token: IERC20Dispatcher, + factory: ContractAddress, + target: u256, + title: ByteArray, + description: ByteArray, + total_contributions: u256, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + #[flat] + OwnableEvent: ownable_component::Event, + ContributionMade: ContributionMade, + Claimed: Claimed, + Upgraded: Upgraded, + } + + #[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 Upgraded { + pub implementation: ClassHash + } + + pub mod Errors { + pub const NOT_FACTORY: felt252 = 'Caller not factory'; + pub const INACTIVE: felt252 = 'Campaign already ended'; + pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; + 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 CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; + pub const FACTORY_ZERO: felt252 = 'Factory address cannot be zero'; + pub const CREATOR_ZERO: felt252 = 'Creator address cannot be zero'; + } + + #[constructor] + fn constructor( + ref self: ContractState, + creator: ContractAddress, + title: ByteArray, + description: ByteArray, + target: u256, + duration: u64, + factory: ContractAddress + ) { + assert(factory.is_non_zero(), Errors::FACTORY_ZERO); + assert(creator.is_non_zero(), Errors::CREATOR_ZERO); + assert(title.len() > 0, Errors::TITLE_EMPTY); + assert(target > 0, Errors::ZERO_TARGET); + assert(duration > 0, Errors::ZERO_DURATION); + + let eth_address = contract_address_const::< + 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 + >(); + self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); + + self.title.write(title); + self.target.write(target); + self.description.write(description); + self.end_time.write(get_block_timestamp() + duration); + self.factory.write(factory); + self.ownable._init(creator); + } + + #[abi(embed_v0)] + impl Campaign_Updated of super::ICampaign { + fn claim(ref self: ContractState) { + self.ownable._assert_only_owner(); + assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); + + let this = get_contract_address(); + let eth_token = self.eth_token.read(); + + let amount = eth_token.balance_of(this); + assert(amount > 0, Errors::ZERO_FUNDS); + + // 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 = eth_token.transfer(get_caller_address(), amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Claimed(Claimed { amount })); + } + + fn contribute(ref self: ContractState, amount: u256) { + assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); + assert(amount > 0, Errors::ZERO_DONATION); + + let contributor = get_caller_address(); + let this = get_contract_address(); + let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); + assert(success, Errors::TRANSFER_FAILED); + + self.contributions.write(contributor, self.contributions.read(contributor) + amount); + self.total_contributions.write(self.total_contributions.read() + amount); + + self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + } + + fn get_title(self: @ContractState) -> ByteArray { + self.title.read() + } + + fn get_description(self: @ContractState) -> ByteArray { + self.description.read() + } + + fn get_target(self: @ContractState) -> u256 { + self.target.read() + } + + fn get_end_time(self: @ContractState) -> u64 { + self.end_time.read() + } + + fn upgrade(ref self: ContractState, impl_hash: ClassHash) { + assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); + assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + + starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); + + self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); + } + } +} diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index aa210e21..2af7ea77 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -51,14 +51,14 @@ pub mod CrowdfundingFactory { } #[derive(Drop, starknet::Event)] - struct ClassHashUpdated { - new_class_hash: ClassHash, + pub struct ClassHashUpdated { + pub new_class_hash: ClassHash, } #[derive(Drop, starknet::Event)] - struct CampaignCreated { - caller: ContractAddress, - contract_address: ContractAddress + pub struct CampaignCreated { + pub caller: ContractAddress, + pub contract_address: ContractAddress } pub mod Errors { diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo index 6d4daea8..1b70f15b 100644 --- a/listings/applications/advanced_factory/src/lib.cairo +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -1,5 +1,6 @@ mod factory; mod campaign; +mod campaign_updated; #[cfg(test)] mod tests; diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 1955379b..4fd5e255 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,15 +1,19 @@ +use core::traits::TryInto; use core::clone::Clone; use core::result::ResultTrait; use advanced_factory::factory::{ - ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait + CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait }; use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address }; -use snforge_std::{declare, ContractClass, ContractClassTrait, start_cheat_caller_address}; +use snforge_std::{ + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, spy_events, SpyOn, + EventSpy, EventAssertions, get_class_hash +}; // Define a target contract to deploy -use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; +use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; @@ -49,6 +53,8 @@ fn test_deploy_factory() { fn test_deploy_campaign() { let factory = deploy_factory(); + let mut spy = spy_events(SpyOn::One(factory.contract_address)); + let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); start_cheat_caller_address(factory.contract_address, campaign_owner); @@ -66,23 +72,69 @@ fn test_deploy_campaign() { assert_eq!(campaign.get_target(), target); assert_eq!(campaign.get_end_time(), get_block_timestamp() + duration); - let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; + let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; assert_eq!(campaign_ownable.owner(), campaign_owner); -} -// #[test] -// fn test_deploy_campaign_argument() { -// let init_value = 10; -// let argument_value = 20; - -// let campaign_class_hash = declare("Campaign").unwrap().class_hash; -// let factory = deploy_factory(campaign_class_hash, init_value); -// let campaign_address = factory.create_campaign_at(argument_value); -// let campaign = ICampaignDispatcher { contract_address: campaign_address }; + spy + .assert_emitted( + @array![ + ( + factory.contract_address, + CrowdfundingFactory::Event::CampaignCreated( + CrowdfundingFactory::CampaignCreated { + caller: campaign_owner, contract_address: campaign_address + } + ) + ) + ] + ); +} -// assert_eq!(campaign.get_current_count(), argument_value); -// } +#[test] +fn test_update_campaign_class_hash() { + let factory = deploy_factory(); + let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60); + let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120); + + let mut spy_factory = spy_events(SpyOn::One(factory.contract_address)); + let mut spy_campaigns = spy_events( + SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) + ); + + let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + factory.update_campaign_class_hash(new_class_hash); + + assert_eq!(factory.get_campaign_class_hash(), new_class_hash); + assert_eq!(get_class_hash(campaign_address_1), new_class_hash); + assert_eq!(get_class_hash(campaign_address_2), new_class_hash); + + spy_factory + .assert_emitted( + @array![ + ( + factory.contract_address, + CrowdfundingFactory::Event::ClassHashUpdated( + CrowdfundingFactory::ClassHashUpdated { new_class_hash } + ) + ) + ] + ); + + spy_campaigns + .assert_emitted( + @array![ + ( + campaign_address_1, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ), + ( + campaign_address_2, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ) + ] + ); +} // #[test] // fn test_deploy_multiple() { // let init_value = 10; From a2902a20d9888397a4e162e149998758badf7081 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 16:06:02 +0200 Subject: [PATCH 036/116] assert old class hash prior to update --- listings/applications/advanced_factory/src/tests.cairo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 4fd5e255..c2aaebae 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -97,6 +97,9 @@ fn test_update_campaign_class_hash() { let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60); let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120); + assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_1)); + assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_2)); + let mut spy_factory = spy_events(SpyOn::One(factory.contract_address)); let mut spy_campaigns = spy_events( SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) From 586a0f027adc66ac6aa39eb23f3d73d65cd7044f Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 16:06:32 +0200 Subject: [PATCH 037/116] remove commented out test --- .../advanced_factory/src/tests.cairo | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index c2aaebae..ee00d2a7 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -138,22 +138,4 @@ fn test_update_campaign_class_hash() { ] ); } -// #[test] -// fn test_deploy_multiple() { -// let init_value = 10; -// let argument_value = 20; - -// let campaign_class_hash = declare("Campaign").unwrap().class_hash; -// let factory = deploy_factory(campaign_class_hash, init_value); - -// let mut campaign_address = factory.create_campaign(); -// let campaign_1 = ICampaignDispatcher { contract_address: campaign_address }; - -// campaign_address = factory.create_campaign_at(argument_value); -// let campaign_2 = ICampaignDispatcher { contract_address: campaign_address }; - -// assert_eq!(campaign_1.get_current_count(), init_value); -// assert_eq!(campaign_2.get_current_count(), argument_value); -// } - From 2eba71b392ea6398bca18849d0cd0bbb31b665d0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 16:08:42 +0200 Subject: [PATCH 038/116] use common alex. storage workspace in using_lists --- listings/advanced-concepts/using_lists/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/advanced-concepts/using_lists/Scarb.toml b/listings/advanced-concepts/using_lists/Scarb.toml index 3ccb4af4..20fc9020 100644 --- a/listings/advanced-concepts/using_lists/Scarb.toml +++ b/listings/advanced-concepts/using_lists/Scarb.toml @@ -5,7 +5,7 @@ edition = '2023_11' [dependencies] starknet.workspace = true -alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad"} +alexandria_storage.workspace = true [scripts] test.workspace = true From 399c12aa0759d044c1b074bfc766dc90ff8be084 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 16:16:54 +0200 Subject: [PATCH 039/116] add missing newline in toml --- listings/applications/advanced_factory/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml index c84b99dd..dce4b726 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -14,4 +14,4 @@ snforge_std.workspace = true test.workspace = true [[target.starknet-contract]] -casm = true \ No newline at end of file +casm = true From 3d71f5913919f8a210b2c65c42e1f60fd70359de Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 18:12:06 +0200 Subject: [PATCH 040/116] move factory tests to separate file --- .../advanced_factory/src/tests.cairo | 142 +----------------- .../advanced_factory/src/tests/factory.cairo | 141 +++++++++++++++++ 2 files changed, 142 insertions(+), 141 deletions(-) create mode 100644 listings/applications/advanced_factory/src/tests/factory.cairo diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index ee00d2a7..bb585f17 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,141 +1 @@ -use core::traits::TryInto; -use core::clone::Clone; -use core::result::ResultTrait; -use advanced_factory::factory::{ - CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait -}; -use starknet::{ - ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address -}; -use snforge_std::{ - declare, ContractClass, ContractClassTrait, start_cheat_caller_address, spy_events, SpyOn, - EventSpy, EventAssertions, get_class_hash -}; - -// Define a target contract to deploy -use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; - - -/// Deploy a campaign factory contract with the provided campaign class hash -fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { - let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; - - let contract = declare("CrowdfundingFactory").unwrap(); - let contract_address = contract.precalculate_address(constructor_calldata); - let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); - start_cheat_caller_address(contract_address, factory_owner); - - contract.deploy(constructor_calldata).unwrap(); - - ICrowdfundingFactoryDispatcher { contract_address } -} - -/// Deploy a campaign factory contract with default campaign class hash -fn deploy_factory() -> ICrowdfundingFactoryDispatcher { - let campaign_class_hash = declare("Campaign").unwrap().class_hash; - deploy_factory_with(campaign_class_hash) -} - -#[test] -fn test_deploy_factory() { - let campaign_class_hash = declare("Campaign").unwrap().class_hash; - let factory = deploy_factory_with(campaign_class_hash); - - assert_eq!(factory.get_campaign_class_hash(), campaign_class_hash); - - let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); - let factory_ownable = IOwnableDispatcher { contract_address: factory.contract_address }; - assert_eq!(factory_ownable.owner(), factory_owner); -} - -#[test] -fn test_deploy_campaign() { - let factory = deploy_factory(); - - let mut spy = spy_events(SpyOn::One(factory.contract_address)); - - let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); - start_cheat_caller_address(factory.contract_address, campaign_owner); - - let title: ByteArray = "New campaign"; - let description: ByteArray = "Some description"; - let target: u256 = 10000; - let duration: u64 = 60; - - let campaign_address = factory - .create_campaign(title.clone(), description.clone(), target, duration); - let campaign = ICampaignDispatcher { contract_address: campaign_address }; - - assert_eq!(campaign.get_title(), title); - assert_eq!(campaign.get_description(), description); - assert_eq!(campaign.get_target(), target); - assert_eq!(campaign.get_end_time(), get_block_timestamp() + duration); - - let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; - assert_eq!(campaign_ownable.owner(), campaign_owner); - - spy - .assert_emitted( - @array![ - ( - factory.contract_address, - CrowdfundingFactory::Event::CampaignCreated( - CrowdfundingFactory::CampaignCreated { - caller: campaign_owner, contract_address: campaign_address - } - ) - ) - ] - ); -} - -#[test] -fn test_update_campaign_class_hash() { - let factory = deploy_factory(); - - let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60); - let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120); - - assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_1)); - assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_2)); - - let mut spy_factory = spy_events(SpyOn::One(factory.contract_address)); - let mut spy_campaigns = spy_events( - SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) - ); - - let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; - factory.update_campaign_class_hash(new_class_hash); - - assert_eq!(factory.get_campaign_class_hash(), new_class_hash); - assert_eq!(get_class_hash(campaign_address_1), new_class_hash); - assert_eq!(get_class_hash(campaign_address_2), new_class_hash); - - spy_factory - .assert_emitted( - @array![ - ( - factory.contract_address, - CrowdfundingFactory::Event::ClassHashUpdated( - CrowdfundingFactory::ClassHashUpdated { new_class_hash } - ) - ) - ] - ); - - spy_campaigns - .assert_emitted( - @array![ - ( - campaign_address_1, - Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) - ), - ( - campaign_address_2, - Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) - ) - ] - ); -} - +mod factory; diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/advanced_factory/src/tests/factory.cairo new file mode 100644 index 00000000..ee00d2a7 --- /dev/null +++ b/listings/applications/advanced_factory/src/tests/factory.cairo @@ -0,0 +1,141 @@ +use core::traits::TryInto; +use core::clone::Clone; +use core::result::ResultTrait; +use advanced_factory::factory::{ + CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait +}; +use starknet::{ + ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address +}; +use snforge_std::{ + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, spy_events, SpyOn, + EventSpy, EventAssertions, get_class_hash +}; + +// Define a target contract to deploy +use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; + + +/// Deploy a campaign factory contract with the provided campaign class hash +fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { + let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; + + let contract = declare("CrowdfundingFactory").unwrap(); + let contract_address = contract.precalculate_address(constructor_calldata); + let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); + start_cheat_caller_address(contract_address, factory_owner); + + contract.deploy(constructor_calldata).unwrap(); + + ICrowdfundingFactoryDispatcher { contract_address } +} + +/// Deploy a campaign factory contract with default campaign class hash +fn deploy_factory() -> ICrowdfundingFactoryDispatcher { + let campaign_class_hash = declare("Campaign").unwrap().class_hash; + deploy_factory_with(campaign_class_hash) +} + +#[test] +fn test_deploy_factory() { + let campaign_class_hash = declare("Campaign").unwrap().class_hash; + let factory = deploy_factory_with(campaign_class_hash); + + assert_eq!(factory.get_campaign_class_hash(), campaign_class_hash); + + let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); + let factory_ownable = IOwnableDispatcher { contract_address: factory.contract_address }; + assert_eq!(factory_ownable.owner(), factory_owner); +} + +#[test] +fn test_deploy_campaign() { + let factory = deploy_factory(); + + let mut spy = spy_events(SpyOn::One(factory.contract_address)); + + let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); + start_cheat_caller_address(factory.contract_address, campaign_owner); + + let title: ByteArray = "New campaign"; + let description: ByteArray = "Some description"; + let target: u256 = 10000; + let duration: u64 = 60; + + let campaign_address = factory + .create_campaign(title.clone(), description.clone(), target, duration); + let campaign = ICampaignDispatcher { contract_address: campaign_address }; + + assert_eq!(campaign.get_title(), title); + assert_eq!(campaign.get_description(), description); + assert_eq!(campaign.get_target(), target); + assert_eq!(campaign.get_end_time(), get_block_timestamp() + duration); + + let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; + assert_eq!(campaign_ownable.owner(), campaign_owner); + + spy + .assert_emitted( + @array![ + ( + factory.contract_address, + CrowdfundingFactory::Event::CampaignCreated( + CrowdfundingFactory::CampaignCreated { + caller: campaign_owner, contract_address: campaign_address + } + ) + ) + ] + ); +} + +#[test] +fn test_update_campaign_class_hash() { + let factory = deploy_factory(); + + let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60); + let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120); + + assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_1)); + assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_2)); + + let mut spy_factory = spy_events(SpyOn::One(factory.contract_address)); + let mut spy_campaigns = spy_events( + SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) + ); + + let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + factory.update_campaign_class_hash(new_class_hash); + + assert_eq!(factory.get_campaign_class_hash(), new_class_hash); + assert_eq!(get_class_hash(campaign_address_1), new_class_hash); + assert_eq!(get_class_hash(campaign_address_2), new_class_hash); + + spy_factory + .assert_emitted( + @array![ + ( + factory.contract_address, + CrowdfundingFactory::Event::ClassHashUpdated( + CrowdfundingFactory::ClassHashUpdated { new_class_hash } + ) + ) + ] + ); + + spy_campaigns + .assert_emitted( + @array![ + ( + campaign_address_1, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ), + ( + campaign_address_2, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ) + ] + ); +} + From 4674726859b5b859797191060b39400c70c288ca Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 18:25:51 +0200 Subject: [PATCH 041/116] add scaffold docs for contracts --- .../advanced_factory/src/campaign.cairo | 3 +++ .../advanced_factory/src/factory.cairo | 7 +++---- .../advanced_factory/src/tests/factory.cairo | 18 +++++++++--------- src/SUMMARY.md | 3 +++ src/applications/crowdfunding/campaign.md | 7 +++++++ src/applications/crowdfunding/crowdfunding.md | 1 + src/applications/crowdfunding/factory.md | 7 +++++++ 7 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/applications/crowdfunding/campaign.md create mode 100644 src/applications/crowdfunding/crowdfunding.md create mode 100644 src/applications/crowdfunding/factory.md diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 91be1788..e77dcd0d 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -1,3 +1,4 @@ +// ANCHOR: contract use starknet::ClassHash; #[starknet::interface] @@ -175,3 +176,5 @@ pub mod Campaign { } } } +// ANCHOR_END: contract + diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 2af7ea77..088d871f 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -2,7 +2,7 @@ pub use starknet::{ContractAddress, ClassHash}; #[starknet::interface] -pub trait ICrowdfundingFactory { +pub trait ICampaignFactory { fn create_campaign( ref self: TContractState, title: ByteArray, @@ -15,7 +15,7 @@ pub trait ICrowdfundingFactory { } #[starknet::contract] -pub mod CrowdfundingFactory { +pub mod CampaignFactory { use core::num::traits::zero::Zero; use starknet::{ ContractAddress, ClassHash, SyscallResultTrait, syscalls::deploy_syscall, @@ -74,7 +74,7 @@ pub mod CrowdfundingFactory { #[abi(embed_v0)] - impl CrowdfundingFactory of super::ICrowdfundingFactory { + impl CampaignFactory of super::ICampaignFactory { // ANCHOR: deploy fn create_campaign( ref self: ContractState, @@ -133,4 +133,3 @@ pub mod CrowdfundingFactory { } // ANCHOR_END: contract - diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/advanced_factory/src/tests/factory.cairo index ee00d2a7..d1297416 100644 --- a/listings/applications/advanced_factory/src/tests/factory.cairo +++ b/listings/applications/advanced_factory/src/tests/factory.cairo @@ -2,7 +2,7 @@ use core::traits::TryInto; use core::clone::Clone; use core::result::ResultTrait; use advanced_factory::factory::{ - CrowdfundingFactory, ICrowdfundingFactoryDispatcher, ICrowdfundingFactoryDispatcherTrait + CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait }; use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address @@ -18,21 +18,21 @@ use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign factory contract with the provided campaign class hash -fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICrowdfundingFactoryDispatcher { +fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICampaignFactoryDispatcher { let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; - let contract = declare("CrowdfundingFactory").unwrap(); + let contract = declare("CampaignFactory").unwrap(); let contract_address = contract.precalculate_address(constructor_calldata); let factory_owner: ContractAddress = contract_address_const::<'factory_owner'>(); start_cheat_caller_address(contract_address, factory_owner); contract.deploy(constructor_calldata).unwrap(); - ICrowdfundingFactoryDispatcher { contract_address } + ICampaignFactoryDispatcher { contract_address } } /// Deploy a campaign factory contract with default campaign class hash -fn deploy_factory() -> ICrowdfundingFactoryDispatcher { +fn deploy_factory() -> ICampaignFactoryDispatcher { let campaign_class_hash = declare("Campaign").unwrap().class_hash; deploy_factory_with(campaign_class_hash) } @@ -80,8 +80,8 @@ fn test_deploy_campaign() { @array![ ( factory.contract_address, - CrowdfundingFactory::Event::CampaignCreated( - CrowdfundingFactory::CampaignCreated { + CampaignFactory::Event::CampaignCreated( + CampaignFactory::CampaignCreated { caller: campaign_owner, contract_address: campaign_address } ) @@ -117,8 +117,8 @@ fn test_update_campaign_class_hash() { @array![ ( factory.contract_address, - CrowdfundingFactory::Event::ClassHashUpdated( - CrowdfundingFactory::ClassHashUpdated { new_class_hash } + CampaignFactory::Event::ClassHashUpdated( + CampaignFactory::ClassHashUpdated { new_class_hash } ) ) ] diff --git a/src/SUMMARY.md b/src/SUMMARY.md index d642f9be..240630ef 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -60,6 +60,9 @@ Summary - [TimeLock](./applications/timelock.md) - [Staking](./applications/staking.md) - [Simple Storage with Starknet-js](./applications/simple_storage_starknetjs.md) +- [Crowdfunding](./applications/crowdfunding/crowdfunding.md) + - [Campaign Contract](./applications/crowdfunding/campaign.md) + - [CampaignFactory Contract](./applications/crowdfunding/factory.md) diff --git a/src/applications/crowdfunding/campaign.md b/src/applications/crowdfunding/campaign.md new file mode 100644 index 00000000..024e3a47 --- /dev/null +++ b/src/applications/crowdfunding/campaign.md @@ -0,0 +1,7 @@ +# Campaign Contract + +This is the Campaign contract. + +```rust +{{#include ../../../listings/applications/advanced_factory/src/campaign.cairo:contract}} +``` diff --git a/src/applications/crowdfunding/crowdfunding.md b/src/applications/crowdfunding/crowdfunding.md new file mode 100644 index 00000000..f7d424c0 --- /dev/null +++ b/src/applications/crowdfunding/crowdfunding.md @@ -0,0 +1 @@ +# Crowdfunding \ No newline at end of file diff --git a/src/applications/crowdfunding/factory.md b/src/applications/crowdfunding/factory.md new file mode 100644 index 00000000..60b052eb --- /dev/null +++ b/src/applications/crowdfunding/factory.md @@ -0,0 +1,7 @@ +# CampaignFactory Contract + +This is the CampaignFactory contract that creates new Campaign contract instances. + +```rust +{{#include ../../../listings/applications/advanced_factory/src/factory.cairo:contract}} +``` From 2c1ef6bf01b4c3214414e9b365767bcedbc90bd7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 18:29:43 +0200 Subject: [PATCH 042/116] add end_time asserts --- .../advanced_factory/src/campaign.cairo | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index e77dcd0d..24512b1a 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -72,7 +72,7 @@ pub mod Campaign { pub mod Errors { pub const NOT_FACTORY: felt252 = 'Caller not factory'; - pub const INACTIVE: felt252 = 'Campaign already ended'; + pub const ENDED: felt252 = 'Campaign already ended'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; pub const ZERO_TARGET: felt252 = 'Target must be > 0'; @@ -118,7 +118,7 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); - assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); + self._assert_ended(); let this = get_contract_address(); let eth_token = self.eth_token.read(); @@ -136,7 +136,7 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); + self._assert_active(); assert(amount > 0, Errors::ZERO_DONATION); let contributor = get_caller_address(); @@ -175,6 +175,18 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } } + + #[generate_trait] + impl CampaignInternalImpl of CampaignInternalTrait { + fn _assert_ended(self: @ContractState) { + assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); + } + + fn _assert_active(self: @ContractState) { + assert(get_block_timestamp() < self.end_time.read(), Errors::ENDED); + } + } } // ANCHOR_END: contract + From 4b62bb650aa759118c2a2bc666e2d35f62f068f8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 18:32:47 +0200 Subject: [PATCH 043/116] refactor private asserts --- .../applications/advanced_factory/src/campaign.cairo | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 24512b1a..083fc030 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -118,7 +118,7 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); - self._assert_ended(); + assert(!self._is_active(), Errors::STILL_ACTIVE); let this = get_contract_address(); let eth_token = self.eth_token.read(); @@ -136,7 +136,7 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - self._assert_active(); + assert(self._is_active(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let contributor = get_caller_address(); @@ -178,12 +178,8 @@ pub mod Campaign { #[generate_trait] impl CampaignInternalImpl of CampaignInternalTrait { - fn _assert_ended(self: @ContractState) { - assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); - } - - fn _assert_active(self: @ContractState) { - assert(get_block_timestamp() < self.end_time.read(), Errors::ENDED); + fn _is_active(self: @ContractState) -> bool { + get_block_timestamp() < self.end_time.read() } } } From 07e85ff9e6387bf816dfb74de249eb5cfa5416ea Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 18:36:07 +0200 Subject: [PATCH 044/116] check if target reached before claiming --- listings/applications/advanced_factory/src/campaign.cairo | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 083fc030..cf581bfc 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -83,6 +83,7 @@ pub mod Campaign { pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; pub const FACTORY_ZERO: felt252 = 'Factory address cannot be zero'; pub const CREATOR_ZERO: felt252 = 'Creator address cannot be zero'; + pub const TARGET_NOT_REACHED: felt252 = 'Target not reached'; } #[constructor] @@ -119,6 +120,7 @@ pub mod Campaign { fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); assert(!self._is_active(), Errors::STILL_ACTIVE); + assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); let eth_token = self.eth_token.read(); @@ -181,6 +183,10 @@ pub mod Campaign { fn _is_active(self: @ContractState) -> bool { get_block_timestamp() < self.end_time.read() } + + fn _is_target_reached(self: @ContractState) -> bool { + self.total_contributions.read() >= self.target.read() + } } } // ANCHOR_END: contract From 1ddf27983dee1ef8552316fa262d8fdce8418baf Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 10 Jun 2024 19:27:46 +0200 Subject: [PATCH 045/116] add ability to withdraw funds --- .../advanced_factory/src/campaign.cairo | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index cf581bfc..ab2b6445 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -10,6 +10,7 @@ pub trait ICampaign { fn get_target(self: @TContractState) -> u256; fn get_end_time(self: @TContractState) -> u64; fn upgrade(ref self: TContractState, impl_hash: ClassHash); + fn withdraw(ref self: TContractState); } #[starknet::contract] @@ -51,6 +52,7 @@ pub mod Campaign { ContributionMade: ContributionMade, Claimed: Claimed, Upgraded: Upgraded, + Withdrawn: Withdrawn, } #[derive(Drop, starknet::Event)] @@ -70,6 +72,13 @@ pub mod Campaign { pub implementation: ClassHash } + #[derive(Drop, starknet::Event)] + pub struct Withdrawn { + #[key] + pub contributor: ContractAddress, + pub amount: u256, + } + pub mod Errors { pub const NOT_FACTORY: felt252 = 'Caller not factory'; pub const ENDED: felt252 = 'Campaign already ended'; @@ -84,6 +93,8 @@ pub mod Campaign { pub const FACTORY_ZERO: felt252 = 'Factory address 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] @@ -176,6 +187,23 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } + + fn withdraw(ref self: ContractState) { + assert(!self._is_active(), Errors::STILL_ACTIVE); + assert(!self._is_target_reached(), Errors::TARGET_ALREADY_REACHED); + assert(self.contributions.read(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); + + let caller = get_caller_address(); + let amount = self.contributions.read(caller); + self.contributions.write(caller, 0); + + // 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 + + self.eth_token.read().transfer(caller, amount); + + self.emit(Event::Withdrawn(Withdrawn { contributor: caller, amount })); + } } #[generate_trait] From 7de30aaf7e68f133bf77a6fa9800bcb97938baec Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 08:39:38 +0200 Subject: [PATCH 046/116] make contributions into a component (now iterable) --- .../advanced_factory/src/campaign.cairo | 19 +++- .../src/campaign/contributions.cairo | 98 +++++++++++++++++++ .../advanced_factory/src/factory.cairo | 1 + 3 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 listings/applications/advanced_factory/src/campaign/contributions.cairo diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index ab2b6445..74a926c1 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -1,3 +1,5 @@ +mod contributions; + // ANCHOR: contract use starknet::ClassHash; @@ -23,18 +25,23 @@ pub mod Campaign { get_caller_address, get_contract_address }; use components::ownable::ownable_component; + use super::contributions::contributable_component; 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 { #[substorage(v0)] ownable: ownable_component::Storage, - contributions: LegacyMap, + #[substorage(v0)] + contributions: contributable_component::Storage, end_time: u64, eth_token: IERC20Dispatcher, factory: ContractAddress, @@ -49,6 +56,7 @@ pub mod Campaign { pub enum Event { #[flat] OwnableEvent: ownable_component::Event, + ContributableEvent: contributable_component::Event, ContributionMade: ContributionMade, Claimed: Claimed, Upgraded: Upgraded, @@ -157,7 +165,7 @@ pub mod Campaign { let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); assert(success, Errors::TRANSFER_FAILED); - self.contributions.write(contributor, self.contributions.read(contributor) + amount); + self.contributions.add(contributor, amount); self.total_contributions.write(self.total_contributions.read() + amount); self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); @@ -191,11 +199,12 @@ pub mod Campaign { fn withdraw(ref self: ContractState) { assert(!self._is_active(), Errors::STILL_ACTIVE); assert(!self._is_target_reached(), Errors::TARGET_ALREADY_REACHED); - assert(self.contributions.read(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); + assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let caller = get_caller_address(); - let amount = self.contributions.read(caller); - self.contributions.write(caller, 0); + let amount = self.contributions.get(caller); + + self.contributions.withhold(caller); // 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 diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo new file mode 100644 index 00000000..1bb54725 --- /dev/null +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -0,0 +1,98 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IContributable { + fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); + fn get(self: @TContractState, contributor: ContractAddress) -> u256; + fn get_all(self: @TContractState) -> Array; + fn withhold(ref self: TContractState, contributor: ContractAddress); +} + +#[starknet::component] +pub mod contributable_component { + use core::array::ArrayTrait; + use starknet::{ContractAddress}; + use core::num::traits::Zero; + + #[derive(Drop, Serde, starknet::Store)] + struct Contribution { + contributor: ContractAddress, + amount: u256, + } + + #[storage] + struct Storage { + index_to_contribution: LegacyMap, + contributor_to_index: LegacyMap>, + total_contributors: u32, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event {} + + #[embeddable_as(Contributable)] + pub impl ContributableImpl< + TContractState, +HasComponent + > of super::IContributable> { + fn add( + ref self: ComponentState, contributor: ContractAddress, amount: u256 + ) { + let i_opt: Option = self.contributor_to_index.read(contributor); + if let Option::Some(index) = i_opt { + let old_contr: Contribution = self.index_to_contribution.read(index); + let new_contr = Contribution { contributor, amount: old_contr.amount + amount }; + self.index_to_contribution.write(index, new_contr); + } else { + let index = self.total_contributors.read(); + self.contributor_to_index.write(contributor, Option::Some(index)); + self.index_to_contribution.write(index, Contribution { contributor, amount }); + self.total_contributors.write(index + 1); + } + } + + fn get(self: @ComponentState, contributor: ContractAddress) -> u256 { + let val: Option = self.contributor_to_index.read(contributor); + match val { + Option::Some(index) => { + let contr: Contribution = self.index_to_contribution.read(index); + contr.amount + }, + Option::None => 0, + } + } + + fn get_all(self: @ComponentState) -> Array { + let mut result = array![]; + + let mut index = self.total_contributors.read(); + while index != 0 { + index -= 1; + let contr: Contribution = self.index_to_contribution.read(index); + result.append(contr.contributor); + }; + + result + } + + fn withhold(ref self: ComponentState, contributor: ContractAddress) { + let i_opt: Option = self.contributor_to_index.read(contributor); + if let Option::Some(index) = i_opt { + self.contributor_to_index.write(contributor, Option::None); + self.total_contributors.write(self.total_contributors.read() - 1); + if self.total_contributors.read() != 0 { + let last_contr: Contribution = self + .index_to_contribution + .read(self.total_contributors.read()); + self.contributor_to_index.write(last_contr.contributor, Option::Some(index)); + self.index_to_contribution.write(index, last_contr); + } else { + self + .index_to_contribution + .write(index, Contribution { contributor: Zero::zero(), amount: 0 }); + } + } + } + } +} + diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 088d871f..2d1305a7 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -133,3 +133,4 @@ pub mod CampaignFactory { } // ANCHOR_END: contract + From 7af01aa03e314338356a28fdfabfe9ee35333e05 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 09:17:44 +0200 Subject: [PATCH 047/116] refactor 'withhold' - contrs map to amt_idx --- .../advanced_factory/src/campaign.cairo | 10 +-- .../src/campaign/contributions.cairo | 80 ++++++++++--------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 74a926c1..7bf12921 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -201,17 +201,15 @@ pub mod Campaign { assert(!self._is_target_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); - let caller = get_caller_address(); - let amount = self.contributions.get(caller); - - self.contributions.withhold(caller); + let contributor = get_caller_address(); + let amount = self.contributions.withhold(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 - self.eth_token.read().transfer(caller, amount); + self.eth_token.read().transfer(contributor, amount); - self.emit(Event::Withdrawn(Withdrawn { contributor: caller, amount })); + self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); } } diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index 1bb54725..f140b32e 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -5,7 +5,7 @@ pub trait IContributable { fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); fn get(self: @TContractState, contributor: ContractAddress) -> u256; fn get_all(self: @TContractState) -> Array; - fn withhold(ref self: TContractState, contributor: ContractAddress); + fn withhold(ref self: TContractState, contributor: ContractAddress) -> u256; } #[starknet::component] @@ -14,16 +14,10 @@ pub mod contributable_component { use starknet::{ContractAddress}; use core::num::traits::Zero; - #[derive(Drop, Serde, starknet::Store)] - struct Contribution { - contributor: ContractAddress, - amount: u256, - } - #[storage] struct Storage { - index_to_contribution: LegacyMap, - contributor_to_index: LegacyMap>, + idx_to_contributor: LegacyMap, + contributor_to_amt_idx: LegacyMap>, total_contributors: u32, } @@ -38,26 +32,23 @@ pub mod contributable_component { fn add( ref self: ComponentState, contributor: ContractAddress, amount: u256 ) { - let i_opt: Option = self.contributor_to_index.read(contributor); - if let Option::Some(index) = i_opt { - let old_contr: Contribution = self.index_to_contribution.read(index); - let new_contr = Contribution { contributor, amount: old_contr.amount + amount }; - self.index_to_contribution.write(index, new_contr); + let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); + if let Option::Some((old_amount, idx)) = amt_idx_opt { + self + .contributor_to_amt_idx + .write(contributor, Option::Some((old_amount + amount, idx))); } else { - let index = self.total_contributors.read(); - self.contributor_to_index.write(contributor, Option::Some(index)); - self.index_to_contribution.write(index, Contribution { contributor, amount }); - self.total_contributors.write(index + 1); + let idx = self.total_contributors.read(); + self.idx_to_contributor.write(idx, contributor); + self.contributor_to_amt_idx.write(contributor, Option::Some((amount, idx))); + self.total_contributors.write(idx + 1); } } fn get(self: @ComponentState, contributor: ContractAddress) -> u256 { - let val: Option = self.contributor_to_index.read(contributor); + let val: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); match val { - Option::Some(index) => { - let contr: Contribution = self.index_to_contribution.read(index); - contr.amount - }, + Option::Some((amt, _)) => amt, Option::None => 0, } } @@ -68,29 +59,40 @@ pub mod contributable_component { let mut index = self.total_contributors.read(); while index != 0 { index -= 1; - let contr: Contribution = self.index_to_contribution.read(index); - result.append(contr.contributor); + result.append(self.idx_to_contributor.read(index)); }; result } - fn withhold(ref self: ComponentState, contributor: ContractAddress) { - let i_opt: Option = self.contributor_to_index.read(contributor); - if let Option::Some(index) = i_opt { - self.contributor_to_index.write(contributor, Option::None); - self.total_contributors.write(self.total_contributors.read() - 1); - if self.total_contributors.read() != 0 { - let last_contr: Contribution = self - .index_to_contribution - .read(self.total_contributors.read()); - self.contributor_to_index.write(last_contr.contributor, Option::Some(index)); - self.index_to_contribution.write(index, last_contr); - } else { + fn withhold( + ref self: ComponentState, contributor: ContractAddress + ) -> u256 { + let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); + if let Option::Some((amt, idx)) = amt_idx_opt { + self.contributor_to_amt_idx.write(contributor, Option::None); + let total_contributors = self.total_contributors.read() - 1; + self.total_contributors.write(total_contributors); + if total_contributors != 0 { + let last_contributor = self.idx_to_contributor.read(total_contributors); + let last_amt_idx: Option<(u256, u32)> = self + .contributor_to_amt_idx + .read(last_contributor); + let last_amt = match last_amt_idx { + Option::Some((l_a, _)) => l_a, + Option::None => 0 + }; self - .index_to_contribution - .write(index, Contribution { contributor: Zero::zero(), amount: 0 }); + .contributor_to_amt_idx + .write(last_contributor, Option::Some((last_amt, idx))); + self.idx_to_contributor.write(idx, last_contributor); } + + self.idx_to_contributor.write(total_contributors, Zero::zero()); + + amt + } else { + 0 } } } From 0ed6fdf8b1cd7750844976b149dd464e1c63f45f Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 09:21:54 +0200 Subject: [PATCH 048/116] add get_contributors func --- listings/applications/advanced_factory/src/campaign.cairo | 7 ++++++- .../advanced_factory/src/campaign/contributions.cairo | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 7bf12921..1fb5c0b9 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -1,12 +1,13 @@ mod contributions; // ANCHOR: contract -use starknet::ClassHash; +use starknet::{ClassHash, ContractAddress}; #[starknet::interface] pub trait ICampaign { fn claim(ref self: TContractState); fn contribute(ref self: TContractState, amount: u256); + fn get_contributors(self: @TContractState) -> Array; fn get_description(self: @TContractState) -> ByteArray; fn get_title(self: @TContractState) -> ByteArray; fn get_target(self: @TContractState) -> u256; @@ -171,6 +172,10 @@ pub mod Campaign { self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); } + fn get_contributors(self: @ContractState) -> Array { + self.contributions.get_contributors_as_arr() + } + fn get_title(self: @ContractState) -> ByteArray { self.title.read() } diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index f140b32e..c433085e 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -4,7 +4,7 @@ use starknet::ContractAddress; pub trait IContributable { fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); fn get(self: @TContractState, contributor: ContractAddress) -> u256; - fn get_all(self: @TContractState) -> Array; + fn get_contributors_as_arr(self: @TContractState) -> Array; fn withhold(ref self: TContractState, contributor: ContractAddress) -> u256; } @@ -53,7 +53,9 @@ pub mod contributable_component { } } - fn get_all(self: @ComponentState) -> Array { + fn get_contributors_as_arr( + self: @ComponentState + ) -> Array { let mut result = array![]; let mut index = self.total_contributors.read(); From b00df420587829f0fcd442fa230e93100ba8913a Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 09:26:30 +0200 Subject: [PATCH 049/116] get_contributors -> get_contributions --- .../advanced_factory/src/campaign.cairo | 6 +++--- .../src/campaign/contributions.cairo | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 1fb5c0b9..a5a84ba3 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -7,7 +7,7 @@ use starknet::{ClassHash, ContractAddress}; pub trait ICampaign { fn claim(ref self: TContractState); fn contribute(ref self: TContractState, amount: u256); - fn get_contributors(self: @TContractState) -> Array; + fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_description(self: @TContractState) -> ByteArray; fn get_title(self: @TContractState) -> ByteArray; fn get_target(self: @TContractState) -> u256; @@ -172,8 +172,8 @@ pub mod Campaign { self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); } - fn get_contributors(self: @ContractState) -> Array { - self.contributions.get_contributors_as_arr() + fn get_contributions(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.contributions.get_contributions_as_arr() } fn get_title(self: @ContractState) -> ByteArray { diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index c433085e..58559b6e 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -4,7 +4,7 @@ use starknet::ContractAddress; pub trait IContributable { fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); fn get(self: @TContractState, contributor: ContractAddress) -> u256; - fn get_contributors_as_arr(self: @TContractState) -> Array; + fn get_contributions_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; fn withhold(ref self: TContractState, contributor: ContractAddress) -> u256; } @@ -53,15 +53,21 @@ pub mod contributable_component { } } - fn get_contributors_as_arr( + fn get_contributions_as_arr( self: @ComponentState - ) -> Array { + ) -> Array<(ContractAddress, u256)> { let mut result = array![]; let mut index = self.total_contributors.read(); while index != 0 { index -= 1; - result.append(self.idx_to_contributor.read(index)); + let contr = self.idx_to_contributor.read(index); + let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contr); + let amount = match amt_idx_opt { + Option::Some((amt, _)) => amt, + Option::None => 0 + }; + result.append((contr, amount)); }; result From 58517ec13677a1f0c65b98ca5d0bb122a1817124 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 10:30:24 +0200 Subject: [PATCH 050/116] total_contributors->contributor_count --- .../src/campaign/contributions.cairo | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index 58559b6e..51704b5f 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -18,7 +18,7 @@ pub mod contributable_component { struct Storage { idx_to_contributor: LegacyMap, contributor_to_amt_idx: LegacyMap>, - total_contributors: u32, + contributor_count: u32, } #[event] @@ -38,10 +38,10 @@ pub mod contributable_component { .contributor_to_amt_idx .write(contributor, Option::Some((old_amount + amount, idx))); } else { - let idx = self.total_contributors.read(); + let idx = self.contributor_count.read(); self.idx_to_contributor.write(idx, contributor); self.contributor_to_amt_idx.write(contributor, Option::Some((amount, idx))); - self.total_contributors.write(idx + 1); + self.contributor_count.write(idx + 1); } } @@ -58,7 +58,7 @@ pub mod contributable_component { ) -> Array<(ContractAddress, u256)> { let mut result = array![]; - let mut index = self.total_contributors.read(); + let mut index = self.contributor_count.read(); while index != 0 { index -= 1; let contr = self.idx_to_contributor.read(index); @@ -79,10 +79,10 @@ pub mod contributable_component { let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); if let Option::Some((amt, idx)) = amt_idx_opt { self.contributor_to_amt_idx.write(contributor, Option::None); - let total_contributors = self.total_contributors.read() - 1; - self.total_contributors.write(total_contributors); - if total_contributors != 0 { - let last_contributor = self.idx_to_contributor.read(total_contributors); + let contributor_count = self.contributor_count.read() - 1; + self.contributor_count.write(contributor_count); + if contributor_count != 0 { + let last_contributor = self.idx_to_contributor.read(contributor_count); let last_amt_idx: Option<(u256, u32)> = self .contributor_to_amt_idx .read(last_contributor); @@ -96,7 +96,7 @@ pub mod contributable_component { self.idx_to_contributor.write(idx, last_contributor); } - self.idx_to_contributor.write(total_contributors, Zero::zero()); + self.idx_to_contributor.write(contributor_count, Zero::zero()); amt } else { From fae8b345217e9e3ee983258c4e9633a74d450bd6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 11:35:48 +0200 Subject: [PATCH 051/116] add tests for campaign upgrade and deploy + update all relevant code in factory --- .../advanced_factory/src/campaign.cairo | 26 ++--- .../advanced_factory/src/factory.cairo | 22 ++++- .../advanced_factory/src/tests.cairo | 1 + .../advanced_factory/src/tests/campaign.cairo | 96 +++++++++++++++++++ 4 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 listings/applications/advanced_factory/src/tests/campaign.cairo diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index a5a84ba3..e5dd45af 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -12,7 +12,7 @@ pub trait ICampaign { fn get_title(self: @TContractState) -> ByteArray; fn get_target(self: @TContractState) -> u256; fn get_end_time(self: @TContractState) -> u64; - fn upgrade(ref self: TContractState, impl_hash: ClassHash); + fn upgrade(ref self: TContractState, impl_hash: ClassHash) -> Result<(), Array>; fn withdraw(ref self: TContractState); } @@ -109,15 +109,13 @@ pub mod Campaign { #[constructor] fn constructor( ref self: ContractState, - creator: ContractAddress, + owner: ContractAddress, title: ByteArray, description: ByteArray, target: u256, duration: u64, - factory: ContractAddress ) { - assert(factory.is_non_zero(), Errors::FACTORY_ZERO); - assert(creator.is_non_zero(), Errors::CREATOR_ZERO); + assert(owner.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); assert(duration > 0, Errors::ZERO_DURATION); @@ -131,8 +129,8 @@ pub mod Campaign { self.target.write(target); self.description.write(description); self.end_time.write(get_block_timestamp() + duration); - self.factory.write(factory); - self.ownable._init(creator); + self.factory.write(get_caller_address()); + self.ownable._init(owner); } #[abi(embed_v0)] @@ -192,13 +190,19 @@ pub mod Campaign { self.end_time.read() } - fn upgrade(ref self: ContractState, impl_hash: ClassHash) { - assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); - assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array> { + if get_caller_address() != self.factory.read() { + return Result::Err(array![Errors::NOT_FACTORY]); + } + if impl_hash.is_zero() { + return Result::Err(array![Errors::CLASS_HASH_ZERO]); + } - starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); + starknet::syscalls::replace_class_syscall(impl_hash)?; self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); + + Result::Ok(()) } fn withdraw(ref self: ContractState) { diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 2d1305a7..9d573038 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -47,6 +47,7 @@ pub mod CampaignFactory { #[flat] OwnableEvent: ownable_component::Event, ClassHashUpdated: ClassHashUpdated, + ClassHashUpdateFailed: ClassHashUpdateFailed, CampaignCreated: CampaignCreated, } @@ -55,6 +56,12 @@ pub mod CampaignFactory { pub new_class_hash: ClassHash, } + #[derive(Drop, starknet::Event)] + pub struct ClassHashUpdateFailed { + pub campaign: ContractAddress, + pub errors: Array + } + #[derive(Drop, starknet::Event)] pub struct CampaignCreated { pub caller: ContractAddress, @@ -84,12 +91,10 @@ pub mod CampaignFactory { duration: u64 ) -> ContractAddress { let caller = get_caller_address(); - let this = get_contract_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((caller, title, description, target), duration, this) - .serialize(ref constructor_calldata); + ((caller, title, description, target), duration).serialize(ref constructor_calldata); // Contract deployment let (contract_address, _) = deploy_syscall( @@ -123,7 +128,16 @@ pub mod CampaignFactory { while let Option::Some(campaign) = campaigns .get(i) .unwrap_syscall() { - campaign.upgrade(new_class_hash); + if let Result::Err(errors) = campaign.upgrade(new_class_hash) { + self + .emit( + Event::ClassHashUpdateFailed( + ClassHashUpdateFailed { + campaign: campaign.contract_address, errors + } + ) + ) + } i += 1; }; diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index bb585f17..0ead7cd0 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1 +1,2 @@ mod factory; +mod campaign; diff --git a/listings/applications/advanced_factory/src/tests/campaign.cairo b/listings/applications/advanced_factory/src/tests/campaign.cairo new file mode 100644 index 00000000..2f32e70b --- /dev/null +++ b/listings/applications/advanced_factory/src/tests/campaign.cairo @@ -0,0 +1,96 @@ +use core::traits::TryInto; +use core::clone::Clone; +use core::result::ResultTrait; +use starknet::{ + ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address +}; +use snforge_std::{ + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash +}; + +use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; + +/// Deploy a campaign contract with the provided data +fn deploy_with( + title: ByteArray, description: ByteArray, target: u256, duration: u64, +) -> ICampaignDispatcher { + let owner = contract_address_const::<'owner'>(); + let mut calldata: Array:: = array![]; + ((owner, title, description, target), duration).serialize(ref calldata); + + let contract = declare("Campaign").unwrap(); + let contract_address = contract.precalculate_address(@calldata); + let factory = contract_address_const::<'factory'>(); + start_cheat_caller_address(contract_address, factory); + + contract.deploy(@calldata).unwrap(); + + stop_cheat_caller_address(contract_address); + + ICampaignDispatcher { contract_address } +} + +/// Deploy a campaign contract with default data +fn deploy() -> ICampaignDispatcher { + deploy_with("title 1", "description 1", 10000, 60) +} + +#[test] +fn test_deploy() { + let campaign = deploy(); + + assert_eq!(campaign.get_title(), "title 1"); + assert_eq!(campaign.get_description(), "description 1"); + assert_eq!(campaign.get_target(), 10000); + assert_eq!(campaign.get_end_time(), get_block_timestamp() + 60); + + let owner: ContractAddress = contract_address_const::<'owner'>(); + let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; + assert_eq!(campaign_ownable.owner(), owner); +} + +#[test] +fn test_upgrade_class_hash() { + let campaign = deploy(); + + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + + let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + + let factory = contract_address_const::<'factory'>(); + start_cheat_caller_address(campaign.contract_address, factory); + + if let Result::Err(errs) = campaign.upgrade(new_class_hash) { + panic(errs) + } + + assert_eq!(get_class_hash(campaign.contract_address), new_class_hash); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ) + ] + ); +} + +#[test] +#[should_panic(expected: 'Caller not factory')] +fn test_upgrade_class_hash_fail() { + let campaign = deploy(); + + let new_class_hash = declare("Campaign_Updated").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) + } +} + From 05ab75ff3cfcf07e1a6e294dda24cb6ba4df5cc9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 12:38:22 +0200 Subject: [PATCH 052/116] add status to campaign --- .../advanced_factory/src/campaign.cairo | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index e5dd45af..fa80d233 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -50,6 +50,15 @@ pub mod Campaign { title: ByteArray, description: ByteArray, total_contributions: u256, + status: Status + } + + #[derive(Drop, PartialEq)] + pub enum Status { + ACTIVE, + SUCCESSFUL, + UNSUCCESSFUL, + CLOSED } #[event] @@ -131,13 +140,14 @@ pub mod Campaign { self.end_time.write(get_block_timestamp() + duration); self.factory.write(get_caller_address()); self.ownable._init(owner); + self.status.write(Status::ACTIVE) } #[abi(embed_v0)] impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self.ownable._assert_only_owner(); - assert(!self._is_active(), Errors::STILL_ACTIVE); + assert(self._is_active(), Errors::ENDED); assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); @@ -146,6 +156,8 @@ pub mod Campaign { let amount = eth_token.balance_of(this); assert(amount > 0, Errors::ZERO_FUNDS); + self.status.write(Status::SUCCESSFUL); + // 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 @@ -155,6 +167,7 @@ pub mod Campaign { self.emit(Event::Claimed(Claimed { amount })); } + fn contribute(ref self: ContractState, amount: u256) { assert(self._is_active(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -206,8 +219,13 @@ pub mod Campaign { } fn withdraw(ref self: ContractState) { - assert(!self._is_active(), Errors::STILL_ACTIVE); - assert(!self._is_target_reached(), Errors::TARGET_ALREADY_REACHED); + if self._is_expired() && !self._is_target_reached() && self._is_active() { + self.status.write(Status::UNSUCCESSFUL); + } + assert( + self.status.read() == Status::UNSUCCESSFUL || self.status.read() == Status::CLOSED, + Errors::STILL_ACTIVE + ); assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); @@ -224,10 +242,14 @@ pub mod Campaign { #[generate_trait] impl CampaignInternalImpl of CampaignInternalTrait { - fn _is_active(self: @ContractState) -> bool { + 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() } From 2ca2ede3dca9c82453ddb43bfe9e8f684443f2c6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 12:57:48 +0200 Subject: [PATCH 053/116] add close fn --- .../advanced_factory/src/campaign.cairo | 24 ++++++++++++++++++- .../src/campaign/contributions.cairo | 6 ++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index fa80d233..36bfcfd9 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -6,6 +6,7 @@ use starknet::{ClassHash, ContractAddress}; #[starknet::interface] pub trait ICampaign { fn claim(ref self: TContractState); + fn close(ref self: TContractState, reason: ByteArray); fn contribute(ref self: TContractState, amount: u256); fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_description(self: @TContractState) -> ByteArray; @@ -69,6 +70,7 @@ pub mod Campaign { ContributableEvent: contributable_component::Event, ContributionMade: ContributionMade, Claimed: Claimed, + Closed: Closed, Upgraded: Upgraded, Withdrawn: Withdrawn, } @@ -85,6 +87,11 @@ pub mod Campaign { pub amount: u256, } + #[derive(Drop, starknet::Event)] + pub struct Closed { + pub reason: ByteArray, + } + #[derive(Drop, starknet::Event)] pub struct Upgraded { pub implementation: ClassHash @@ -167,6 +174,21 @@ pub mod Campaign { self.emit(Event::Claimed(Claimed { amount })); } + fn close(ref self: ContractState, reason: ByteArray) { + self.ownable._assert_only_owner(); + assert(self._is_active(), Errors::ENDED); + + self.status.write(Status::CLOSED); + + let mut contributions = self.get_contributions(); + while let Option::Some((contributor, amt)) = contributions + .pop_front() { + self.contributions.remove(contributor); + self.eth_token.read().transfer(contributor, amt); + }; + + self.emit(Event::Closed(Closed { reason })); + } fn contribute(ref self: ContractState, amount: u256) { assert(self._is_active(), Errors::ENDED); @@ -229,7 +251,7 @@ pub mod Campaign { assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); - let amount = self.contributions.withhold(contributor); + 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 diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index 51704b5f..847dbca2 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -5,7 +5,7 @@ pub trait IContributable { fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); fn get(self: @TContractState, contributor: ContractAddress) -> u256; fn get_contributions_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; - fn withhold(ref self: TContractState, contributor: ContractAddress) -> u256; + fn remove(ref self: TContractState, contributor: ContractAddress) -> u256; } #[starknet::component] @@ -73,9 +73,7 @@ pub mod contributable_component { result } - fn withhold( - ref self: ComponentState, contributor: ContractAddress - ) -> u256 { + fn remove(ref self: ComponentState, contributor: ContractAddress) -> u256 { let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); if let Option::Some((amt, idx)) = amt_idx_opt { self.contributor_to_amt_idx.write(contributor, Option::None); From cc2705ad84f8cbd3f0b548671dbae9e928bf0099 Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 13:05:59 +0200 Subject: [PATCH 054/116] pass desired donation token in ctor --- .../advanced_factory/src/campaign.cairo | 22 +++++++++---------- .../advanced_factory/src/factory.cairo | 9 +++++--- .../advanced_factory/src/tests/campaign.cairo | 6 ++--- .../advanced_factory/src/tests/factory.cairo | 8 ++++--- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 36bfcfd9..281c3360 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -45,7 +45,7 @@ pub mod Campaign { #[substorage(v0)] contributions: contributable_component::Storage, end_time: u64, - eth_token: IERC20Dispatcher, + token: IERC20Dispatcher, factory: ContractAddress, target: u256, title: ByteArray, @@ -54,7 +54,7 @@ pub mod Campaign { status: Status } - #[derive(Drop, PartialEq)] + #[derive(Drop, PartialEq, starknet::Store)] pub enum Status { ACTIVE, SUCCESSFUL, @@ -130,16 +130,14 @@ pub mod Campaign { description: ByteArray, target: u256, duration: u64, + token_address: ContractAddress ) { assert(owner.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); assert(duration > 0, Errors::ZERO_DURATION); - let eth_address = contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >(); - self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); + self.token.write(IERC20Dispatcher { contract_address: token_address }); self.title.write(title); self.target.write(target); @@ -158,9 +156,9 @@ pub mod Campaign { assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); - let eth_token = self.eth_token.read(); + let token = self.token.read(); - let amount = eth_token.balance_of(this); + let amount = token.balance_of(this); assert(amount > 0, Errors::ZERO_FUNDS); self.status.write(Status::SUCCESSFUL); @@ -168,7 +166,7 @@ pub mod Campaign { // 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 = eth_token.transfer(get_caller_address(), amount); + let success = token.transfer(get_caller_address(), amount); assert(success, Errors::TRANSFER_FAILED); self.emit(Event::Claimed(Claimed { amount })); @@ -184,7 +182,7 @@ pub mod Campaign { while let Option::Some((contributor, amt)) = contributions .pop_front() { self.contributions.remove(contributor); - self.eth_token.read().transfer(contributor, amt); + self.token.read().transfer(contributor, amt); }; self.emit(Event::Closed(Closed { reason })); @@ -196,7 +194,7 @@ pub mod Campaign { let contributor = get_caller_address(); let this = get_contract_address(); - let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); + let success = self.token.read().transfer_from(contributor, this, amount.into()); assert(success, Errors::TRANSFER_FAILED); self.contributions.add(contributor, amount); @@ -256,7 +254,7 @@ pub mod Campaign { // 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 - self.eth_token.read().transfer(contributor, amount); + self.token.read().transfer(contributor, amount); self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); } diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/advanced_factory/src/factory.cairo index 9d573038..14f7d987 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/advanced_factory/src/factory.cairo @@ -8,7 +8,8 @@ pub trait ICampaignFactory { title: ByteArray, description: ByteArray, target: u256, - duration: u64 + duration: 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); @@ -88,13 +89,15 @@ pub mod CampaignFactory { title: ByteArray, description: ByteArray, target: u256, - duration: u64 + duration: u64, + token_address: ContractAddress, ) -> ContractAddress { let caller = get_caller_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((caller, title, description, target), duration).serialize(ref constructor_calldata); + ((caller, title, description, target), duration, token_address) + .serialize(ref constructor_calldata); // Contract deployment let (contract_address, _) = deploy_syscall( diff --git a/listings/applications/advanced_factory/src/tests/campaign.cairo b/listings/applications/advanced_factory/src/tests/campaign.cairo index 2f32e70b..dd017c9a 100644 --- a/listings/applications/advanced_factory/src/tests/campaign.cairo +++ b/listings/applications/advanced_factory/src/tests/campaign.cairo @@ -14,11 +14,11 @@ use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign contract with the provided data fn deploy_with( - title: ByteArray, description: ByteArray, target: u256, duration: u64, + title: ByteArray, description: ByteArray, target: u256, duration: u64, token: ContractAddress ) -> ICampaignDispatcher { let owner = contract_address_const::<'owner'>(); let mut calldata: Array:: = array![]; - ((owner, title, description, target), duration).serialize(ref calldata); + ((owner, title, description, target), duration, token).serialize(ref calldata); let contract = declare("Campaign").unwrap(); let contract_address = contract.precalculate_address(@calldata); @@ -34,7 +34,7 @@ fn deploy_with( /// Deploy a campaign contract with default data fn deploy() -> ICampaignDispatcher { - deploy_with("title 1", "description 1", 10000, 60) + deploy_with("title 1", "description 1", 10000, 60, contract_address_const::<'token'>()) } #[test] diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/advanced_factory/src/tests/factory.cairo index d1297416..59b284b7 100644 --- a/listings/applications/advanced_factory/src/tests/factory.cairo +++ b/listings/applications/advanced_factory/src/tests/factory.cairo @@ -62,9 +62,10 @@ fn test_deploy_campaign() { let description: ByteArray = "Some description"; let target: u256 = 10000; let duration: u64 = 60; + let token = contract_address_const::<'token'>(); let campaign_address = factory - .create_campaign(title.clone(), description.clone(), target, duration); + .create_campaign(title.clone(), description.clone(), target, duration, token); let campaign = ICampaignDispatcher { contract_address: campaign_address }; assert_eq!(campaign.get_title(), title); @@ -94,8 +95,9 @@ fn test_deploy_campaign() { fn test_update_campaign_class_hash() { let factory = deploy_factory(); - let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60); - let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120); + let token = contract_address_const::<'token'>(); + let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60, token); + let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120, token); assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_1)); assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_2)); From 9280f9a4ebd030856d9cf92f26f8a1c95252708a Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 13:15:04 +0200 Subject: [PATCH 055/116] merge all getters into get_details --- .../advanced_factory/src/campaign.cairo | 54 ++++++++++--------- .../advanced_factory/src/tests/campaign.cairo | 12 +++-- .../advanced_factory/src/tests/factory.cairo | 12 +++-- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 281c3360..46242833 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -3,16 +3,31 @@ mod contributions; // ANCHOR: contract use starknet::{ClassHash, ContractAddress}; +#[derive(Drop, Debug, Serde, PartialEq, starknet::Store)] +pub enum Status { + ACTIVE, + SUCCESSFUL, + UNSUCCESSFUL, + CLOSED +} + +#[derive(Drop, Serde)] +pub struct Details { + pub target: u256, + pub title: ByteArray, + pub end_time: u64, + pub description: ByteArray, + pub status: Status, + pub token: ContractAddress, +} + #[starknet::interface] pub trait ICampaign { fn claim(ref self: TContractState); fn close(ref self: TContractState, reason: ByteArray); fn contribute(ref self: TContractState, amount: u256); fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; - fn get_description(self: @TContractState) -> ByteArray; - fn get_title(self: @TContractState) -> ByteArray; - fn get_target(self: @TContractState) -> u256; - fn get_end_time(self: @TContractState) -> u64; + fn get_details(self: @TContractState) -> Details; fn upgrade(ref self: TContractState, impl_hash: ClassHash) -> Result<(), Array>; fn withdraw(ref self: TContractState); } @@ -28,6 +43,7 @@ pub mod Campaign { }; use components::ownable::ownable_component; use super::contributions::contributable_component; + use super::{Details, Status}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: contributable_component, storage: contributions, event: ContributableEvent); @@ -54,13 +70,6 @@ pub mod Campaign { status: Status } - #[derive(Drop, PartialEq, starknet::Store)] - pub enum Status { - ACTIVE, - SUCCESSFUL, - UNSUCCESSFUL, - CLOSED - } #[event] #[derive(Drop, starknet::Event)] @@ -207,20 +216,15 @@ pub mod Campaign { self.contributions.get_contributions_as_arr() } - fn get_title(self: @ContractState) -> ByteArray { - self.title.read() - } - - fn get_description(self: @ContractState) -> ByteArray { - self.description.read() - } - - fn get_target(self: @ContractState) -> u256 { - self.target.read() - } - - fn get_end_time(self: @ContractState) -> u64 { - self.end_time.read() + fn get_details(self: @ContractState) -> Details { + Details { + 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 + } } fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array> { diff --git a/listings/applications/advanced_factory/src/tests/campaign.cairo b/listings/applications/advanced_factory/src/tests/campaign.cairo index dd017c9a..bebe8073 100644 --- a/listings/applications/advanced_factory/src/tests/campaign.cairo +++ b/listings/applications/advanced_factory/src/tests/campaign.cairo @@ -10,6 +10,7 @@ use snforge_std::{ }; use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use advanced_factory::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign contract with the provided data @@ -41,10 +42,13 @@ fn deploy() -> ICampaignDispatcher { fn test_deploy() { let campaign = deploy(); - assert_eq!(campaign.get_title(), "title 1"); - assert_eq!(campaign.get_description(), "description 1"); - assert_eq!(campaign.get_target(), 10000); - assert_eq!(campaign.get_end_time(), get_block_timestamp() + 60); + 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.status, Status::ACTIVE); + assert_eq!(details.token, contract_address_const::<'token'>()); let owner: ContractAddress = contract_address_const::<'owner'>(); let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/advanced_factory/src/tests/factory.cairo index 59b284b7..9c7a2fdd 100644 --- a/listings/applications/advanced_factory/src/tests/factory.cairo +++ b/listings/applications/advanced_factory/src/tests/factory.cairo @@ -14,6 +14,7 @@ use snforge_std::{ // Define a target contract to deploy use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use advanced_factory::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; @@ -68,10 +69,13 @@ fn test_deploy_campaign() { .create_campaign(title.clone(), description.clone(), target, duration, token); let campaign = ICampaignDispatcher { contract_address: campaign_address }; - assert_eq!(campaign.get_title(), title); - assert_eq!(campaign.get_description(), description); - assert_eq!(campaign.get_target(), target); - assert_eq!(campaign.get_end_time(), get_block_timestamp() + duration); + let details = campaign.get_details(); + assert_eq!(details.title, title); + assert_eq!(details.description, description); + assert_eq!(details.target, target); + assert_eq!(details.end_time, get_block_timestamp() + duration); + assert_eq!(details.status, Status::ACTIVE); + assert_eq!(details.token, token); let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; assert_eq!(campaign_ownable.owner(), campaign_owner); From a99aacabba90d1bd03e1fb4822ad640a1ce5bf2a Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 11 Jun 2024 13:21:49 +0200 Subject: [PATCH 056/116] return total_contributions in details --- listings/applications/advanced_factory/src/campaign.cairo | 4 +++- .../applications/advanced_factory/src/tests/campaign.cairo | 1 + .../applications/advanced_factory/src/tests/factory.cairo | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/advanced_factory/src/campaign.cairo index 46242833..c309f9bf 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/advanced_factory/src/campaign.cairo @@ -19,6 +19,7 @@ pub struct Details { pub description: ByteArray, pub status: Status, pub token: ContractAddress, + pub total_contributions: u256, } #[starknet::interface] @@ -223,7 +224,8 @@ pub mod Campaign { target: self.target.read(), end_time: self.end_time.read(), status: self.status.read(), - token: self.token.read().contract_address + token: self.token.read().contract_address, + total_contributions: self.total_contributions.read(), } } diff --git a/listings/applications/advanced_factory/src/tests/campaign.cairo b/listings/applications/advanced_factory/src/tests/campaign.cairo index bebe8073..5420a4b4 100644 --- a/listings/applications/advanced_factory/src/tests/campaign.cairo +++ b/listings/applications/advanced_factory/src/tests/campaign.cairo @@ -49,6 +49,7 @@ fn test_deploy() { assert_eq!(details.end_time, get_block_timestamp() + 60); assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, contract_address_const::<'token'>()); + assert_eq!(details.total_contributions, 0); let owner: ContractAddress = contract_address_const::<'owner'>(); let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/advanced_factory/src/tests/factory.cairo index 9c7a2fdd..5a72afee 100644 --- a/listings/applications/advanced_factory/src/tests/factory.cairo +++ b/listings/applications/advanced_factory/src/tests/factory.cairo @@ -76,6 +76,7 @@ fn test_deploy_campaign() { assert_eq!(details.end_time, get_block_timestamp() + duration); assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, token); + assert_eq!(details.total_contributions, 0); let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; assert_eq!(campaign_ownable.owner(), campaign_owner); From 47a9d7239f8dd8dcb37ac70e4b3a34267e5c8224 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 07:59:50 +0200 Subject: [PATCH 057/116] remove rev version from alexandria dep --- Scarb.lock | 2 +- Scarb.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Scarb.lock b/Scarb.lock index ce5fb820..fac3df11 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "alexandria_storage" version = "0.3.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" +source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" [[package]] name = "bytearray" diff --git a/Scarb.toml b/Scarb.toml index 1f1df74f..5f4c01d9 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -16,7 +16,7 @@ starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" } -alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" } [workspace.package] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." From 8d68d86cf7db89fa8edd944d3496e3e13a911afd Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 08:04:36 +0200 Subject: [PATCH 058/116] verbose names --- .../applications/advanced_factory/error.log | 2 + .../src/campaign/contributions.cairo | 66 ++++++++++--------- 2 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 listings/applications/advanced_factory/error.log diff --git a/listings/applications/advanced_factory/error.log b/listings/applications/advanced_factory/error.log new file mode 100644 index 00000000..8e820339 --- /dev/null +++ b/listings/applications/advanced_factory/error.log @@ -0,0 +1,2 @@ + Compiling advanced_factory v0.1.0 (/home/nenad/repos/work/StarknetByExample/listings/applications/advanced_factory/Scarb.toml) + Finished release target(s) in 9 seconds diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/advanced_factory/src/campaign/contributions.cairo index 847dbca2..d76316bb 100644 --- a/listings/applications/advanced_factory/src/campaign/contributions.cairo +++ b/listings/applications/advanced_factory/src/campaign/contributions.cairo @@ -16,8 +16,8 @@ pub mod contributable_component { #[storage] struct Storage { - idx_to_contributor: LegacyMap, - contributor_to_amt_idx: LegacyMap>, + index_to_contributor: LegacyMap, + contributor_to_amount_index: LegacyMap>, contributor_count: u32, } @@ -32,23 +32,25 @@ pub mod contributable_component { fn add( ref self: ComponentState, contributor: ContractAddress, amount: u256 ) { - let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); - if let Option::Some((old_amount, idx)) = amt_idx_opt { + let amount_index_option: Option<(u256, u32)> = self + .contributor_to_amount_index + .read(contributor); + if let Option::Some((old_amount, index)) = amount_index_option { self - .contributor_to_amt_idx - .write(contributor, Option::Some((old_amount + amount, idx))); + .contributor_to_amount_index + .write(contributor, Option::Some((old_amount + amount, index))); } else { - let idx = self.contributor_count.read(); - self.idx_to_contributor.write(idx, contributor); - self.contributor_to_amt_idx.write(contributor, Option::Some((amount, idx))); - self.contributor_count.write(idx + 1); + let index = self.contributor_count.read(); + self.index_to_contributor.write(index, contributor); + self.contributor_to_amount_index.write(contributor, Option::Some((amount, index))); + self.contributor_count.write(index + 1); } } fn get(self: @ComponentState, contributor: ContractAddress) -> u256 { - let val: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); + let val: Option<(u256, u32)> = self.contributor_to_amount_index.read(contributor); match val { - Option::Some((amt, _)) => amt, + Option::Some((amount, _)) => amount, Option::None => 0, } } @@ -61,42 +63,46 @@ pub mod contributable_component { let mut index = self.contributor_count.read(); while index != 0 { index -= 1; - let contr = self.idx_to_contributor.read(index); - let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contr); - let amount = match amt_idx_opt { - Option::Some((amt, _)) => amt, + let contributor = self.index_to_contributor.read(index); + let amount_index_option: Option<(u256, u32)> = self + .contributor_to_amount_index + .read(contributor); + let amount = match amount_index_option { + Option::Some((amount, _)) => amount, Option::None => 0 }; - result.append((contr, amount)); + result.append((contributor, amount)); }; result } fn remove(ref self: ComponentState, contributor: ContractAddress) -> u256 { - let amt_idx_opt: Option<(u256, u32)> = self.contributor_to_amt_idx.read(contributor); - if let Option::Some((amt, idx)) = amt_idx_opt { - self.contributor_to_amt_idx.write(contributor, Option::None); + let amount_index_option: Option<(u256, u32)> = self + .contributor_to_amount_index + .read(contributor); + if let Option::Some((amount, index)) = amount_index_option { + self.contributor_to_amount_index.write(contributor, Option::None); let contributor_count = self.contributor_count.read() - 1; self.contributor_count.write(contributor_count); if contributor_count != 0 { - let last_contributor = self.idx_to_contributor.read(contributor_count); - let last_amt_idx: Option<(u256, u32)> = self - .contributor_to_amt_idx + let last_contributor = self.index_to_contributor.read(contributor_count); + let last_amount_index: Option<(u256, u32)> = self + .contributor_to_amount_index .read(last_contributor); - let last_amt = match last_amt_idx { - Option::Some((l_a, _)) => l_a, + let last_amount = match last_amount_index { + Option::Some((last_amount, _)) => last_amount, Option::None => 0 }; self - .contributor_to_amt_idx - .write(last_contributor, Option::Some((last_amt, idx))); - self.idx_to_contributor.write(idx, last_contributor); + .contributor_to_amount_index + .write(last_contributor, Option::Some((last_amount, index))); + self.index_to_contributor.write(index, last_contributor); } - self.idx_to_contributor.write(contributor_count, Zero::zero()); + self.index_to_contributor.write(contributor_count, Zero::zero()); - amt + amount } else { 0 } From cd82c991dec2188907f5cbad6b555e4f78a8af8c Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 08:41:26 +0200 Subject: [PATCH 059/116] reorg. folder structure --- Scarb.lock | 29 ++- .../applications/advanced_factory/error.log | 2 - .../src/campaign_updated.cairo | 177 ------------------ .../advanced_factory/src/lib.cairo | 6 - .../advanced_factory/src/tests.cairo | 2 - .../{advanced_factory => campaign}/.gitignore | 0 .../{advanced_factory => campaign}/Scarb.toml | 5 +- .../src/campaign.cairo | 2 +- .../src/campaign/contributions.cairo | 0 .../campaign/src/campaign_upgrade.cairo | 8 + listings/applications/campaign/src/lib.cairo | 5 + .../src/tests.cairo} | 10 +- .../applications/campaign_factory/.gitignore | 2 + .../applications/campaign_factory/Scarb.toml | 18 ++ .../src/campaign_upgrade.cairo | 8 + .../src/contract.cairo} | 2 +- .../campaign_factory/src/lib.cairo | 5 + .../src/tests.cairo} | 9 +- src/applications/crowdfunding/campaign.md | 2 +- src/applications/crowdfunding/factory.md | 2 +- 20 files changed, 81 insertions(+), 213 deletions(-) delete mode 100644 listings/applications/advanced_factory/error.log delete mode 100644 listings/applications/advanced_factory/src/campaign_updated.cairo delete mode 100644 listings/applications/advanced_factory/src/lib.cairo delete mode 100644 listings/applications/advanced_factory/src/tests.cairo rename listings/applications/{advanced_factory => campaign}/.gitignore (100%) rename listings/applications/{advanced_factory => campaign}/Scarb.toml (80%) rename listings/applications/{advanced_factory => campaign}/src/campaign.cairo (99%) rename listings/applications/{advanced_factory => campaign}/src/campaign/contributions.cairo (100%) create mode 100644 listings/applications/campaign/src/campaign_upgrade.cairo create mode 100644 listings/applications/campaign/src/lib.cairo rename listings/applications/{advanced_factory/src/tests/campaign.cairo => campaign/src/tests.cairo} (90%) create mode 100644 listings/applications/campaign_factory/.gitignore create mode 100644 listings/applications/campaign_factory/Scarb.toml create mode 100644 listings/applications/campaign_factory/src/campaign_upgrade.cairo rename listings/applications/{advanced_factory/src/factory.cairo => campaign_factory/src/contract.cairo} (98%) create mode 100644 listings/applications/campaign_factory/src/lib.cairo rename listings/applications/{advanced_factory/src/tests/factory.cairo => campaign_factory/src/tests.cairo} (95%) diff --git a/Scarb.lock b/Scarb.lock index fac3df11..91070e94 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,16 +1,6 @@ # Code generated by scarb DO NOT EDIT. version = 1 -[[package]] -name = "advanced_factory" -version = "0.1.0" -dependencies = [ - "alexandria_storage", - "components", - "openzeppelin", - "snforge_std", -] - [[package]] name = "alexandria_storage" version = "0.3.0" @@ -28,6 +18,25 @@ version = "0.1.0" name = "calling_other_contracts" version = "0.1.0" +[[package]] +name = "campaign" +version = "0.1.0" +dependencies = [ + "components", + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "campaign_factory" +version = "0.1.0" +dependencies = [ + "alexandria_storage", + "campaign", + "components", + "snforge_std", +] + [[package]] name = "components" version = "0.1.0" diff --git a/listings/applications/advanced_factory/error.log b/listings/applications/advanced_factory/error.log deleted file mode 100644 index 8e820339..00000000 --- a/listings/applications/advanced_factory/error.log +++ /dev/null @@ -1,2 +0,0 @@ - Compiling advanced_factory v0.1.0 (/home/nenad/repos/work/StarknetByExample/listings/applications/advanced_factory/Scarb.toml) - Finished release target(s) in 9 seconds diff --git a/listings/applications/advanced_factory/src/campaign_updated.cairo b/listings/applications/advanced_factory/src/campaign_updated.cairo deleted file mode 100644 index ed16adae..00000000 --- a/listings/applications/advanced_factory/src/campaign_updated.cairo +++ /dev/null @@ -1,177 +0,0 @@ -use starknet::ClassHash; - -#[starknet::interface] -pub trait ICampaign { - fn claim(ref self: TContractState); - fn contribute(ref self: TContractState, amount: u256); - fn get_description(self: @TContractState) -> ByteArray; - fn get_title(self: @TContractState) -> ByteArray; - fn get_target(self: @TContractState) -> u256; - fn get_end_time(self: @TContractState) -> u64; - fn upgrade(ref self: TContractState, impl_hash: ClassHash); -} - -#[starknet::contract] -pub mod Campaign_Updated { - 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 - }; - use components::ownable::ownable_component; - - component!(path: ownable_component, storage: ownable, event: OwnableEvent); - - #[abi(embed_v0)] - pub impl OwnableImpl = ownable_component::Ownable; - impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - ownable: ownable_component::Storage, - contributions: LegacyMap, - end_time: u64, - eth_token: IERC20Dispatcher, - factory: ContractAddress, - target: u256, - title: ByteArray, - description: ByteArray, - total_contributions: u256, - } - - #[event] - #[derive(Drop, starknet::Event)] - pub enum Event { - #[flat] - OwnableEvent: ownable_component::Event, - ContributionMade: ContributionMade, - Claimed: Claimed, - Upgraded: Upgraded, - } - - #[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 Upgraded { - pub implementation: ClassHash - } - - pub mod Errors { - pub const NOT_FACTORY: felt252 = 'Caller not factory'; - pub const INACTIVE: felt252 = 'Campaign already ended'; - pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; - 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 CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; - pub const FACTORY_ZERO: felt252 = 'Factory address cannot be zero'; - pub const CREATOR_ZERO: felt252 = 'Creator address cannot be zero'; - } - - #[constructor] - fn constructor( - ref self: ContractState, - creator: ContractAddress, - title: ByteArray, - description: ByteArray, - target: u256, - duration: u64, - factory: ContractAddress - ) { - assert(factory.is_non_zero(), Errors::FACTORY_ZERO); - assert(creator.is_non_zero(), Errors::CREATOR_ZERO); - assert(title.len() > 0, Errors::TITLE_EMPTY); - assert(target > 0, Errors::ZERO_TARGET); - assert(duration > 0, Errors::ZERO_DURATION); - - let eth_address = contract_address_const::< - 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 - >(); - self.eth_token.write(IERC20Dispatcher { contract_address: eth_address }); - - self.title.write(title); - self.target.write(target); - self.description.write(description); - self.end_time.write(get_block_timestamp() + duration); - self.factory.write(factory); - self.ownable._init(creator); - } - - #[abi(embed_v0)] - impl Campaign_Updated of super::ICampaign { - fn claim(ref self: ContractState) { - self.ownable._assert_only_owner(); - assert(get_block_timestamp() >= self.end_time.read(), Errors::STILL_ACTIVE); - - let this = get_contract_address(); - let eth_token = self.eth_token.read(); - - let amount = eth_token.balance_of(this); - assert(amount > 0, Errors::ZERO_FUNDS); - - // 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 = eth_token.transfer(get_caller_address(), amount); - assert(success, Errors::TRANSFER_FAILED); - - self.emit(Event::Claimed(Claimed { amount })); - } - - fn contribute(ref self: ContractState, amount: u256) { - assert(get_block_timestamp() < self.end_time.read(), Errors::INACTIVE); - assert(amount > 0, Errors::ZERO_DONATION); - - let contributor = get_caller_address(); - let this = get_contract_address(); - let success = self.eth_token.read().transfer_from(contributor, this, amount.into()); - assert(success, Errors::TRANSFER_FAILED); - - self.contributions.write(contributor, self.contributions.read(contributor) + amount); - self.total_contributions.write(self.total_contributions.read() + amount); - - self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); - } - - fn get_title(self: @ContractState) -> ByteArray { - self.title.read() - } - - fn get_description(self: @ContractState) -> ByteArray { - self.description.read() - } - - fn get_target(self: @ContractState) -> u256 { - self.target.read() - } - - fn get_end_time(self: @ContractState) -> u64 { - self.end_time.read() - } - - fn upgrade(ref self: ContractState, impl_hash: ClassHash) { - assert(get_caller_address() == self.factory.read(), Errors::NOT_FACTORY); - assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - - starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); - - self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); - } - } -} diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo deleted file mode 100644 index 1b70f15b..00000000 --- a/listings/applications/advanced_factory/src/lib.cairo +++ /dev/null @@ -1,6 +0,0 @@ -mod factory; -mod campaign; -mod campaign_updated; - -#[cfg(test)] -mod tests; diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo deleted file mode 100644 index 0ead7cd0..00000000 --- a/listings/applications/advanced_factory/src/tests.cairo +++ /dev/null @@ -1,2 +0,0 @@ -mod factory; -mod campaign; diff --git a/listings/applications/advanced_factory/.gitignore b/listings/applications/campaign/.gitignore similarity index 100% rename from listings/applications/advanced_factory/.gitignore rename to listings/applications/campaign/.gitignore diff --git a/listings/applications/advanced_factory/Scarb.toml b/listings/applications/campaign/Scarb.toml similarity index 80% rename from listings/applications/advanced_factory/Scarb.toml rename to listings/applications/campaign/Scarb.toml index dce4b726..23693e28 100644 --- a/listings/applications/advanced_factory/Scarb.toml +++ b/listings/applications/campaign/Scarb.toml @@ -1,13 +1,14 @@ [package] -name = "advanced_factory" +name = "campaign" version.workspace = true edition = "2023_11" +[lib] + [dependencies] starknet.workspace = true openzeppelin.workspace = true components.workspace = true -alexandria_storage.workspace = true snforge_std.workspace = true [scripts] diff --git a/listings/applications/advanced_factory/src/campaign.cairo b/listings/applications/campaign/src/campaign.cairo similarity index 99% rename from listings/applications/advanced_factory/src/campaign.cairo rename to listings/applications/campaign/src/campaign.cairo index c309f9bf..89281044 100644 --- a/listings/applications/advanced_factory/src/campaign.cairo +++ b/listings/applications/campaign/src/campaign.cairo @@ -1,4 +1,4 @@ -mod contributions; +pub mod contributions; // ANCHOR: contract use starknet::{ClassHash, ContractAddress}; diff --git a/listings/applications/advanced_factory/src/campaign/contributions.cairo b/listings/applications/campaign/src/campaign/contributions.cairo similarity index 100% rename from listings/applications/advanced_factory/src/campaign/contributions.cairo rename to listings/applications/campaign/src/campaign/contributions.cairo diff --git a/listings/applications/campaign/src/campaign_upgrade.cairo b/listings/applications/campaign/src/campaign_upgrade.cairo new file mode 100644 index 00000000..919c40ae --- /dev/null +++ b/listings/applications/campaign/src/campaign_upgrade.cairo @@ -0,0 +1,8 @@ +#[starknet::contract] +pub mod MockContract { + #[storage] + struct Storage {} + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} +} diff --git a/listings/applications/campaign/src/lib.cairo b/listings/applications/campaign/src/lib.cairo new file mode 100644 index 00000000..f268acff --- /dev/null +++ b/listings/applications/campaign/src/lib.cairo @@ -0,0 +1,5 @@ +pub mod campaign; +mod campaign_upgrade; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/advanced_factory/src/tests/campaign.cairo b/listings/applications/campaign/src/tests.cairo similarity index 90% rename from listings/applications/advanced_factory/src/tests/campaign.cairo rename to listings/applications/campaign/src/tests.cairo index 5420a4b4..149dc4ff 100644 --- a/listings/applications/advanced_factory/src/tests/campaign.cairo +++ b/listings/applications/campaign/src/tests.cairo @@ -2,15 +2,15 @@ use core::traits::TryInto; use core::clone::Clone; use core::result::ResultTrait; use starknet::{ - ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address + ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address, }; use snforge_std::{ declare, ContractClass, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash }; -use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use advanced_factory::campaign::Status; +use campaign::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use campaign::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign contract with the provided data @@ -62,7 +62,7 @@ fn test_upgrade_class_hash() { let mut spy = spy_events(SpyOn::One(campaign.contract_address)); - let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + let new_class_hash = declare("MockContract").unwrap().class_hash; let factory = contract_address_const::<'factory'>(); start_cheat_caller_address(campaign.contract_address, factory); @@ -89,7 +89,7 @@ fn test_upgrade_class_hash() { fn test_upgrade_class_hash_fail() { let campaign = deploy(); - let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + let new_class_hash = declare("MockContract").unwrap().class_hash; let owner = contract_address_const::<'owner'>(); start_cheat_caller_address(campaign.contract_address, owner); diff --git a/listings/applications/campaign_factory/.gitignore b/listings/applications/campaign_factory/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/listings/applications/campaign_factory/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/listings/applications/campaign_factory/Scarb.toml b/listings/applications/campaign_factory/Scarb.toml new file mode 100644 index 00000000..3c2e748e --- /dev/null +++ b/listings/applications/campaign_factory/Scarb.toml @@ -0,0 +1,18 @@ +[package] +name = "campaign_factory" +version.workspace = true +edition = "2023_11" + +[dependencies] +starknet.workspace = true +components.workspace = true +alexandria_storage.workspace = true +snforge_std.workspace = true +campaign = { path = "../campaign" } + +[scripts] +test.workspace = true + +[[target.starknet-contract]] +casm = true +build-external-contracts = ["campaign::campaign::Campaign"] diff --git a/listings/applications/campaign_factory/src/campaign_upgrade.cairo b/listings/applications/campaign_factory/src/campaign_upgrade.cairo new file mode 100644 index 00000000..919c40ae --- /dev/null +++ b/listings/applications/campaign_factory/src/campaign_upgrade.cairo @@ -0,0 +1,8 @@ +#[starknet::contract] +pub mod MockContract { + #[storage] + struct Storage {} + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} +} diff --git a/listings/applications/advanced_factory/src/factory.cairo b/listings/applications/campaign_factory/src/contract.cairo similarity index 98% rename from listings/applications/advanced_factory/src/factory.cairo rename to listings/applications/campaign_factory/src/contract.cairo index 14f7d987..efdc2d7c 100644 --- a/listings/applications/advanced_factory/src/factory.cairo +++ b/listings/applications/campaign_factory/src/contract.cairo @@ -23,7 +23,7 @@ pub mod CampaignFactory { get_caller_address, get_contract_address }; use alexandria_storage::list::{List, ListTrait}; - use advanced_factory::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; + use campaign::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; use components::ownable::ownable_component; component!(path: ownable_component, storage: ownable, event: OwnableEvent); diff --git a/listings/applications/campaign_factory/src/lib.cairo b/listings/applications/campaign_factory/src/lib.cairo new file mode 100644 index 00000000..12767290 --- /dev/null +++ b/listings/applications/campaign_factory/src/lib.cairo @@ -0,0 +1,5 @@ +mod contract; +mod campaign_upgrade; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/advanced_factory/src/tests/factory.cairo b/listings/applications/campaign_factory/src/tests.cairo similarity index 95% rename from listings/applications/advanced_factory/src/tests/factory.cairo rename to listings/applications/campaign_factory/src/tests.cairo index 5a72afee..79f5707b 100644 --- a/listings/applications/advanced_factory/src/tests/factory.cairo +++ b/listings/applications/campaign_factory/src/tests.cairo @@ -1,7 +1,7 @@ use core::traits::TryInto; use core::clone::Clone; use core::result::ResultTrait; -use advanced_factory::factory::{ +use campaign_factory::contract::{ CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait }; use starknet::{ @@ -13,8 +13,8 @@ use snforge_std::{ }; // Define a target contract to deploy -use advanced_factory::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use advanced_factory::campaign::Status; +use campaign::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use campaign::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; @@ -112,7 +112,7 @@ fn test_update_campaign_class_hash() { SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) ); - let new_class_hash = declare("Campaign_Updated").unwrap().class_hash; + let new_class_hash = declare("MockContract").unwrap().class_hash; factory.update_campaign_class_hash(new_class_hash); assert_eq!(factory.get_campaign_class_hash(), new_class_hash); @@ -145,4 +145,3 @@ fn test_update_campaign_class_hash() { ] ); } - diff --git a/src/applications/crowdfunding/campaign.md b/src/applications/crowdfunding/campaign.md index 024e3a47..b3f30396 100644 --- a/src/applications/crowdfunding/campaign.md +++ b/src/applications/crowdfunding/campaign.md @@ -3,5 +3,5 @@ This is the Campaign contract. ```rust -{{#include ../../../listings/applications/advanced_factory/src/campaign.cairo:contract}} +{{#include ../../../listings/applications/campaign/src/contract.cairo:contract}} ``` diff --git a/src/applications/crowdfunding/factory.md b/src/applications/crowdfunding/factory.md index 60b052eb..4b793b83 100644 --- a/src/applications/crowdfunding/factory.md +++ b/src/applications/crowdfunding/factory.md @@ -3,5 +3,5 @@ This is the CampaignFactory contract that creates new Campaign contract instances. ```rust -{{#include ../../../listings/applications/advanced_factory/src/factory.cairo:contract}} +{{#include ../../../listings/applications/campaign_factory/src/contract.cairo:contract}} ``` From cf2b9161d80e6d8b9398fd2492e5db91c86e972f Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 11:47:58 +0200 Subject: [PATCH 060/116] add tag to alexandria dep --- Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scarb.toml b/Scarb.toml index 5f4c01d9..1f1df74f 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -16,7 +16,7 @@ starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" } -alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } [workspace.package] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." From 22ac28d8679609220b5d374be64d185fae296bd1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 11:48:26 +0200 Subject: [PATCH 061/116] campaign_upgrade.cairo->mock_upgrade.cairo --- listings/applications/campaign/src/lib.cairo | 2 +- .../campaign/src/{campaign_upgrade.cairo => mock_upgrade.cairo} | 0 listings/applications/campaign_factory/src/lib.cairo | 2 +- .../src/{campaign_upgrade.cairo => mock_upgrade.cairo} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename listings/applications/campaign/src/{campaign_upgrade.cairo => mock_upgrade.cairo} (100%) rename listings/applications/campaign_factory/src/{campaign_upgrade.cairo => mock_upgrade.cairo} (100%) diff --git a/listings/applications/campaign/src/lib.cairo b/listings/applications/campaign/src/lib.cairo index f268acff..3e5429ad 100644 --- a/listings/applications/campaign/src/lib.cairo +++ b/listings/applications/campaign/src/lib.cairo @@ -1,5 +1,5 @@ pub mod campaign; -mod campaign_upgrade; +mod mock_upgrade; #[cfg(test)] mod tests; diff --git a/listings/applications/campaign/src/campaign_upgrade.cairo b/listings/applications/campaign/src/mock_upgrade.cairo similarity index 100% rename from listings/applications/campaign/src/campaign_upgrade.cairo rename to listings/applications/campaign/src/mock_upgrade.cairo diff --git a/listings/applications/campaign_factory/src/lib.cairo b/listings/applications/campaign_factory/src/lib.cairo index 12767290..541355ed 100644 --- a/listings/applications/campaign_factory/src/lib.cairo +++ b/listings/applications/campaign_factory/src/lib.cairo @@ -1,5 +1,5 @@ mod contract; -mod campaign_upgrade; +mod mock_upgrade; #[cfg(test)] mod tests; diff --git a/listings/applications/campaign_factory/src/campaign_upgrade.cairo b/listings/applications/campaign_factory/src/mock_upgrade.cairo similarity index 100% rename from listings/applications/campaign_factory/src/campaign_upgrade.cairo rename to listings/applications/campaign_factory/src/mock_upgrade.cairo From 38068f8915d3edb7f533bdbf9439d72a2b6aa92f Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 12:01:07 +0200 Subject: [PATCH 062/116] add explicit alexandria rev + make crowdfunding contracts standalone chapters --- Scarb.lock | 40 +++++++++---------- Scarb.toml | 1 + .../{campaign => advanced_factory}/.gitignore | 0 .../Scarb.toml | 6 +-- .../src/contract.cairo | 2 +- .../src/lib.cairo | 0 .../src/mock_upgrade.cairo | 0 .../src/tests.cairo | 6 +-- .../.gitignore | 0 .../{campaign => crowdfunding}/Scarb.toml | 2 +- .../src/campaign.cairo | 0 .../src/campaign/contributions.cairo | 0 .../{campaign => crowdfunding}/src/lib.cairo | 0 .../src/mock_upgrade.cairo | 0 .../src/tests.cairo | 4 +- src/SUMMARY.md | 5 +-- .../factory.md => advanced_factory.md} | 4 +- src/applications/crowdfunding.md | 7 ++++ src/applications/crowdfunding/campaign.md | 7 ---- src/applications/crowdfunding/crowdfunding.md | 1 - 20 files changed, 42 insertions(+), 43 deletions(-) rename listings/applications/{campaign => advanced_factory}/.gitignore (100%) rename listings/applications/{campaign_factory => advanced_factory}/Scarb.toml (66%) rename listings/applications/{campaign_factory => advanced_factory}/src/contract.cairo (98%) rename listings/applications/{campaign_factory => advanced_factory}/src/lib.cairo (100%) rename listings/applications/{campaign => advanced_factory}/src/mock_upgrade.cairo (100%) rename listings/applications/{campaign_factory => advanced_factory}/src/tests.cairo (97%) rename listings/applications/{campaign_factory => crowdfunding}/.gitignore (100%) rename listings/applications/{campaign => crowdfunding}/Scarb.toml (92%) rename listings/applications/{campaign => crowdfunding}/src/campaign.cairo (100%) rename listings/applications/{campaign => crowdfunding}/src/campaign/contributions.cairo (100%) rename listings/applications/{campaign => crowdfunding}/src/lib.cairo (100%) rename listings/applications/{campaign_factory => crowdfunding}/src/mock_upgrade.cairo (100%) rename listings/applications/{campaign => crowdfunding}/src/tests.cairo (96%) rename src/applications/{crowdfunding/factory.md => advanced_factory.md} (55%) create mode 100644 src/applications/crowdfunding.md delete mode 100644 src/applications/crowdfunding/campaign.md delete mode 100644 src/applications/crowdfunding/crowdfunding.md diff --git a/Scarb.lock b/Scarb.lock index 91070e94..a055e8a9 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,10 +1,20 @@ # Code generated by scarb DO NOT EDIT. version = 1 +[[package]] +name = "advanced_factory" +version = "0.1.0" +dependencies = [ + "alexandria_storage", + "components", + "crowdfunding", + "snforge_std", +] + [[package]] name = "alexandria_storage" version = "0.3.0" -source = "git+https://github.com/keep-starknet-strange/alexandria.git#3041887b95cf10f9d3cd8d75326c754b331f9573" +source = "git+https://github.com/keep-starknet-strange/alexandria.git?rev=800f5ad#800f5ad217847b5ded63c0302a444161766ee9d6" [[package]] name = "bytearray" @@ -18,25 +28,6 @@ version = "0.1.0" name = "calling_other_contracts" version = "0.1.0" -[[package]] -name = "campaign" -version = "0.1.0" -dependencies = [ - "components", - "openzeppelin", - "snforge_std", -] - -[[package]] -name = "campaign_factory" -version = "0.1.0" -dependencies = [ - "alexandria_storage", - "campaign", - "components", - "snforge_std", -] - [[package]] name = "components" version = "0.1.0" @@ -63,6 +54,15 @@ version = "0.1.0" name = "counter" version = "0.1.0" +[[package]] +name = "crowdfunding" +version = "0.1.0" +dependencies = [ + "components", + "openzeppelin", + "snforge_std", +] + [[package]] name = "custom_type_serde" version = "0.1.0" diff --git a/Scarb.toml b/Scarb.toml index 1f1df74f..8df8fcb2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -16,6 +16,7 @@ starknet = ">=2.6.4" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" } components = { path = "listings/applications/components" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" } +# The latest Alexandria release supports only Cairo v2.6.0, so using explicit rev that supports Cairo v2.6.3 alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" } [workspace.package] diff --git a/listings/applications/campaign/.gitignore b/listings/applications/advanced_factory/.gitignore similarity index 100% rename from listings/applications/campaign/.gitignore rename to listings/applications/advanced_factory/.gitignore diff --git a/listings/applications/campaign_factory/Scarb.toml b/listings/applications/advanced_factory/Scarb.toml similarity index 66% rename from listings/applications/campaign_factory/Scarb.toml rename to listings/applications/advanced_factory/Scarb.toml index 3c2e748e..5935e01f 100644 --- a/listings/applications/campaign_factory/Scarb.toml +++ b/listings/applications/advanced_factory/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "campaign_factory" +name = "advanced_factory" version.workspace = true edition = "2023_11" @@ -8,11 +8,11 @@ starknet.workspace = true components.workspace = true alexandria_storage.workspace = true snforge_std.workspace = true -campaign = { path = "../campaign" } +crowdfunding = { path = "../crowdfunding" } [scripts] test.workspace = true [[target.starknet-contract]] casm = true -build-external-contracts = ["campaign::campaign::Campaign"] +build-external-contracts = ["crowdfunding::campaign::Campaign"] diff --git a/listings/applications/campaign_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo similarity index 98% rename from listings/applications/campaign_factory/src/contract.cairo rename to listings/applications/advanced_factory/src/contract.cairo index efdc2d7c..62810fda 100644 --- a/listings/applications/campaign_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -23,7 +23,7 @@ pub mod CampaignFactory { get_caller_address, get_contract_address }; use alexandria_storage::list::{List, ListTrait}; - use campaign::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; + use crowdfunding::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; use components::ownable::ownable_component; component!(path: ownable_component, storage: ownable, event: OwnableEvent); diff --git a/listings/applications/campaign_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo similarity index 100% rename from listings/applications/campaign_factory/src/lib.cairo rename to listings/applications/advanced_factory/src/lib.cairo diff --git a/listings/applications/campaign/src/mock_upgrade.cairo b/listings/applications/advanced_factory/src/mock_upgrade.cairo similarity index 100% rename from listings/applications/campaign/src/mock_upgrade.cairo rename to listings/applications/advanced_factory/src/mock_upgrade.cairo diff --git a/listings/applications/campaign_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo similarity index 97% rename from listings/applications/campaign_factory/src/tests.cairo rename to listings/applications/advanced_factory/src/tests.cairo index 79f5707b..d015e006 100644 --- a/listings/applications/campaign_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -1,7 +1,7 @@ use core::traits::TryInto; use core::clone::Clone; use core::result::ResultTrait; -use campaign_factory::contract::{ +use advanced_factory::contract::{ CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait }; use starknet::{ @@ -13,8 +13,8 @@ use snforge_std::{ }; // Define a target contract to deploy -use campaign::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use campaign::campaign::Status; +use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; diff --git a/listings/applications/campaign_factory/.gitignore b/listings/applications/crowdfunding/.gitignore similarity index 100% rename from listings/applications/campaign_factory/.gitignore rename to listings/applications/crowdfunding/.gitignore diff --git a/listings/applications/campaign/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml similarity index 92% rename from listings/applications/campaign/Scarb.toml rename to listings/applications/crowdfunding/Scarb.toml index 23693e28..fed579e7 100644 --- a/listings/applications/campaign/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -1,5 +1,5 @@ [package] -name = "campaign" +name = "crowdfunding" version.workspace = true edition = "2023_11" diff --git a/listings/applications/campaign/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo similarity index 100% rename from listings/applications/campaign/src/campaign.cairo rename to listings/applications/crowdfunding/src/campaign.cairo diff --git a/listings/applications/campaign/src/campaign/contributions.cairo b/listings/applications/crowdfunding/src/campaign/contributions.cairo similarity index 100% rename from listings/applications/campaign/src/campaign/contributions.cairo rename to listings/applications/crowdfunding/src/campaign/contributions.cairo diff --git a/listings/applications/campaign/src/lib.cairo b/listings/applications/crowdfunding/src/lib.cairo similarity index 100% rename from listings/applications/campaign/src/lib.cairo rename to listings/applications/crowdfunding/src/lib.cairo diff --git a/listings/applications/campaign_factory/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo similarity index 100% rename from listings/applications/campaign_factory/src/mock_upgrade.cairo rename to listings/applications/crowdfunding/src/mock_upgrade.cairo diff --git a/listings/applications/campaign/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo similarity index 96% rename from listings/applications/campaign/src/tests.cairo rename to listings/applications/crowdfunding/src/tests.cairo index 149dc4ff..5bd710dc 100644 --- a/listings/applications/campaign/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -9,8 +9,8 @@ use snforge_std::{ stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash }; -use campaign::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use campaign::campaign::Status; +use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; /// Deploy a campaign contract with the provided data diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 240630ef..07551165 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -60,9 +60,8 @@ Summary - [TimeLock](./applications/timelock.md) - [Staking](./applications/staking.md) - [Simple Storage with Starknet-js](./applications/simple_storage_starknetjs.md) -- [Crowdfunding](./applications/crowdfunding/crowdfunding.md) - - [Campaign Contract](./applications/crowdfunding/campaign.md) - - [CampaignFactory Contract](./applications/crowdfunding/factory.md) +- [Crowdfunding Campaign](./applications/crowdfunding.md) +- [AdvancedFactory: Crowdfunding](./applications/advanced_factory.md) diff --git a/src/applications/crowdfunding/factory.md b/src/applications/advanced_factory.md similarity index 55% rename from src/applications/crowdfunding/factory.md rename to src/applications/advanced_factory.md index 4b793b83..14c0bf3d 100644 --- a/src/applications/crowdfunding/factory.md +++ b/src/applications/advanced_factory.md @@ -1,7 +1,7 @@ -# CampaignFactory Contract +# AdvancedFactory: Crowdfunding This is the CampaignFactory contract that creates new Campaign contract instances. ```rust -{{#include ../../../listings/applications/campaign_factory/src/contract.cairo:contract}} +{{#include ../../../listings/applications/advanced_factory/src/contract.cairo:contract}} ``` diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md new file mode 100644 index 00000000..25f1bcf2 --- /dev/null +++ b/src/applications/crowdfunding.md @@ -0,0 +1,7 @@ +# Crowdfunding Campaign + +This is the Crowdfunding Campaign contract. + +```rust +{{#include ../../../listings/applications/crowdfunding/src/contract.cairo:contract}} +``` diff --git a/src/applications/crowdfunding/campaign.md b/src/applications/crowdfunding/campaign.md deleted file mode 100644 index b3f30396..00000000 --- a/src/applications/crowdfunding/campaign.md +++ /dev/null @@ -1,7 +0,0 @@ -# Campaign Contract - -This is the Campaign contract. - -```rust -{{#include ../../../listings/applications/campaign/src/contract.cairo:contract}} -``` diff --git a/src/applications/crowdfunding/crowdfunding.md b/src/applications/crowdfunding/crowdfunding.md deleted file mode 100644 index f7d424c0..00000000 --- a/src/applications/crowdfunding/crowdfunding.md +++ /dev/null @@ -1 +0,0 @@ -# Crowdfunding \ No newline at end of file From a451d907693f79c9fca1e868e9da0ead7aa463e8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 12:26:15 +0200 Subject: [PATCH 063/116] add status pending --- .../advanced_factory/src/tests.cairo | 2 +- .../crowdfunding/src/campaign.cairo | 22 ++++++++++++++++--- .../applications/crowdfunding/src/tests.cairo | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index d015e006..d4371bda 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -74,7 +74,7 @@ fn test_deploy_campaign() { assert_eq!(details.description, description); assert_eq!(details.target, target); assert_eq!(details.end_time, get_block_timestamp() + duration); - assert_eq!(details.status, Status::ACTIVE); + assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, token); assert_eq!(details.total_contributions, 0); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 89281044..7ca23e8b 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -6,9 +6,10 @@ use starknet::{ClassHash, ContractAddress}; #[derive(Drop, Debug, Serde, PartialEq, starknet::Store)] pub enum Status { ACTIVE, + CLOSED, + PENDING, SUCCESSFUL, UNSUCCESSFUL, - CLOSED } #[derive(Drop, Serde)] @@ -29,6 +30,7 @@ pub trait ICampaign { fn contribute(ref self: TContractState, amount: u256); fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; + fn start(ref self: TContractState); fn upgrade(ref self: TContractState, impl_hash: ClassHash) -> Result<(), Array>; fn withdraw(ref self: TContractState); } @@ -71,12 +73,12 @@ pub mod Campaign { status: Status } - #[event] #[derive(Drop, starknet::Event)] pub enum Event { #[flat] OwnableEvent: ownable_component::Event, + Activated: Activated, ContributableEvent: contributable_component::Event, ContributionMade: ContributionMade, Claimed: Claimed, @@ -97,6 +99,9 @@ pub mod Campaign { pub amount: u256, } + #[derive(Drop, starknet::Event)] + pub struct Activated {} + #[derive(Drop, starknet::Event)] pub struct Closed { pub reason: ByteArray, @@ -117,6 +122,7 @@ pub mod Campaign { pub mod Errors { pub const NOT_FACTORY: felt252 = 'Caller not factory'; 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 ZERO_DONATION: felt252 = 'Donation must be > 0'; pub const ZERO_TARGET: felt252 = 'Target must be > 0'; @@ -155,7 +161,7 @@ pub mod Campaign { self.end_time.write(get_block_timestamp() + duration); self.factory.write(get_caller_address()); self.ownable._init(owner); - self.status.write(Status::ACTIVE) + self.status.write(Status::PENDING) } #[abi(embed_v0)] @@ -164,6 +170,7 @@ pub mod Campaign { self.ownable._assert_only_owner(); assert(self._is_active(), Errors::ENDED); assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); + // no need to check end_time, as the owner can prematurely end the campaign let this = get_contract_address(); let token = self.token.read(); @@ -229,6 +236,15 @@ pub mod Campaign { } } + fn start(ref self: ContractState) { + self.ownable._assert_only_owner(); + assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); + + self.status.write(Status::ACTIVE); + + self.emit(Event::Activated(Activated {})); + } + fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array> { if get_caller_address() != self.factory.read() { return Result::Err(array![Errors::NOT_FACTORY]); diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 5bd710dc..52548c2b 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -47,7 +47,7 @@ fn test_deploy() { assert_eq!(details.description, "description 1"); assert_eq!(details.target, 10000); assert_eq!(details.end_time, get_block_timestamp() + 60); - assert_eq!(details.status, Status::ACTIVE); + assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_contributions, 0); From 7c23cfdaaa8ea576406c85a5447b17e7c42f52d4 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 12:30:12 +0200 Subject: [PATCH 064/116] field rename: factory->creator --- listings/applications/crowdfunding/src/campaign.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 7ca23e8b..9659c7ef 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -65,7 +65,7 @@ pub mod Campaign { contributions: contributable_component::Storage, end_time: u64, token: IERC20Dispatcher, - factory: ContractAddress, + creator: ContractAddress, target: u256, title: ByteArray, description: ByteArray, @@ -159,7 +159,7 @@ pub mod Campaign { self.target.write(target); self.description.write(description); self.end_time.write(get_block_timestamp() + duration); - self.factory.write(get_caller_address()); + self.creator.write(get_caller_address()); self.ownable._init(owner); self.status.write(Status::PENDING) } @@ -246,7 +246,7 @@ pub mod Campaign { } fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array> { - if get_caller_address() != self.factory.read() { + if get_caller_address() != self.creator.read() { return Result::Err(array![Errors::NOT_FACTORY]); } if impl_hash.is_zero() { From ec6edb6b7031575931911d8422dd4215bc087fb2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 12:36:48 +0200 Subject: [PATCH 065/116] refund users when upgrading campaign --- .../crowdfunding/src/campaign.cairo | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 9659c7ef..acac0297 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -183,7 +183,8 @@ pub mod Campaign { // 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 = token.transfer(get_caller_address(), amount); + let owner = get_caller_address(); + let success = token.transfer(owner, amount); assert(success, Errors::TRANSFER_FAILED); self.emit(Event::Claimed(Claimed { amount })); @@ -195,12 +196,7 @@ pub mod Campaign { self.status.write(Status::CLOSED); - let mut contributions = self.get_contributions(); - while let Option::Some((contributor, amt)) = contributions - .pop_front() { - self.contributions.remove(contributor); - self.token.read().transfer(contributor, amt); - }; + self._refund_all(); self.emit(Event::Closed(Closed { reason })); } @@ -253,6 +249,8 @@ pub mod Campaign { return Result::Err(array![Errors::CLASS_HASH_ZERO]); } + self._refund_all(); + starknet::syscalls::replace_class_syscall(impl_hash)?; self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); @@ -295,6 +293,15 @@ pub mod Campaign { fn _is_target_reached(self: @ContractState) -> bool { self.total_contributions.read() >= self.target.read() } + + fn _refund_all(ref self: ContractState) { + let mut contributions = self.contributions.get_contributions_as_arr(); + while let Option::Some((contributor, amt)) = contributions + .pop_front() { + self.contributions.remove(contributor); + self.token.read().transfer(contributor, amt); + }; + } } } // ANCHOR_END: contract From eca7eec4e72987fff0e736118e9b3fd6cfd4f68f Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 13:07:37 +0200 Subject: [PATCH 066/116] Make owner the calling address, and creator is the campaign manager --- .../advanced_factory/src/tests.cairo | 9 +++--- .../crowdfunding/src/campaign.cairo | 32 ++++++++++++------- .../applications/crowdfunding/src/tests.cairo | 19 +++++------ 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index d4371bda..31bfca2a 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -56,8 +56,8 @@ fn test_deploy_campaign() { let mut spy = spy_events(SpyOn::One(factory.contract_address)); - let campaign_owner: ContractAddress = contract_address_const::<'campaign_owner'>(); - start_cheat_caller_address(factory.contract_address, campaign_owner); + let campaign_creator: ContractAddress = contract_address_const::<'campaign_creator'>(); + start_cheat_caller_address(factory.contract_address, campaign_creator); let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; @@ -77,9 +77,10 @@ fn test_deploy_campaign() { assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, token); assert_eq!(details.total_contributions, 0); + assert_eq!(details.creator, campaign_creator); let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; - assert_eq!(campaign_ownable.owner(), campaign_owner); + assert_eq!(campaign_ownable.owner(), factory.contract_address); spy .assert_emitted( @@ -88,7 +89,7 @@ fn test_deploy_campaign() { factory.contract_address, CampaignFactory::Event::CampaignCreated( CampaignFactory::CampaignCreated { - caller: campaign_owner, contract_address: campaign_address + caller: campaign_creator, contract_address: campaign_address } ) ) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index acac0297..2a8063f9 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -14,6 +14,7 @@ pub enum Status { #[derive(Drop, Serde)] pub struct Details { + pub creator: ContractAddress, pub target: u256, pub title: ByteArray, pub end_time: u64, @@ -42,7 +43,7 @@ pub mod Campaign { use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; use starknet::{ ClassHash, ContractAddress, SyscallResultTrait, get_block_timestamp, contract_address_const, - get_caller_address, get_contract_address + get_caller_address, get_contract_address, class_hash::class_hash_const }; use components::ownable::ownable_component; use super::contributions::contributable_component; @@ -120,7 +121,7 @@ pub mod Campaign { } pub mod Errors { - pub const NOT_FACTORY: felt252 = 'Caller not factory'; + 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'; @@ -131,7 +132,7 @@ pub mod Campaign { pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const TITLE_EMPTY: felt252 = 'Title empty'; pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; - pub const FACTORY_ZERO: felt252 = 'Factory address cannot be zero'; + 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'; @@ -141,14 +142,14 @@ pub mod Campaign { #[constructor] fn constructor( ref self: ContractState, - owner: ContractAddress, + creator: ContractAddress, title: ByteArray, description: ByteArray, target: u256, duration: u64, token_address: ContractAddress ) { - assert(owner.is_non_zero(), Errors::CREATOR_ZERO); + assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); assert(duration > 0, Errors::ZERO_DURATION); @@ -159,15 +160,15 @@ pub mod Campaign { self.target.write(target); self.description.write(description); self.end_time.write(get_block_timestamp() + duration); - self.creator.write(get_caller_address()); - self.ownable._init(owner); + self.creator.write(creator); + self.ownable._init(get_caller_address()); self.status.write(Status::PENDING) } #[abi(embed_v0)] impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { - self.ownable._assert_only_owner(); + self._assert_only_creator(); assert(self._is_active(), Errors::ENDED); assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); // no need to check end_time, as the owner can prematurely end the campaign @@ -191,7 +192,7 @@ pub mod Campaign { } fn close(ref self: ContractState, reason: ByteArray) { - self.ownable._assert_only_owner(); + self._assert_only_creator(); assert(self._is_active(), Errors::ENDED); self.status.write(Status::CLOSED); @@ -222,6 +223,7 @@ pub mod Campaign { fn get_details(self: @ContractState) -> Details { Details { + creator: self.creator.read(), title: self.title.read(), description: self.description.read(), target: self.target.read(), @@ -233,7 +235,7 @@ pub mod Campaign { } fn start(ref self: ContractState) { - self.ownable._assert_only_owner(); + self._assert_only_creator(); assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); self.status.write(Status::ACTIVE); @@ -242,8 +244,8 @@ pub mod Campaign { } fn upgrade(ref self: ContractState, impl_hash: ClassHash) -> Result<(), Array> { - if get_caller_address() != self.creator.read() { - return Result::Err(array![Errors::NOT_FACTORY]); + if get_caller_address() != self.ownable.owner() { + return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]); } if impl_hash.is_zero() { return Result::Err(array![Errors::CLASS_HASH_ZERO]); @@ -282,6 +284,12 @@ pub mod Campaign { #[generate_trait] impl CampaignInternalImpl 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() } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 52548c2b..a13b9f23 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -17,14 +17,14 @@ use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; fn deploy_with( title: ByteArray, description: ByteArray, target: u256, duration: u64, token: ContractAddress ) -> ICampaignDispatcher { - let owner = contract_address_const::<'owner'>(); + let creator = contract_address_const::<'creator'>(); let mut calldata: Array:: = array![]; - ((owner, title, description, target), duration, token).serialize(ref calldata); + ((creator, title, description, target), duration, token).serialize(ref calldata); let contract = declare("Campaign").unwrap(); let contract_address = contract.precalculate_address(@calldata); - let factory = contract_address_const::<'factory'>(); - start_cheat_caller_address(contract_address, factory); + let owner = contract_address_const::<'owner'>(); + start_cheat_caller_address(contract_address, owner); contract.deploy(@calldata).unwrap(); @@ -50,6 +50,7 @@ fn test_deploy() { assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_contributions, 0); + assert_eq!(details.creator, contract_address_const::<'creator'>()); let owner: ContractAddress = contract_address_const::<'owner'>(); let campaign_ownable = IOwnableDispatcher { contract_address: campaign.contract_address }; @@ -64,8 +65,8 @@ fn test_upgrade_class_hash() { let new_class_hash = declare("MockContract").unwrap().class_hash; - let factory = contract_address_const::<'factory'>(); - start_cheat_caller_address(campaign.contract_address, factory); + 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) @@ -85,14 +86,14 @@ fn test_upgrade_class_hash() { } #[test] -#[should_panic(expected: 'Caller not factory')] +#[should_panic(expected: 'Not owner')] fn test_upgrade_class_hash_fail() { let campaign = deploy(); let new_class_hash = declare("MockContract").unwrap().class_hash; - let owner = contract_address_const::<'owner'>(); - start_cheat_caller_address(campaign.contract_address, owner); + let random_address = contract_address_const::<'random_address'>(); + start_cheat_caller_address(campaign.contract_address, random_address); if let Result::Err(errs) = campaign.upgrade(new_class_hash) { panic(errs) From 3803a6f7d4b62c834fc3ca3dd926d81553cafb02 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 14:02:41 +0200 Subject: [PATCH 067/116] add get_contributor (amount) func --- listings/applications/crowdfunding/src/campaign.cairo | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 2a8063f9..b598084a 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -29,6 +29,7 @@ pub trait ICampaign { fn claim(ref self: TContractState); fn close(ref self: TContractState, reason: ByteArray); fn contribute(ref self: TContractState, amount: u256); + fn get_contribution(self: @TContractState, contributor: ContractAddress) -> u256; fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn start(ref self: TContractState); @@ -217,6 +218,10 @@ pub mod Campaign { 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() } From f9941a3138126527954535e153bed181b274f3c3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 12 Jun 2024 19:45:07 +0200 Subject: [PATCH 068/116] Add successful campaign test --- listings/applications/crowdfunding/Scarb.toml | 3 +- .../crowdfunding/src/campaign.cairo | 21 ++++- .../applications/crowdfunding/src/tests.cairo | 93 +++++++++++++++++++ 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml index fed579e7..e3f947a6 100644 --- a/listings/applications/crowdfunding/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -3,7 +3,7 @@ name = "crowdfunding" version.workspace = true edition = "2023_11" -[lib] +# [lib] [dependencies] starknet.workspace = true @@ -16,3 +16,4 @@ test.workspace = true [[target.starknet-contract]] casm = true +build-external-contracts = ["openzeppelin::presets::erc20::ERC20"] diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index b598084a..b5e7e26f 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -209,7 +209,7 @@ pub mod Campaign { let contributor = get_caller_address(); let this = get_contract_address(); - let success = self.token.read().transfer_from(contributor, this, amount.into()); + let success = self.token.read().transfer_from(contributor, this, amount); assert(success, Errors::TRANSFER_FAILED); self.contributions.add(contributor, amount); @@ -248,6 +248,25 @@ pub mod Campaign { 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 new problems: + /// - 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) -> Result<(), Array> { if get_caller_address() != self.ownable.owner() { return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]); diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index a13b9f23..c60baf0f 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -12,6 +12,9 @@ use snforge_std::{ use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +const ERC20_SUPPLY: u256 = 10000; /// Deploy a campaign contract with the provided data fn deploy_with( @@ -38,6 +41,54 @@ fn deploy() -> ICampaignDispatcher { deploy_with("title 1", "description 1", 10000, 60, contract_address_const::<'token'>()) } +fn deploy_with_token() -> ICampaignDispatcher { + // 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'>(); + + // 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(); + + // 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); + + // deploy the actual Campaign contract + let campaign_dispatcher = deploy_with("title 1", "description 1", 10000, 60, erc20_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); + + // 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 +} + +fn _get_token_dispatcher(campaign: ICampaignDispatcher) -> IERC20Dispatcher { + let token_address = campaign.get_details().token; + IERC20Dispatcher { contract_address: token_address } +} + #[test] fn test_deploy() { let campaign = deploy(); @@ -57,6 +108,48 @@ fn test_deploy() { assert_eq!(campaign_ownable.owner(), owner); } +#[test] +fn test_successful_campaign() { + let campaign = deploy_with_token(); + let token = _get_token_dispatcher(campaign); + + 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'>(); + + start_cheat_caller_address(campaign.contract_address, creator); + campaign.start(); + assert_eq!(campaign.get_details().status, Status::ACTIVE); + + start_cheat_caller_address(campaign.contract_address, contributor_1); + let mut prev_balance = token.balance_of(contributor_1); + campaign.contribute(3000); + assert_eq!(campaign.get_details().total_contributions, 3000); + assert_eq!(campaign.get_contribution(contributor_1), 3000); + assert_eq!(token.balance_of(contributor_1), prev_balance - 3000); + + start_cheat_caller_address(campaign.contract_address, contributor_2); + prev_balance = token.balance_of(contributor_2); + campaign.contribute(500); + assert_eq!(campaign.get_details().total_contributions, 3500); + assert_eq!(campaign.get_contribution(contributor_2), 500); + assert_eq!(token.balance_of(contributor_2), prev_balance - 500); + + start_cheat_caller_address(campaign.contract_address, contributor_3); + prev_balance = token.balance_of(contributor_3); + campaign.contribute(7000); + assert_eq!(campaign.get_details().total_contributions, 10500); + assert_eq!(campaign.get_contribution(contributor_3), 7000); + assert_eq!(token.balance_of(contributor_3), prev_balance - 7000); + + 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); +} + #[test] fn test_upgrade_class_hash() { let campaign = deploy(); From bb304532fc48c98e4df66408948f2d9ed289d03c Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 07:14:30 +0200 Subject: [PATCH 069/116] update comment for upgrade --- listings/applications/crowdfunding/src/campaign.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index b5e7e26f..e2df6757 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -264,9 +264,9 @@ pub mod Campaign { /// - 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 new problems: - /// - 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. + /// - 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) -> Result<(), Array> { if get_caller_address() != self.ownable.owner() { return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]); From 92042e5420d06df082bc3544997a44a6923fe627 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 07:15:10 +0200 Subject: [PATCH 070/116] _refund_all->_withdraw_all --- listings/applications/crowdfunding/src/campaign.cairo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index e2df6757..f55406d9 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -198,7 +198,7 @@ pub mod Campaign { self.status.write(Status::CLOSED); - self._refund_all(); + self._withdraw_all(); self.emit(Event::Closed(Closed { reason })); } @@ -275,7 +275,7 @@ pub mod Campaign { return Result::Err(array![Errors::CLASS_HASH_ZERO]); } - self._refund_all(); + self._withdraw_all(); starknet::syscalls::replace_class_syscall(impl_hash)?; @@ -326,7 +326,7 @@ pub mod Campaign { self.total_contributions.read() >= self.target.read() } - fn _refund_all(ref self: ContractState) { + fn _withdraw_all(ref self: ContractState) { let mut contributions = self.contributions.get_contributions_as_arr(); while let Option::Some((contributor, amt)) = contributions .pop_front() { From 7bcaa0744cca809ca3014e7a50a37c5bca2ffd8f Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 07:43:37 +0200 Subject: [PATCH 071/116] update checks for withdraw --- .../applications/crowdfunding/src/campaign.cairo | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index f55406d9..6587a02b 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -126,13 +126,15 @@ pub mod Campaign { 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 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 CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; 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'; @@ -288,10 +290,10 @@ pub mod Campaign { if self._is_expired() && !self._is_target_reached() && self._is_active() { self.status.write(Status::UNSUCCESSFUL); } - assert( - self.status.read() == Status::UNSUCCESSFUL || self.status.read() == Status::CLOSED, - Errors::STILL_ACTIVE - ); + assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::SUCCESSFUL, Errors::TARGET_ALREADY_REACHED); + 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(); From f6609533b66ed9a46c3fd70343df27f9fd777616 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 08:15:28 +0200 Subject: [PATCH 072/116] rework contribute --- .../crowdfunding/src/campaign.cairo | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 6587a02b..09546a20 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -94,6 +94,7 @@ pub mod Campaign { #[key] pub contributor: ContractAddress, pub amount: u256, + pub status: Status, } #[derive(Drop, starknet::Event)] @@ -128,6 +129,7 @@ pub mod Campaign { 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 UNSUCCESSFUL: felt252 = 'Campaign unsuccessful'; 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'; @@ -172,8 +174,8 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self._assert_only_creator(); - assert(self._is_active(), Errors::ENDED); - assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); + assert(self.status.read() == Status::SUCCESSFUL, Errors::TARGET_NOT_REACHED); + assert(self._is_expired(), Errors::STILL_ACTIVE); // no need to check end_time, as the owner can prematurely end the campaign let this = get_contract_address(); @@ -182,8 +184,6 @@ pub mod Campaign { let amount = token.balance_of(this); assert(amount > 0, Errors::ZERO_FUNDS); - self.status.write(Status::SUCCESSFUL); - // 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 @@ -206,7 +206,9 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - assert(self._is_active(), Errors::ENDED); + assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::CLOSED, Errors::CLOSED); + assert(!self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let contributor = get_caller_address(); @@ -217,7 +219,13 @@ pub mod Campaign { self.contributions.add(contributor, amount); self.total_contributions.write(self.total_contributions.read() + amount); - self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + if self._is_target_reached() { + self.status.write(Status::SUCCESSFUL); + } + + let status = self.status.read(); + + self.emit(Event::ContributionMade(ContributionMade { contributor, amount, status })); } fn get_contribution(self: @ContractState, contributor: ContractAddress) -> u256 { From 1aa6bc1283dd4c40ba4ed99a3c3b80919c0bdfd4 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 08:54:33 +0200 Subject: [PATCH 073/116] rework all funcs --- .../crowdfunding/src/campaign.cairo | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 09546a20..03619308 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -94,7 +94,6 @@ pub mod Campaign { #[key] pub contributor: ContractAddress, pub amount: u256, - pub status: Status, } #[derive(Drop, starknet::Event)] @@ -174,9 +173,8 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self._assert_only_creator(); - assert(self.status.read() == Status::SUCCESSFUL, Errors::TARGET_NOT_REACHED); - assert(self._is_expired(), Errors::STILL_ACTIVE); - // no need to check end_time, as the owner can prematurely end the campaign + 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(); @@ -184,8 +182,10 @@ pub mod Campaign { let amount = token.balance_of(this); assert(amount > 0, Errors::ZERO_FUNDS); - // 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 + 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); @@ -198,7 +198,11 @@ pub mod Campaign { self._assert_only_creator(); assert(self._is_active(), Errors::ENDED); - self.status.write(Status::CLOSED); + if !self._is_target_reached() && self._is_expired() { + self.status.write(Status::UNSUCCESSFUL); + } else { + self.status.write(Status::CLOSED); + } self._withdraw_all(); @@ -206,9 +210,11 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); - assert(self.status.read() != Status::CLOSED, Errors::CLOSED); - assert(!self._is_expired(), Errors::ENDED); + let status = self.status.read(); + assert(status != Status::PENDING, Errors::STILL_PENDING); + assert(status != Status::CLOSED, Errors::CLOSED); + assert(status != Status::UNSUCCESSFUL, Errors::UNSUCCESSFUL); + assert(status != Status::SUCCESSFUL, Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let contributor = get_caller_address(); @@ -219,13 +225,7 @@ pub mod Campaign { self.contributions.add(contributor, amount); self.total_contributions.write(self.total_contributions.read() + amount); - if self._is_target_reached() { - self.status.write(Status::SUCCESSFUL); - } - - let status = self.status.read(); - - self.emit(Event::ContributionMade(ContributionMade { contributor, amount, status })); + self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); } fn get_contribution(self: @ContractState, contributor: ContractAddress) -> u256 { @@ -253,6 +253,7 @@ pub mod Campaign { self._assert_only_creator(); assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); + // TODO: calculate end_time here self.status.write(Status::ACTIVE); self.emit(Event::Activated(Activated {})); @@ -284,8 +285,15 @@ pub mod Campaign { if impl_hash.is_zero() { return Result::Err(array![Errors::CLASS_HASH_ZERO]); } + if self.status.read() != Status::ACTIVE && self.status.read() != Status::PENDING { + return Result::Err(array![Errors::ENDED]); + } - self._withdraw_all(); + // only active campaigns have no funds to refund + if self.status.read() == Status::ACTIVE { + self._withdraw_all(); + self.total_contributions.write(0); + } starknet::syscalls::replace_class_syscall(impl_hash)?; @@ -295,11 +303,8 @@ pub mod Campaign { } fn withdraw(ref self: ContractState) { - if self._is_expired() && !self._is_target_reached() && self._is_active() { - self.status.write(Status::UNSUCCESSFUL); - } assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); - assert(self.status.read() != Status::SUCCESSFUL, Errors::TARGET_ALREADY_REACHED); + 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); @@ -310,7 +315,8 @@ pub mod Campaign { // 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 - self.token.read().transfer(contributor, amount); + let success = self.token.read().transfer(contributor, amount); + assert(success, Errors::TRANSFER_FAILED); self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); } @@ -341,7 +347,8 @@ pub mod Campaign { while let Option::Some((contributor, amt)) = contributions .pop_front() { self.contributions.remove(contributor); - self.token.read().transfer(contributor, amt); + let success = self.token.read().transfer(contributor, amt); + assert(success, Errors::TRANSFER_FAILED); }; } } From 61562897b174e4ab02cecb61e70a98cdd3823c58 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 09:30:23 +0200 Subject: [PATCH 074/116] unsuccessful -> failed --- listings/applications/crowdfunding/src/campaign.cairo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 03619308..d0b5b3d4 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -9,7 +9,7 @@ pub enum Status { CLOSED, PENDING, SUCCESSFUL, - UNSUCCESSFUL, + FAILED, } #[derive(Drop, Serde)] @@ -128,7 +128,7 @@ pub mod Campaign { 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 UNSUCCESSFUL: felt252 = 'Campaign unsuccessful'; + 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'; @@ -199,7 +199,7 @@ pub mod Campaign { assert(self._is_active(), Errors::ENDED); if !self._is_target_reached() && self._is_expired() { - self.status.write(Status::UNSUCCESSFUL); + self.status.write(Status::FAILED); } else { self.status.write(Status::CLOSED); } @@ -213,7 +213,7 @@ pub mod Campaign { let status = self.status.read(); assert(status != Status::PENDING, Errors::STILL_PENDING); assert(status != Status::CLOSED, Errors::CLOSED); - assert(status != Status::UNSUCCESSFUL, Errors::UNSUCCESSFUL); + assert(status != Status::FAILED, Errors::FAILED); assert(status != Status::SUCCESSFUL, Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); From 93d2860e7541b50d0c27c4712ecdbd5caca7f888 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 09:47:35 +0200 Subject: [PATCH 075/116] calc end_time in start fn --- .../crowdfunding/src/campaign.cairo | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index d0b5b3d4..fe62a1a8 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -32,7 +32,7 @@ pub trait ICampaign { fn get_contribution(self: @TContractState, contributor: ContractAddress) -> u256; fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; - fn start(ref self: TContractState); + fn start(ref self: TContractState, duration: u64); fn upgrade(ref self: TContractState, impl_hash: ClassHash) -> Result<(), Array>; fn withdraw(ref self: TContractState); } @@ -150,20 +150,17 @@ pub mod Campaign { title: ByteArray, description: ByteArray, target: u256, - duration: u64, token_address: ContractAddress ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); assert(target > 0, Errors::ZERO_TARGET); - assert(duration > 0, Errors::ZERO_DURATION); self.token.write(IERC20Dispatcher { contract_address: token_address }); self.title.write(title); self.target.write(target); self.description.write(description); - self.end_time.write(get_block_timestamp() + duration); self.creator.write(creator); self.ownable._init(get_caller_address()); self.status.write(Status::PENDING) @@ -210,11 +207,8 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - let status = self.status.read(); - assert(status != Status::PENDING, Errors::STILL_PENDING); - assert(status != Status::CLOSED, Errors::CLOSED); - assert(status != Status::FAILED, Errors::FAILED); - assert(status != Status::SUCCESSFUL, Errors::ENDED); + 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(); @@ -249,11 +243,12 @@ pub mod Campaign { } } - fn start(ref self: ContractState) { + 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); - // TODO: calculate end_time here + self.end_time.write(get_block_timestamp() + duration); self.status.write(Status::ACTIVE); self.emit(Event::Activated(Activated {})); From bb61698645cd8f9fc607a9c233cf348a1bc3b8cb Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 09:50:13 +0200 Subject: [PATCH 076/116] calc end_time in upgrade fn --- .../applications/crowdfunding/src/campaign.cairo | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index fe62a1a8..6aa44aff 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -33,7 +33,9 @@ pub trait ICampaign { fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn start(ref self: TContractState, duration: u64); - fn upgrade(ref self: TContractState, impl_hash: ClassHash) -> Result<(), Array>; + fn upgrade( + ref self: TContractState, impl_hash: ClassHash, new_duration: u64 + ) -> Result<(), Array>; fn withdraw(ref self: TContractState); } @@ -273,7 +275,9 @@ pub mod Campaign { /// - 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) -> Result<(), Array> { + fn upgrade( + ref self: ContractState, impl_hash: ClassHash, new_duration: u64 + ) -> Result<(), Array> { if get_caller_address() != self.ownable.owner() { return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]); } @@ -283,11 +287,15 @@ pub mod Campaign { if self.status.read() != Status::ACTIVE && self.status.read() != Status::PENDING { return Result::Err(array![Errors::ENDED]); } + if new_duration > 0 { + return Result::Err(array![Errors::ZERO_DURATION]); + } // only active campaigns have no funds to refund if self.status.read() == Status::ACTIVE { self._withdraw_all(); self.total_contributions.write(0); + self.end_time.write(get_block_timestamp() + new_duration); } starknet::syscalls::replace_class_syscall(impl_hash)?; From fad077c43451c3e6bd6898110b1aa4b1951c74e3 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 10:18:12 +0200 Subject: [PATCH 077/116] makes upgrades callable only by creators in factory --- .../advanced_factory/src/contract.cairo | 64 +++++++++---------- .../crowdfunding/src/campaign.cairo | 41 +++++------- 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo index 62810fda..c56c505c 100644 --- a/listings/applications/advanced_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -8,11 +8,13 @@ pub trait ICampaignFactory { title: ByteArray, description: ByteArray, target: u256, - duration: 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 + ); } #[starknet::contract] @@ -36,8 +38,8 @@ pub mod CampaignFactory { struct Storage { #[substorage(v0)] ownable: ownable_component::Storage, - /// Store all of the created campaign instances' addresses - campaigns: List, + /// Store all of the created campaign instances' addresses and thei class hashes + campaigns: LegacyMap<(ContractAddress, ContractAddress), ClassHash>, /// Store the class hash of the contract to deploy campaign_class_hash: ClassHash, } @@ -47,9 +49,9 @@ pub mod CampaignFactory { pub enum Event { #[flat] OwnableEvent: ownable_component::Event, - ClassHashUpdated: ClassHashUpdated, - ClassHashUpdateFailed: ClassHashUpdateFailed, + CampaignClassHashUpgraded: CampaignClassHashUpgraded, CampaignCreated: CampaignCreated, + ClassHashUpdated: ClassHashUpdated, } #[derive(Drop, starknet::Event)] @@ -58,19 +60,21 @@ pub mod CampaignFactory { } #[derive(Drop, starknet::Event)] - pub struct ClassHashUpdateFailed { + pub struct CampaignClassHashUpgraded { pub campaign: ContractAddress, - pub errors: Array } #[derive(Drop, starknet::Event)] pub struct CampaignCreated { - pub caller: ContractAddress, + pub creator: ContractAddress, pub contract_address: ContractAddress } pub mod Errors { pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; + pub const ZERO_ADDRESS: felt252 = 'Zero address'; + pub const SAME_IMPLEMENTATION: felt252 = 'Implementation is unchanged'; + pub const CAMPAIGN_NOT_FOUND: felt252 = 'Campaign not found'; } #[constructor] @@ -89,14 +93,13 @@ pub mod CampaignFactory { title: ByteArray, description: ByteArray, target: u256, - duration: u64, token_address: ContractAddress, ) -> ContractAddress { - let caller = get_caller_address(); + let creator = get_caller_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((caller, title, description, target), duration, token_address) + ((creator, title, description, target), token_address) .serialize(ref constructor_calldata); // Contract deployment @@ -106,10 +109,9 @@ pub mod CampaignFactory { .unwrap_syscall(); // track new campaign instance - let mut campaigns = self.campaigns.read(); - campaigns.append(ICampaignDispatcher { contract_address }).unwrap(); + self.campaigns.write((creator, contract_address), self.campaign_class_hash.read()); - self.emit(Event::CampaignCreated(CampaignCreated { caller, contract_address })); + self.emit(Event::CampaignCreated(CampaignCreated { creator, contract_address })); contract_address } @@ -121,31 +123,27 @@ pub mod CampaignFactory { fn update_campaign_class_hash(ref self: ContractState, new_class_hash: ClassHash) { self.ownable._assert_only_owner(); + assert(new_class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); // update own campaign class hash value self.campaign_class_hash.write(new_class_hash); - // upgrade each campaign with the new class hash - let campaigns = self.campaigns.read(); - let mut i = 0; - while let Option::Some(campaign) = campaigns - .get(i) - .unwrap_syscall() { - if let Result::Err(errors) = campaign.upgrade(new_class_hash) { - self - .emit( - Event::ClassHashUpdateFailed( - ClassHashUpdateFailed { - campaign: campaign.contract_address, errors - } - ) - ) - } - i += 1; - }; - self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); } + + fn upgrade_campaign_implementation( + ref self: ContractState, campaign_address: ContractAddress, new_duration: Option + ) { + assert(campaign_address.is_non_zero(), Errors::ZERO_ADDRESS); + + let creator = get_caller_address(); + let old_class_hash = self.campaigns.read((creator, campaign_address)); + assert(old_class_hash.is_non_zero(), Errors::CAMPAIGN_NOT_FOUND); + 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); + } } } // ANCHOR_END: contract diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 6aa44aff..d617379c 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -33,9 +33,7 @@ pub trait ICampaign { fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn start(ref self: TContractState, duration: u64); - fn upgrade( - ref self: TContractState, impl_hash: ClassHash, new_duration: u64 - ) -> Result<(), Array>; + fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); fn withdraw(ref self: TContractState); } @@ -275,34 +273,29 @@ pub mod Campaign { /// - 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: u64 - ) -> Result<(), Array> { - if get_caller_address() != self.ownable.owner() { - return Result::Err(array![components::ownable::Errors::UNAUTHORIZED]); - } - if impl_hash.is_zero() { - return Result::Err(array![Errors::CLASS_HASH_ZERO]); - } - if self.status.read() != Status::ACTIVE && self.status.read() != Status::PENDING { - return Result::Err(array![Errors::ENDED]); - } - if new_duration > 0 { - return Result::Err(array![Errors::ZERO_DURATION]); - } - - // only active campaigns have no funds to refund + 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(); self.total_contributions.write(0); - self.end_time.write(get_block_timestamp() + new_duration); + self.end_time.write(get_block_timestamp() + duration); } - starknet::syscalls::replace_class_syscall(impl_hash)?; + starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); - - Result::Ok(()) } fn withdraw(ref self: ContractState) { From 50f65b773ae1c802b7d8aad16af64fb0fdb5c647 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 10:36:32 +0200 Subject: [PATCH 078/116] fix factory tests --- .../advanced_factory/src/tests.cairo | 84 ++++++++++++++----- listings/applications/crowdfunding/Scarb.toml | 2 +- .../crowdfunding/src/campaign.cairo | 2 +- 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 31bfca2a..0e4dd8f5 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -8,8 +8,8 @@ use starknet::{ ContractAddress, ClassHash, get_block_timestamp, contract_address_const, get_caller_address }; use snforge_std::{ - declare, ContractClass, ContractClassTrait, start_cheat_caller_address, spy_events, SpyOn, - EventSpy, EventAssertions, get_class_hash + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash }; // Define a target contract to deploy @@ -29,6 +29,8 @@ fn deploy_factory_with(campaign_class_hash: ClassHash) -> ICampaignFactoryDispat contract.deploy(constructor_calldata).unwrap(); + stop_cheat_caller_address(contract_address); + ICampaignFactoryDispatcher { contract_address } } @@ -51,7 +53,7 @@ fn test_deploy_factory() { } #[test] -fn test_deploy_campaign() { +fn test_create_campaign() { let factory = deploy_factory(); let mut spy = spy_events(SpyOn::One(factory.contract_address)); @@ -62,18 +64,17 @@ fn test_deploy_campaign() { let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; let target: u256 = 10000; - let duration: u64 = 60; let token = contract_address_const::<'token'>(); let campaign_address = factory - .create_campaign(title.clone(), description.clone(), target, duration, token); + .create_campaign(title.clone(), description.clone(), target, 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.target, target); - assert_eq!(details.end_time, get_block_timestamp() + duration); + assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::PENDING); assert_eq!(details.token, token); assert_eq!(details.total_contributions, 0); @@ -89,7 +90,7 @@ fn test_deploy_campaign() { factory.contract_address, CampaignFactory::Event::CampaignCreated( CampaignFactory::CampaignCreated { - caller: campaign_creator, contract_address: campaign_address + creator: campaign_creator, contract_address: campaign_address } ) ) @@ -98,29 +99,46 @@ fn test_deploy_campaign() { } #[test] -fn test_update_campaign_class_hash() { +fn test_uprade_campaign_class_hash() { let factory = deploy_factory(); + let old_class_hash = factory.get_campaign_class_hash(); + let new_class_hash = declare("MockContract").unwrap().class_hash; let token = contract_address_const::<'token'>(); - let campaign_address_1 = factory.create_campaign("title 1", "description 1", 10000, 60, token); - let campaign_address_2 = factory.create_campaign("title 2", "description 2", 20000, 120, token); - assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_1)); - assert_eq!(factory.get_campaign_class_hash(), get_class_hash(campaign_address_2)); + // deploy a pending campaign with the old class hash + 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, token); + + assert_eq!(old_class_hash, get_class_hash(pending_campaign)); + + // deploy an active campaign with the old class hash + 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, token); + stop_cheat_caller_address(factory.contract_address); + + start_cheat_caller_address(active_campaign, active_campaign_creator); + ICampaignDispatcher { contract_address: active_campaign }.start(60); + stop_cheat_caller_address(active_campaign); - let mut spy_factory = spy_events(SpyOn::One(factory.contract_address)); - let mut spy_campaigns = spy_events( - SpyOn::Multiple(array![campaign_address_1, campaign_address_2]) + assert_eq!(old_class_hash, get_class_hash(active_campaign)); + + // update the factory's campaign class hash value + let mut spy = spy_events( + SpyOn::Multiple(array![factory.contract_address, pending_campaign, active_campaign]) ); - let new_class_hash = declare("MockContract").unwrap().class_hash; + let factory_owner = contract_address_const::<'factory_owner'>(); + start_cheat_caller_address(factory.contract_address, factory_owner); factory.update_campaign_class_hash(new_class_hash); assert_eq!(factory.get_campaign_class_hash(), new_class_hash); - assert_eq!(get_class_hash(campaign_address_1), new_class_hash); - assert_eq!(get_class_hash(campaign_address_2), new_class_hash); + assert_eq!(old_class_hash, get_class_hash(pending_campaign)); + assert_eq!(old_class_hash, get_class_hash(active_campaign)); - spy_factory + spy .assert_emitted( @array![ ( @@ -132,15 +150,35 @@ fn test_update_campaign_class_hash() { ] ); - spy_campaigns + // upgrade pending campaign + start_cheat_caller_address(factory.contract_address, pending_campaign_creator); + factory.upgrade_campaign_implementation(pending_campaign, Option::None); + + assert_eq!(get_class_hash(pending_campaign), new_class_hash); + assert_eq!(get_class_hash(active_campaign), old_class_hash); + + spy .assert_emitted( @array![ ( - campaign_address_1, + pending_campaign, Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) - ), + ) + ] + ); + + // upgrade active campaign + start_cheat_caller_address(factory.contract_address, active_campaign_creator); + factory.upgrade_campaign_implementation(active_campaign, Option::Some(60)); + + assert_eq!(get_class_hash(pending_campaign), new_class_hash); + assert_eq!(get_class_hash(active_campaign), new_class_hash); + + spy + .assert_emitted( + @array![ ( - campaign_address_2, + active_campaign, Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) ) ] diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml index e3f947a6..2d781ca8 100644 --- a/listings/applications/crowdfunding/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -3,7 +3,7 @@ name = "crowdfunding" version.workspace = true edition = "2023_11" -# [lib] +[lib] [dependencies] starknet.workspace = true diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index d617379c..8b9d1bd3 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -277,7 +277,7 @@ pub mod Campaign { 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, + self.status.read() == Status::ACTIVE || self.status.read() == Status::PENDING, Errors::ENDED ); From cae2898a54b9053f1e62818b7eccb36aa7cf12d2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 12:08:57 +0200 Subject: [PATCH 079/116] fix crowdfunding tests --- .../crowdfunding/src/campaign.cairo | 19 +- .../crowdfunding/src/mock_upgrade.cairo | 319 +++++++++++++++++- .../applications/crowdfunding/src/tests.cairo | 216 ++++++++---- 3 files changed, 484 insertions(+), 70 deletions(-) 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" } + ) + ) + ] + ); } From 441d5c13a241249228ebc290d37fcfbd83e68882 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 16:19:11 +0200 Subject: [PATCH 080/116] reduce total contri. when withdraw from act. camp --- .../applications/crowdfunding/src/campaign.cairo | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index a9e79006..4f01ade8 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -140,6 +140,7 @@ pub mod Campaign { 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 ZERO_ADDRESS_CONTRIBUTOR: 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'; @@ -147,6 +148,7 @@ pub mod Campaign { 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'; + pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; } #[constructor] @@ -157,7 +159,7 @@ pub mod Campaign { description: ByteArray, target: u256, token_address: ContractAddress, - // TODO: add recepient address + // TODO: add recepient address? ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); @@ -261,6 +263,7 @@ pub mod Campaign { 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. @@ -315,8 +318,11 @@ pub mod Campaign { 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 + // if the campaign is "failed", then there's 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 + if self._is_active() { + self.total_contributions.write(self.total_contributions.read() - amount); + } let success = self.token.read().transfer(contributor, amount); assert(success, Errors::TRANSFER_FAILED); From df0d76356a098aebacd3c27cc88965584091adb2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 16:25:38 +0200 Subject: [PATCH 081/116] add refund fn --- .../crowdfunding/src/campaign.cairo | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 4f01ade8..29c02511 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -33,6 +33,7 @@ pub trait ICampaign { fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn start(ref self: TContractState, duration: u64); + fn refund(ref self: TContractState, contributor: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); fn withdraw(ref self: TContractState); } @@ -81,15 +82,19 @@ pub mod Campaign { #[flat] OwnableEvent: ownable_component::Event, Activated: Activated, - ContributableEvent: contributable_component::Event, - ContributionMade: ContributionMade, Claimed: Claimed, Closed: Closed, + ContributableEvent: contributable_component::Event, + ContributionMade: ContributionMade, + Refunded: Refunded, Upgraded: Upgraded, Withdrawn: Withdrawn, WithdrawnAll: WithdrawnAll, } + #[derive(Drop, starknet::Event)] + pub struct Activated {} + #[derive(Drop, starknet::Event)] pub struct ContributionMade { #[key] @@ -103,10 +108,15 @@ pub mod Campaign { } #[derive(Drop, starknet::Event)] - pub struct Activated {} + pub struct Closed { + pub reason: ByteArray, + } #[derive(Drop, starknet::Event)] - pub struct Closed { + pub struct Refunded { + #[key] + pub contributor: ContractAddress, + pub amount: u256, pub reason: ByteArray, } @@ -263,6 +273,18 @@ pub mod Campaign { self.emit(Event::Activated(Activated {})); } + fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { + self._assert_only_creator(); + assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); + assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self._is_active(), Errors::ENDED); + assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + + let amount = self._refund(contributor); + + self.emit(Event::Refunded(Refunded { contributor, 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 contributors, @@ -316,16 +338,7 @@ pub mod Campaign { assert(self.contributions.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); - let amount = self.contributions.remove(contributor); - - // if the campaign is "failed", then there's 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 - if self._is_active() { - self.total_contributions.write(self.total_contributions.read() - amount); - } - - let success = self.token.read().transfer(contributor, amount); - assert(success, Errors::TRANSFER_FAILED); + let amount = self._refund(contributor); self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); } @@ -351,6 +364,21 @@ pub mod Campaign { self.total_contributions.read() >= self.target.read() } + fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { + let amount = self.contributions.remove(contributor); + + // if the campaign is "failed", then there's 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 + if self.status.read() == Status::ACTIVE { + self.total_contributions.write(self.total_contributions.read() - amount); + } + + let success = self.token.read().transfer(contributor, amount); + assert(success, Errors::TRANSFER_FAILED); + + amount + } + fn _withdraw_all(ref self: ContractState, reason: ByteArray) { let mut contributions = self.contributions.get_contributions_as_arr(); while let Option::Some((contributor, amt)) = contributions From 71d4ff59c3c68839b1a0a32ada5df04e83db6e9e Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 16:29:58 +0200 Subject: [PATCH 082/116] refactor withdraw_all to use _refund --- .../crowdfunding/src/campaign.cairo | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 29c02511..a9d36694 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -89,7 +89,7 @@ pub mod Campaign { Refunded: Refunded, Upgraded: Upgraded, Withdrawn: Withdrawn, - WithdrawnAll: WithdrawnAll, + RefundedAll: RefundedAll, } #[derive(Drop, starknet::Event)] @@ -120,6 +120,11 @@ pub mod Campaign { pub reason: ByteArray, } + #[derive(Drop, starknet::Event)] + pub struct RefundedAll { + pub reason: ByteArray, + } + #[derive(Drop, starknet::Event)] pub struct Upgraded { pub implementation: ClassHash @@ -132,11 +137,6 @@ 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'; @@ -220,7 +220,7 @@ pub mod Campaign { self.status.write(Status::CLOSED); } - self._withdraw_all(reason.clone()); + self._refund_all(reason.clone()); self.emit(Event::Closed(Closed { reason })); } @@ -320,8 +320,7 @@ pub mod Campaign { Option::None => 0, }; assert(duration > 0, Errors::ZERO_DURATION); - self._withdraw_all("contract upgraded"); - self.total_contributions.write(0); + self._refund_all("contract upgraded"); self.end_time.write(get_block_timestamp() + duration); } @@ -379,16 +378,13 @@ pub mod Campaign { amount } - fn _withdraw_all(ref self: ContractState, reason: ByteArray) { + fn _refund_all(ref self: ContractState, reason: ByteArray) { let mut contributions = self.contributions.get_contributions_as_arr(); - while let Option::Some((contributor, amt)) = contributions + while let Option::Some((contributor, _)) = contributions .pop_front() { - self.contributions.remove(contributor); - let success = self.token.read().transfer(contributor, amt); - assert(success, Errors::TRANSFER_FAILED); + self._refund(contributor); }; - - self.emit(Event::WithdrawnAll(WithdrawnAll { reason })); + self.emit(Event::RefundedAll(RefundedAll { reason })); } } } From 0671b0e9059e6c68107c6e9f987c1900f4332a89 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 16:41:15 +0200 Subject: [PATCH 083/116] pending->draft --- .../crowdfunding/src/campaign.cairo | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index a9d36694..d6c86c86 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -7,7 +7,7 @@ use starknet::{ClassHash, ContractAddress}; pub enum Status { ACTIVE, CLOSED, - PENDING, + DRAFT, SUCCESSFUL, FAILED, } @@ -140,9 +140,9 @@ pub mod Campaign { 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 NOT_DRAFT: felt252 = 'Campaign not draft'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; - pub const STILL_PENDING: felt252 = 'Campaign not yet active'; + pub const STILL_DRAFT: 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'; @@ -182,7 +182,7 @@ pub mod Campaign { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - self.status.write(Status::PENDING) + self.status.write(Status::DRAFT) } #[abi(embed_v0)] @@ -226,7 +226,7 @@ pub mod Campaign { } fn contribute(ref self: ContractState, amount: u256) { - assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active() && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -264,7 +264,7 @@ pub mod Campaign { fn start(ref self: ContractState, duration: u64) { self._assert_only_creator(); - assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); + assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); assert(duration > 0, Errors::ZERO_DURATION); self.end_time.write(get_block_timestamp() + duration); @@ -276,7 +276,7 @@ pub mod Campaign { fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { self._assert_only_creator(); assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); - assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active(), Errors::ENDED); assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); @@ -285,7 +285,6 @@ pub mod Campaign { self.emit(Event::Refunded(Refunded { contributor, 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 contributors, /// and there's nothing stopping them from implementing a malicious upgrade. @@ -309,7 +308,7 @@ pub mod Campaign { 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, + self.status.read() == Status::ACTIVE || self.status.read() == Status::DRAFT, Errors::ENDED ); @@ -330,7 +329,7 @@ pub mod Campaign { } fn withdraw(ref self: ContractState) { - assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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); From f6258cbc56989d84af698c1e8d23f53cd056db03 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 16:41:20 +0200 Subject: [PATCH 084/116] fix mock and tests --- .../advanced_factory/src/tests.cairo | 2 +- .../crowdfunding/src/mock_upgrade.cairo | 105 ++++++++++-------- .../applications/crowdfunding/src/tests.cairo | 6 +- 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 0e4dd8f5..00896c35 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -75,7 +75,7 @@ fn test_create_campaign() { assert_eq!(details.description, description); assert_eq!(details.target, target); assert_eq!(details.end_time, 0); - assert_eq!(details.status, Status::PENDING); + assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, token); assert_eq!(details.total_contributions, 0); assert_eq!(details.creator, campaign_creator); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 2b3227ef..53c5f2e3 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -9,7 +9,7 @@ pub mod MockUpgrade { }; use components::ownable::ownable_component; use crowdfunding::campaign::contributions::contributable_component; - use crowdfunding::campaign::{ICampaign, Details, Status}; + use crowdfunding::campaign::{ICampaign, Details, Status, Campaign::Errors}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: contributable_component, storage: contributions, event: ContributableEvent); @@ -42,15 +42,19 @@ pub mod MockUpgrade { #[flat] OwnableEvent: ownable_component::Event, Activated: Activated, - ContributableEvent: contributable_component::Event, - ContributionMade: ContributionMade, Claimed: Claimed, Closed: Closed, + ContributableEvent: contributable_component::Event, + ContributionMade: ContributionMade, + Refunded: Refunded, Upgraded: Upgraded, Withdrawn: Withdrawn, - WithdrawnAll: WithdrawnAll, + RefundedAll: RefundedAll, } + #[derive(Drop, starknet::Event)] + pub struct Activated {} + #[derive(Drop, starknet::Event)] pub struct ContributionMade { #[key] @@ -64,10 +68,20 @@ pub mod MockUpgrade { } #[derive(Drop, starknet::Event)] - pub struct Activated {} + pub struct Closed { + pub reason: ByteArray, + } #[derive(Drop, starknet::Event)] - pub struct Closed { + pub struct Refunded { + #[key] + pub contributor: ContractAddress, + pub amount: u256, + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct RefundedAll { pub reason: ByteArray, } @@ -83,33 +97,6 @@ pub mod MockUpgrade { 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, @@ -131,7 +118,7 @@ pub mod MockUpgrade { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - self.status.write(Status::PENDING) + self.status.write(Status::DRAFT) } #[abi(embed_v0)] @@ -169,13 +156,13 @@ pub mod MockUpgrade { self.status.write(Status::CLOSED); } - self._withdraw_all(reason.clone()); + self._refund_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.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active() && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -213,7 +200,7 @@ pub mod MockUpgrade { fn start(ref self: ContractState, duration: u64) { self._assert_only_creator(); - assert(self.status.read() == Status::PENDING, Errors::NOT_PENDING); + assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); assert(duration > 0, Errors::ZERO_DURATION); self.end_time.write(get_block_timestamp() + duration); @@ -222,6 +209,18 @@ pub mod MockUpgrade { self.emit(Event::Activated(Activated {})); } + fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { + self._assert_only_creator(); + assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); + assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + assert(self._is_active(), Errors::ENDED); + assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + + let amount = self._refund(contributor); + + self.emit(Event::Refunded(Refunded { contributor, 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 contributors, /// and there's nothing stopping them from implementing a malicious upgrade. @@ -245,7 +244,7 @@ pub mod MockUpgrade { 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, + self.status.read() == Status::ACTIVE || self.status.read() == Status::DRAFT, Errors::ENDED ); @@ -256,7 +255,7 @@ pub mod MockUpgrade { Option::None => 0, }; assert(duration > 0, Errors::ZERO_DURATION); - self._withdraw_all("contract upgraded"); + self._refund_all("contract upgraded"); self.total_contributions.write(0); self.end_time.write(get_block_timestamp() + duration); } @@ -267,7 +266,7 @@ pub mod MockUpgrade { } fn withdraw(ref self: ContractState) { - assert(self.status.read() != Status::PENDING, Errors::STILL_PENDING); + assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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); @@ -306,16 +305,28 @@ pub mod MockUpgrade { self.total_contributions.read() >= self.target.read() } - fn _withdraw_all(ref self: ContractState, reason: ByteArray) { + fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { + let amount = self.contributions.remove(contributor); + + // if the campaign is "failed", then there's 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 + if self.status.read() == Status::ACTIVE { + self.total_contributions.write(self.total_contributions.read() - amount); + } + + let success = self.token.read().transfer(contributor, amount); + assert(success, Errors::TRANSFER_FAILED); + + amount + } + + fn _refund_all(ref self: ContractState, reason: ByteArray) { let mut contributions = self.contributions.get_contributions_as_arr(); - while let Option::Some((contributor, amt)) = contributions + while let Option::Some((contributor, _)) = contributions .pop_front() { - self.contributions.remove(contributor); - let success = self.token.read().transfer(contributor, amt); - assert(success, Errors::TRANSFER_FAILED); + self._refund(contributor); }; - - self.emit(Event::WithdrawnAll(WithdrawnAll { reason })); + self.emit(Event::RefundedAll(RefundedAll { reason })); } } } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 79c07889..d251392a 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -96,7 +96,7 @@ fn test_deploy() { assert_eq!(details.description, "description 1"); assert_eq!(details.target, 10000); assert_eq!(details.end_time, 0); - assert_eq!(details.status, Status::PENDING); + assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_contributions, 0); assert_eq!(details.creator, contract_address_const::<'creator'>()); @@ -277,8 +277,8 @@ fn test_upgrade_class_hash() { ), ( campaign.contract_address, - Campaign::Event::WithdrawnAll( - Campaign::WithdrawnAll { reason: "contract upgraded" } + Campaign::Event::RefundedAll( + Campaign::RefundedAll { reason: "contract upgraded" } ) ) ] From f236df12495e0fef65f7f0f99634730a93180a06 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 17:05:44 +0200 Subject: [PATCH 085/116] add test for close --- .../crowdfunding/src/campaign.cairo | 4 +- .../applications/crowdfunding/src/tests.cairo | 102 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index d6c86c86..8e9bf9f3 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -110,6 +110,7 @@ pub mod Campaign { #[derive(Drop, starknet::Event)] pub struct Closed { pub reason: ByteArray, + pub status: Status, } #[derive(Drop, starknet::Event)] @@ -221,8 +222,9 @@ pub mod Campaign { } self._refund_all(reason.clone()); + let status = self.status.read(); - self.emit(Event::Closed(Closed { reason })); + self.emit(Event::Closed(Closed { reason, status })); } fn contribute(ref self: ContractState, amount: u256) { diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index d251392a..0c286ded 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -285,3 +285,105 @@ fn test_upgrade_class_hash() { ); } +#[test] +fn test_close() { + let contract_class = declare("Campaign").unwrap(); + let token_class = declare("ERC20").unwrap(); + let duration: u64 = 60; + + // test closed campaign + let (campaign, token) = deploy_with_token(contract_class, token_class); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + 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 total_contributions = campaign.get_details().total_contributions; + + start_cheat_caller_address(campaign.contract_address, creator); + campaign.close("testing"); + stop_cheat_caller_address(campaign.contract_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, total_contributions); + assert_eq!(campaign.get_details().status, Status::CLOSED); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::RefundedAll(Campaign::RefundedAll { reason: "testing" }) + ), + ( + campaign.contract_address, + Campaign::Event::Closed( + Campaign::Closed { reason: "testing", status: Status::CLOSED } + ) + ) + ] + ); + + // test failed campaign + let (campaign, token) = deploy_with_token(contract_class, token_class); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + 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 total_contributions = campaign.get_details().total_contributions; + + cheat_block_timestamp_global(duration); + + start_cheat_caller_address(campaign.contract_address, creator); + campaign.close("testing"); + stop_cheat_caller_address(campaign.contract_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, total_contributions); + assert_eq!(campaign.get_details().status, Status::FAILED); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::RefundedAll(Campaign::RefundedAll { reason: "testing" }) + ), + ( + campaign.contract_address, + Campaign::Event::Closed( + Campaign::Closed { reason: "testing", status: Status::FAILED } + ) + ) + ] + ); +} From 863abaf30c033b6b8db8b2ab3f98525e5d63fee5 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 17:20:22 +0200 Subject: [PATCH 086/116] add test for withdraw --- .../crowdfunding/src/campaign.cairo | 7 +- .../crowdfunding/src/mock_upgrade.cairo | 10 ++- .../applications/crowdfunding/src/tests.cairo | 83 +++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 8e9bf9f3..0e272e65 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -35,7 +35,7 @@ pub trait ICampaign { fn start(ref self: TContractState, duration: u64); fn refund(ref self: TContractState, contributor: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); - fn withdraw(ref self: TContractState); + fn withdraw(ref self: TContractState, reason: ByteArray); } #[starknet::contract] @@ -136,6 +136,7 @@ pub mod Campaign { #[key] pub contributor: ContractAddress, pub amount: u256, + pub reason: ByteArray, } pub mod Errors { @@ -330,7 +331,7 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - fn withdraw(ref self: ContractState) { + fn withdraw(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); assert(self.status.read() != Status::CLOSED, Errors::CLOSED); @@ -340,7 +341,7 @@ pub mod Campaign { let contributor = get_caller_address(); let amount = self._refund(contributor); - self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); + self.emit(Event::Withdrawn(Withdrawn { contributor, amount, reason })); } } diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 53c5f2e3..300bdff0 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -36,6 +36,7 @@ pub mod MockUpgrade { status: Status } + #[event] #[derive(Drop, starknet::Event)] pub enum Event { @@ -70,6 +71,7 @@ pub mod MockUpgrade { #[derive(Drop, starknet::Event)] pub struct Closed { pub reason: ByteArray, + pub status: Status, } #[derive(Drop, starknet::Event)] @@ -95,6 +97,7 @@ pub mod MockUpgrade { #[key] pub contributor: ContractAddress, pub amount: u256, + pub reason: ByteArray, } #[constructor] @@ -157,8 +160,9 @@ pub mod MockUpgrade { } self._refund_all(reason.clone()); + let status = self.status.read(); - self.emit(Event::Closed(Closed { reason })); + self.emit(Event::Closed(Closed { reason, status })); } fn contribute(ref self: ContractState, amount: u256) { @@ -265,7 +269,7 @@ pub mod MockUpgrade { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - fn withdraw(ref self: ContractState) { + fn withdraw(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); assert(self.status.read() != Status::CLOSED, Errors::CLOSED); @@ -281,7 +285,7 @@ pub mod MockUpgrade { let success = self.token.read().transfer(contributor, amount); assert(success, Errors::TRANSFER_FAILED); - self.emit(Event::Withdrawn(Withdrawn { contributor, amount })); + self.emit(Event::Withdrawn(Withdrawn { contributor, amount, reason })); } } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 0c286ded..efb052b1 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -387,3 +387,86 @@ fn test_close() { ] ); } + +#[test] +fn test_refund() { + // setup + let duration: u64 = 60; + let (campaign, token) = deploy_with_token( + declare("Campaign").unwrap(), declare("ERC20").unwrap() + ); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + let creator = contract_address_const::<'creator'>(); + let contributor = contract_address_const::<'contributor_1'>(); + let amount: u256 = 3000; + let prev_balance = token.balance_of(contributor); + + // donate + start_cheat_caller_address(campaign.contract_address, creator); + campaign.start(duration); + start_cheat_caller_address(campaign.contract_address, contributor); + campaign.contribute(amount); + assert_eq!(campaign.get_details().total_contributions, amount); + assert_eq!(campaign.get_contribution(contributor), amount); + assert_eq!(token.balance_of(contributor), prev_balance - amount); + + // refund + start_cheat_caller_address(campaign.contract_address, creator); + campaign.refund(contributor, "testing"); + assert_eq!(campaign.get_details().total_contributions, 0); + assert_eq!(campaign.get_contribution(contributor), 0); + assert_eq!(token.balance_of(contributor), prev_balance); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Refunded( + Campaign::Refunded { contributor, amount, reason: "testing" } + ) + ) + ] + ); +} + +#[test] +fn test_withdraw() { + // setup + let duration: u64 = 60; + let (campaign, token) = deploy_with_token( + declare("Campaign").unwrap(), declare("ERC20").unwrap() + ); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + let creator = contract_address_const::<'creator'>(); + let contributor = contract_address_const::<'contributor_1'>(); + let amount: u256 = 3000; + let prev_balance = token.balance_of(contributor); + start_cheat_caller_address(campaign.contract_address, creator); + campaign.start(duration); + + // donate + start_cheat_caller_address(campaign.contract_address, contributor); + campaign.contribute(amount); + assert_eq!(campaign.get_details().total_contributions, amount); + assert_eq!(campaign.get_contribution(contributor), amount); + assert_eq!(token.balance_of(contributor), prev_balance - amount); + + // withdraw + campaign.withdraw("testing"); + assert_eq!(campaign.get_details().total_contributions, 0); + assert_eq!(campaign.get_contribution(contributor), 0); + assert_eq!(token.balance_of(contributor), prev_balance); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Withdrawn( + Campaign::Withdrawn { contributor, amount, reason: "testing" } + ) + ) + ] + ); +} From d3221c86386c6c1a150f79eb12070b1f52dccb3c Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 13 Jun 2024 17:40:08 +0200 Subject: [PATCH 087/116] upgrade > update end_time only if duration provided --- .../crowdfunding/src/campaign.cairo | 41 ++++--------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 0e272e65..05a605e2 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -191,7 +191,9 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn claim(ref self: ContractState) { self._assert_only_creator(); - assert(self._is_active() && self._is_expired(), Errors::STILL_ACTIVE); + assert( + self.status.read() == Status::ACTIVE && self._is_expired(), Errors::STILL_ACTIVE + ); assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); @@ -214,7 +216,7 @@ pub mod Campaign { fn close(ref self: ContractState, reason: ByteArray) { self._assert_only_creator(); - assert(self._is_active(), Errors::ENDED); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); if !self._is_target_reached() && self._is_expired() { self.status.write(Status::FAILED); @@ -230,7 +232,7 @@ pub mod Campaign { fn contribute(ref self: ContractState, amount: u256) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); - assert(self._is_active() && !self._is_expired(), Errors::ENDED); + assert(self.status.read() == Status::ACTIVE && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); let contributor = get_caller_address(); @@ -280,7 +282,7 @@ pub mod Campaign { self._assert_only_creator(); assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); - assert(self._is_active(), Errors::ENDED); + assert(self.status.read() == Status::ACTIVE, Errors::ENDED); assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); let amount = self._refund(contributor); @@ -288,25 +290,6 @@ pub mod Campaign { self.emit(Event::Refunded(Refunded { contributor, 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 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); @@ -317,13 +300,11 @@ pub mod Campaign { // 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(duration) = new_duration { + assert(duration > 0, Errors::ZERO_DURATION); + self.end_time.write(get_block_timestamp() + duration); }; - assert(duration > 0, Errors::ZERO_DURATION); self._refund_all("contract upgraded"); - self.end_time.write(get_block_timestamp() + duration); } starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); @@ -357,10 +338,6 @@ pub mod Campaign { 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() } From af95cb4f3d4767e04c855968b6bb0f1ae1875d0a Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 07:31:11 +0200 Subject: [PATCH 088/116] close->cancel --- .../applications/crowdfunding/src/campaign.cairo | 10 +++++----- .../applications/crowdfunding/src/mock_upgrade.cairo | 8 ++++---- listings/applications/crowdfunding/src/tests.cairo | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 05a605e2..bad726ee 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -27,7 +27,7 @@ pub struct Details { #[starknet::interface] pub trait ICampaign { fn claim(ref self: TContractState); - fn close(ref self: TContractState, reason: ByteArray); + fn cancel(ref self: TContractState, reason: ByteArray); fn contribute(ref self: TContractState, amount: u256); fn get_contribution(self: @TContractState, contributor: ContractAddress) -> u256; fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; @@ -83,7 +83,7 @@ pub mod Campaign { OwnableEvent: ownable_component::Event, Activated: Activated, Claimed: Claimed, - Closed: Closed, + Canceled: Canceled, ContributableEvent: contributable_component::Event, ContributionMade: ContributionMade, Refunded: Refunded, @@ -108,7 +108,7 @@ pub mod Campaign { } #[derive(Drop, starknet::Event)] - pub struct Closed { + pub struct Canceled { pub reason: ByteArray, pub status: Status, } @@ -214,7 +214,7 @@ pub mod Campaign { self.emit(Event::Claimed(Claimed { amount })); } - fn close(ref self: ContractState, reason: ByteArray) { + fn cancel(ref self: ContractState, reason: ByteArray) { self._assert_only_creator(); assert(self.status.read() == Status::ACTIVE, Errors::ENDED); @@ -227,7 +227,7 @@ pub mod Campaign { self._refund_all(reason.clone()); let status = self.status.read(); - self.emit(Event::Closed(Closed { reason, status })); + self.emit(Event::Canceled(Canceled { reason, status })); } fn contribute(ref self: ContractState, amount: u256) { diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 300bdff0..771019fc 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -44,7 +44,7 @@ pub mod MockUpgrade { OwnableEvent: ownable_component::Event, Activated: Activated, Claimed: Claimed, - Closed: Closed, + Canceled: Canceled, ContributableEvent: contributable_component::Event, ContributionMade: ContributionMade, Refunded: Refunded, @@ -69,7 +69,7 @@ pub mod MockUpgrade { } #[derive(Drop, starknet::Event)] - pub struct Closed { + pub struct Canceled { pub reason: ByteArray, pub status: Status, } @@ -149,7 +149,7 @@ pub mod MockUpgrade { self.emit(Event::Claimed(Claimed { amount })); } - fn close(ref self: ContractState, reason: ByteArray) { + fn cancel(ref self: ContractState, reason: ByteArray) { self._assert_only_creator(); assert(self._is_active(), Errors::ENDED); @@ -162,7 +162,7 @@ pub mod MockUpgrade { self._refund_all(reason.clone()); let status = self.status.read(); - self.emit(Event::Closed(Closed { reason, status })); + self.emit(Event::Canceled(Canceled { reason, status })); } fn contribute(ref self: ContractState, amount: u256) { diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index efb052b1..3790a2a4 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -313,7 +313,7 @@ fn test_close() { let total_contributions = campaign.get_details().total_contributions; start_cheat_caller_address(campaign.contract_address, creator); - campaign.close("testing"); + campaign.cancel("testing"); stop_cheat_caller_address(campaign.contract_address); assert_eq!(prev_balance_contributor_1, token.balance_of(contributor_1)); @@ -331,8 +331,8 @@ fn test_close() { ), ( campaign.contract_address, - Campaign::Event::Closed( - Campaign::Closed { reason: "testing", status: Status::CLOSED } + Campaign::Event::Canceled( + Campaign::Canceled { reason: "testing", status: Status::CLOSED } ) ) ] @@ -362,7 +362,7 @@ fn test_close() { cheat_block_timestamp_global(duration); start_cheat_caller_address(campaign.contract_address, creator); - campaign.close("testing"); + campaign.cancel("testing"); stop_cheat_caller_address(campaign.contract_address); assert_eq!(prev_balance_contributor_1, token.balance_of(contributor_1)); @@ -380,8 +380,8 @@ fn test_close() { ), ( campaign.contract_address, - Campaign::Event::Closed( - Campaign::Closed { reason: "testing", status: Status::FAILED } + Campaign::Event::Canceled( + Campaign::Canceled { reason: "testing", status: Status::FAILED } ) ) ] From 07fbd06158a57b1b86150bed25edcb445a60ccd7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 07:44:22 +0200 Subject: [PATCH 089/116] rename to more align with Solidity by example --- .../advanced_factory/src/tests.cairo | 4 +- .../crowdfunding/src/campaign.cairo | 112 +++++++++--------- .../{contributions.cairo => pledges.cairo} | 14 +-- .../crowdfunding/src/mock_upgrade.cairo | 76 ++++++------ .../applications/crowdfunding/src/tests.cairo | 106 ++++++++--------- 5 files changed, 156 insertions(+), 156 deletions(-) rename listings/applications/crowdfunding/src/campaign/{contributions.cairo => pledges.cairo} (92%) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 00896c35..8233f664 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -77,7 +77,7 @@ fn test_create_campaign() { assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, token); - assert_eq!(details.total_contributions, 0); + assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, campaign_creator); let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; @@ -120,7 +120,7 @@ fn test_uprade_campaign_class_hash() { stop_cheat_caller_address(factory.contract_address); start_cheat_caller_address(active_campaign, active_campaign_creator); - ICampaignDispatcher { contract_address: active_campaign }.start(60); + ICampaignDispatcher { contract_address: active_campaign }.launch(60); stop_cheat_caller_address(active_campaign); assert_eq!(old_class_hash, get_class_hash(active_campaign)); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index bad726ee..6bee4adb 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -1,4 +1,4 @@ -pub mod contributions; +pub mod pledges; // ANCHOR: contract use starknet::{ClassHash, ContractAddress}; @@ -21,21 +21,21 @@ pub struct Details { pub description: ByteArray, pub status: Status, pub token: ContractAddress, - pub total_contributions: u256, + pub total_pledges: u256, } #[starknet::interface] pub trait ICampaign { fn claim(ref self: TContractState); fn cancel(ref self: TContractState, reason: ByteArray); - fn contribute(ref self: TContractState, amount: u256); - fn get_contribution(self: @TContractState, contributor: ContractAddress) -> u256; - fn get_contributions(self: @TContractState) -> Array<(ContractAddress, u256)>; + fn pledge(ref self: TContractState, amount: u256); + fn get_pledge(self: @TContractState, contributor: ContractAddress) -> u256; + fn get_pledges(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; - fn start(ref self: TContractState, duration: u64); + fn launch(ref self: TContractState, duration: u64); fn refund(ref self: TContractState, contributor: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); - fn withdraw(ref self: TContractState, reason: ByteArray); + fn unpledge(ref self: TContractState, reason: ByteArray); } #[starknet::contract] @@ -48,31 +48,31 @@ pub mod Campaign { get_caller_address, get_contract_address, class_hash::class_hash_const }; use components::ownable::ownable_component; - use super::contributions::contributable_component; + use super::pledges::pledgeable_component; use super::{Details, Status}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); - component!(path: contributable_component, storage: contributions, event: ContributableEvent); + component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); #[abi(embed_v0)] pub impl OwnableImpl = ownable_component::Ownable; impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; #[abi(embed_v0)] - impl ContributableImpl = contributable_component::Contributable; + impl PledgeableImpl = pledgeable_component::Pledgeable; #[storage] struct Storage { #[substorage(v0)] ownable: ownable_component::Storage, #[substorage(v0)] - contributions: contributable_component::Storage, + pledges: pledgeable_component::Storage, end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, target: u256, title: ByteArray, description: ByteArray, - total_contributions: u256, + total_pledges: u256, status: Status } @@ -81,25 +81,21 @@ pub mod Campaign { pub enum Event { #[flat] OwnableEvent: ownable_component::Event, - Activated: Activated, Claimed: Claimed, Canceled: Canceled, - ContributableEvent: contributable_component::Event, - ContributionMade: ContributionMade, + Launched: Launched, + PledgeableEvent: pledgeable_component::Event, + PledgeMade: PledgeMade, Refunded: Refunded, - Upgraded: Upgraded, - Withdrawn: Withdrawn, RefundedAll: RefundedAll, + Unpledged: Unpledged, + Upgraded: Upgraded, } #[derive(Drop, starknet::Event)] - pub struct Activated {} - - #[derive(Drop, starknet::Event)] - pub struct ContributionMade { - #[key] - pub contributor: ContractAddress, - pub amount: u256, + pub struct Canceled { + pub reason: ByteArray, + pub status: Status, } #[derive(Drop, starknet::Event)] @@ -108,9 +104,13 @@ pub mod Campaign { } #[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 contributor: ContractAddress, + pub amount: u256, } #[derive(Drop, starknet::Event)] @@ -127,18 +127,18 @@ pub mod Campaign { } #[derive(Drop, starknet::Event)] - pub struct Upgraded { - pub implementation: ClassHash - } - - #[derive(Drop, starknet::Event)] - pub struct Withdrawn { + pub struct Unpledged { #[key] pub contributor: ContractAddress, pub amount: u256, pub reason: ByteArray, } + #[derive(Drop, starknet::Event)] + pub struct Upgraded { + pub implementation: ClassHash + } + pub mod Errors { pub const NOT_CREATOR: felt252 = 'Not creator'; pub const ENDED: felt252 = 'Campaign already ended'; @@ -159,7 +159,7 @@ pub mod Campaign { 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'; + pub const NOTHING_TO_WITHDRAW: felt252 = 'Nothing to unpledge'; pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; } @@ -204,7 +204,7 @@ pub mod Campaign { self.status.write(Status::SUCCESSFUL); - // no need to reset the contributions, as the campaign has ended + // no need to reset the pledges, as the campaign has ended // and the data can be used as a testament to how much was raised let owner = get_caller_address(); @@ -230,7 +230,7 @@ pub mod Campaign { self.emit(Event::Canceled(Canceled { reason, status })); } - fn contribute(ref self: ContractState, amount: u256) { + fn pledge(ref self: ContractState, amount: u256) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() == Status::ACTIVE && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -240,18 +240,18 @@ pub mod Campaign { 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.pledges.add(contributor, amount); + self.total_pledges.write(self.total_pledges.read() + amount); - self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + self.emit(Event::PledgeMade(PledgeMade { contributor, amount })); } - fn get_contribution(self: @ContractState, contributor: ContractAddress) -> u256 { - self.contributions.get(contributor) + fn get_pledge(self: @ContractState, contributor: ContractAddress) -> u256 { + self.pledges.get(contributor) } - fn get_contributions(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.contributions.get_contributions_as_arr() + fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.pledges.get_pledges_as_arr() } fn get_details(self: @ContractState) -> Details { @@ -263,11 +263,11 @@ pub mod Campaign { end_time: self.end_time.read(), status: self.status.read(), token: self.token.read().contract_address, - total_contributions: self.total_contributions.read(), + total_pledges: self.total_pledges.read(), } } - fn start(ref self: ContractState, duration: u64) { + fn launch(ref self: ContractState, duration: u64) { self._assert_only_creator(); assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); assert(duration > 0, Errors::ZERO_DURATION); @@ -275,7 +275,7 @@ pub mod Campaign { self.end_time.write(get_block_timestamp() + duration); self.status.write(Status::ACTIVE); - self.emit(Event::Activated(Activated {})); + self.emit(Event::Launched(Launched {})); } fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { @@ -283,7 +283,7 @@ pub mod Campaign { assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + assert(self.pledges.get(contributor) != 0, Errors::NOTHING_TO_REFUND); let amount = self._refund(contributor); @@ -312,17 +312,17 @@ pub mod Campaign { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - fn withdraw(ref self: ContractState, reason: ByteArray) { + fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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); + assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); let amount = self._refund(contributor); - self.emit(Event::Withdrawn(Withdrawn { contributor, amount, reason })); + self.emit(Event::Unpledged(Unpledged { contributor, amount, reason })); } } @@ -339,16 +339,16 @@ pub mod Campaign { } fn _is_target_reached(self: @ContractState) -> bool { - self.total_contributions.read() >= self.target.read() + self.total_pledges.read() >= self.target.read() } fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { - let amount = self.contributions.remove(contributor); + let amount = self.pledges.remove(contributor); - // if the campaign is "failed", then there's no need to set total_contributions to 0, as + // if the campaign is "failed", then there's no need to set total_pledges to 0, as // the campaign has ended and the field can be used as a testament to how much was raised if self.status.read() == Status::ACTIVE { - self.total_contributions.write(self.total_contributions.read() - amount); + self.total_pledges.write(self.total_pledges.read() - amount); } let success = self.token.read().transfer(contributor, amount); @@ -358,8 +358,8 @@ pub mod Campaign { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut contributions = self.contributions.get_contributions_as_arr(); - while let Option::Some((contributor, _)) = contributions + let mut pledges = self.pledges.get_pledges_as_arr(); + while let Option::Some((contributor, _)) = pledges .pop_front() { self._refund(contributor); }; diff --git a/listings/applications/crowdfunding/src/campaign/contributions.cairo b/listings/applications/crowdfunding/src/campaign/pledges.cairo similarity index 92% rename from listings/applications/crowdfunding/src/campaign/contributions.cairo rename to listings/applications/crowdfunding/src/campaign/pledges.cairo index d76316bb..ee99be74 100644 --- a/listings/applications/crowdfunding/src/campaign/contributions.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledges.cairo @@ -1,15 +1,15 @@ use starknet::ContractAddress; #[starknet::interface] -pub trait IContributable { +pub trait IPledgeable { fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); fn get(self: @TContractState, contributor: ContractAddress) -> u256; - fn get_contributions_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; + fn get_pledges_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; fn remove(ref self: TContractState, contributor: ContractAddress) -> u256; } #[starknet::component] -pub mod contributable_component { +pub mod pledgeable_component { use core::array::ArrayTrait; use starknet::{ContractAddress}; use core::num::traits::Zero; @@ -25,10 +25,10 @@ pub mod contributable_component { #[derive(Drop, starknet::Event)] pub enum Event {} - #[embeddable_as(Contributable)] - pub impl ContributableImpl< + #[embeddable_as(Pledgeable)] + pub impl PledgeableImpl< TContractState, +HasComponent - > of super::IContributable> { + > of super::IPledgeable> { fn add( ref self: ComponentState, contributor: ContractAddress, amount: u256 ) { @@ -55,7 +55,7 @@ pub mod contributable_component { } } - fn get_contributions_as_arr( + fn get_pledges_as_arr( self: @ComponentState ) -> Array<(ContractAddress, u256)> { let mut result = array![]; diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 771019fc..696a9113 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -8,31 +8,31 @@ pub mod MockUpgrade { 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::pledges::pledgeable_component; use crowdfunding::campaign::{ICampaign, Details, Status, Campaign::Errors}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); - component!(path: contributable_component, storage: contributions, event: ContributableEvent); + component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); #[abi(embed_v0)] pub impl OwnableImpl = ownable_component::Ownable; impl OwnableInternalImpl = ownable_component::OwnableInternalImpl; #[abi(embed_v0)] - impl ContributableImpl = contributable_component::Contributable; + impl PledgeableImpl = pledgeable_component::Pledgeable; #[storage] struct Storage { #[substorage(v0)] ownable: ownable_component::Storage, #[substorage(v0)] - contributions: contributable_component::Storage, + pledges: pledgeable_component::Storage, end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, target: u256, title: ByteArray, description: ByteArray, - total_contributions: u256, + total_pledges: u256, status: Status } @@ -42,22 +42,22 @@ pub mod MockUpgrade { pub enum Event { #[flat] OwnableEvent: ownable_component::Event, - Activated: Activated, + Launched: Launched, Claimed: Claimed, Canceled: Canceled, - ContributableEvent: contributable_component::Event, - ContributionMade: ContributionMade, + PledgeableEvent: pledgeable_component::Event, + PledgeMade: PledgeMade, Refunded: Refunded, Upgraded: Upgraded, - Withdrawn: Withdrawn, + Unpledged: Unpledged, RefundedAll: RefundedAll, } #[derive(Drop, starknet::Event)] - pub struct Activated {} + pub struct Launched {} #[derive(Drop, starknet::Event)] - pub struct ContributionMade { + pub struct PledgeMade { #[key] pub contributor: ContractAddress, pub amount: u256, @@ -93,7 +93,7 @@ pub mod MockUpgrade { } #[derive(Drop, starknet::Event)] - pub struct Withdrawn { + pub struct Unpledged { #[key] pub contributor: ContractAddress, pub amount: u256, @@ -139,7 +139,7 @@ pub mod MockUpgrade { self.status.write(Status::SUCCESSFUL); - // no need to reset the contributions, as the campaign has ended + // no need to reset the pledges, as the campaign has ended // and the data can be used as a testament to how much was raised let owner = get_caller_address(); @@ -165,7 +165,7 @@ pub mod MockUpgrade { self.emit(Event::Canceled(Canceled { reason, status })); } - fn contribute(ref self: ContractState, amount: u256) { + fn pledge(ref self: ContractState, amount: u256) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active() && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -175,18 +175,18 @@ pub mod MockUpgrade { 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.pledges.add(contributor, amount); + self.total_pledges.write(self.total_pledges.read() + amount); - self.emit(Event::ContributionMade(ContributionMade { contributor, amount })); + self.emit(Event::PledgeMade(PledgeMade { contributor, amount })); } - fn get_contribution(self: @ContractState, contributor: ContractAddress) -> u256 { - self.contributions.get(contributor) + fn get_pledge(self: @ContractState, contributor: ContractAddress) -> u256 { + self.pledges.get(contributor) } - fn get_contributions(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.contributions.get_contributions_as_arr() + fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { + self.pledges.get_pledges_as_arr() } fn get_details(self: @ContractState) -> Details { @@ -198,11 +198,11 @@ pub mod MockUpgrade { end_time: self.end_time.read(), status: self.status.read(), token: self.token.read().contract_address, - total_contributions: self.total_contributions.read(), + total_pledges: self.total_pledges.read(), } } - fn start(ref self: ContractState, duration: u64) { + fn launch(ref self: ContractState, duration: u64) { self._assert_only_creator(); assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); assert(duration > 0, Errors::ZERO_DURATION); @@ -210,7 +210,7 @@ pub mod MockUpgrade { self.end_time.write(get_block_timestamp() + duration); self.status.write(Status::ACTIVE); - self.emit(Event::Activated(Activated {})); + self.emit(Event::Launched(Launched {})); } fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { @@ -218,7 +218,7 @@ pub mod MockUpgrade { assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active(), Errors::ENDED); - assert(self.contributions.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + assert(self.pledges.get(contributor) != 0, Errors::NOTHING_TO_REFUND); let amount = self._refund(contributor); @@ -242,7 +242,7 @@ pub mod MockUpgrade { /// 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 + /// 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) { self.ownable._assert_only_owner(); @@ -260,7 +260,7 @@ pub mod MockUpgrade { }; assert(duration > 0, Errors::ZERO_DURATION); self._refund_all("contract upgraded"); - self.total_contributions.write(0); + self.total_pledges.write(0); self.end_time.write(get_block_timestamp() + duration); } @@ -269,23 +269,23 @@ pub mod MockUpgrade { self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); } - fn withdraw(ref self: ContractState, reason: ByteArray) { + fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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); + assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); - let amount = self.contributions.remove(contributor); + let amount = self.pledges.remove(contributor); - // no need to set total_contributions to 0, as the campaign has ended + // no need to set total_pledges 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, reason })); + self.emit(Event::Unpledged(Unpledged { contributor, amount, reason })); } } @@ -306,16 +306,16 @@ pub mod MockUpgrade { } fn _is_target_reached(self: @ContractState) -> bool { - self.total_contributions.read() >= self.target.read() + self.total_pledges.read() >= self.target.read() } fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { - let amount = self.contributions.remove(contributor); + let amount = self.pledges.remove(contributor); - // if the campaign is "failed", then there's no need to set total_contributions to 0, as + // if the campaign is "failed", then there's no need to set total_pledges to 0, as // the campaign has ended and the field can be used as a testament to how much was raised if self.status.read() == Status::ACTIVE { - self.total_contributions.write(self.total_contributions.read() - amount); + self.total_pledges.write(self.total_pledges.read() - amount); } let success = self.token.read().transfer(contributor, amount); @@ -325,8 +325,8 @@ pub mod MockUpgrade { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut contributions = self.contributions.get_contributions_as_arr(); - while let Option::Some((contributor, _)) = contributions + let mut pledges = self.pledges.get_pledges_as_arr(); + while let Option::Some((contributor, _)) = pledges .pop_front() { self._refund(contributor); }; diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 3790a2a4..ce7071e2 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -68,7 +68,7 @@ fn deploy_with_token( // deploy the actual Campaign contract let campaign_dispatcher = deploy(contract, "title 1", "description 1", 10000, token_address); - // approve the contributions for each contributor + // approve the pledges for each contributor start_cheat_caller_address(token_address, contributor_1); token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); start_cheat_caller_address(token_address, contributor_2); @@ -98,7 +98,7 @@ fn test_deploy() { assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, contract_address_const::<'token'>()); - assert_eq!(details.total_contributions, 0); + assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, contract_address_const::<'creator'>()); let owner: ContractAddress = contract_address_const::<'owner'>(); @@ -122,21 +122,21 @@ fn test_successful_campaign() { // start campaign start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(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 {}))] + @array![(campaign.contract_address, Campaign::Event::Launched(Campaign::Launched {}))] ); // 1st donation start_cheat_caller_address(campaign.contract_address, contributor_1); let mut prev_balance = token.balance_of(contributor_1); - campaign.contribute(3000); - assert_eq!(campaign.get_details().total_contributions, 3000); - assert_eq!(campaign.get_contribution(contributor_1), 3000); + campaign.pledge(3000); + assert_eq!(campaign.get_details().total_pledges, 3000); + assert_eq!(campaign.get_pledge(contributor_1), 3000); assert_eq!(token.balance_of(contributor_1), prev_balance - 3000); spy @@ -144,8 +144,8 @@ fn test_successful_campaign() { @array![ ( campaign.contract_address, - Campaign::Event::ContributionMade( - Campaign::ContributionMade { contributor: contributor_1, amount: 3000 } + Campaign::Event::PledgeMade( + Campaign::PledgeMade { contributor: contributor_1, amount: 3000 } ) ) ] @@ -154,9 +154,9 @@ fn test_successful_campaign() { // 2nd donation start_cheat_caller_address(campaign.contract_address, contributor_2); prev_balance = token.balance_of(contributor_2); - campaign.contribute(500); - assert_eq!(campaign.get_details().total_contributions, 3500); - assert_eq!(campaign.get_contribution(contributor_2), 500); + campaign.pledge(500); + assert_eq!(campaign.get_details().total_pledges, 3500); + assert_eq!(campaign.get_pledge(contributor_2), 500); assert_eq!(token.balance_of(contributor_2), prev_balance - 500); spy @@ -164,8 +164,8 @@ fn test_successful_campaign() { @array![ ( campaign.contract_address, - Campaign::Event::ContributionMade( - Campaign::ContributionMade { contributor: contributor_2, amount: 500 } + Campaign::Event::PledgeMade( + Campaign::PledgeMade { contributor: contributor_2, amount: 500 } ) ) ] @@ -174,9 +174,9 @@ fn test_successful_campaign() { // 3rd donation start_cheat_caller_address(campaign.contract_address, contributor_3); prev_balance = token.balance_of(contributor_3); - campaign.contribute(7000); - assert_eq!(campaign.get_details().total_contributions, 10500); - assert_eq!(campaign.get_contribution(contributor_3), 7000); + campaign.pledge(7000); + assert_eq!(campaign.get_details().total_pledges, 10500); + assert_eq!(campaign.get_pledge(contributor_3), 7000); assert_eq!(token.balance_of(contributor_3), prev_balance - 7000); spy @@ -184,8 +184,8 @@ fn test_successful_campaign() { @array![ ( campaign.contract_address, - Campaign::Event::ContributionMade( - Campaign::ContributionMade { contributor: contributor_3, amount: 7000 } + Campaign::Event::PledgeMade( + Campaign::PledgeMade { contributor: contributor_3, amount: 7000 } ) ) ] @@ -250,13 +250,13 @@ fn test_upgrade_class_hash() { let prev_balance_contributor_3 = token.balance_of(contributor_3); start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, contributor_1); - campaign.contribute(3000); + campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, contributor_2); - campaign.contribute(1000); + campaign.pledge(1000); start_cheat_caller_address(campaign.contract_address, contributor_3); - campaign.contribute(2000); + campaign.pledge(2000); start_cheat_caller_address(campaign.contract_address, owner); campaign.upgrade(new_class_hash, Option::Some(duration)); @@ -265,7 +265,7 @@ fn test_upgrade_class_hash() { 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().total_pledges, 0); assert_eq!(campaign.get_details().end_time, get_block_timestamp() + duration); spy @@ -303,14 +303,14 @@ fn test_close() { let prev_balance_contributor_3 = token.balance_of(contributor_3); start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, contributor_1); - campaign.contribute(3000); + campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, contributor_2); - campaign.contribute(1000); + campaign.pledge(1000); start_cheat_caller_address(campaign.contract_address, contributor_3); - campaign.contribute(2000); - let total_contributions = campaign.get_details().total_contributions; + campaign.pledge(2000); + let total_pledges = campaign.get_details().total_pledges; start_cheat_caller_address(campaign.contract_address, creator); campaign.cancel("testing"); @@ -319,7 +319,7 @@ fn test_close() { 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, total_contributions); + assert_eq!(campaign.get_details().total_pledges, total_pledges); assert_eq!(campaign.get_details().status, Status::CLOSED); spy @@ -350,14 +350,14 @@ fn test_close() { let prev_balance_contributor_3 = token.balance_of(contributor_3); start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, contributor_1); - campaign.contribute(3000); + campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, contributor_2); - campaign.contribute(1000); + campaign.pledge(1000); start_cheat_caller_address(campaign.contract_address, contributor_3); - campaign.contribute(2000); - let total_contributions = campaign.get_details().total_contributions; + campaign.pledge(2000); + let total_pledges = campaign.get_details().total_pledges; cheat_block_timestamp_global(duration); @@ -368,7 +368,7 @@ fn test_close() { 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, total_contributions); + assert_eq!(campaign.get_details().total_pledges, total_pledges); assert_eq!(campaign.get_details().status, Status::FAILED); spy @@ -403,18 +403,18 @@ fn test_refund() { // donate start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, contributor); - campaign.contribute(amount); - assert_eq!(campaign.get_details().total_contributions, amount); - assert_eq!(campaign.get_contribution(contributor), amount); + campaign.pledge(amount); + assert_eq!(campaign.get_details().total_pledges, amount); + assert_eq!(campaign.get_pledge(contributor), amount); assert_eq!(token.balance_of(contributor), prev_balance - amount); // refund start_cheat_caller_address(campaign.contract_address, creator); campaign.refund(contributor, "testing"); - assert_eq!(campaign.get_details().total_contributions, 0); - assert_eq!(campaign.get_contribution(contributor), 0); + assert_eq!(campaign.get_details().total_pledges, 0); + assert_eq!(campaign.get_pledge(contributor), 0); assert_eq!(token.balance_of(contributor), prev_balance); spy @@ -431,7 +431,7 @@ fn test_refund() { } #[test] -fn test_withdraw() { +fn test_unpledge() { // setup let duration: u64 = 60; let (campaign, token) = deploy_with_token( @@ -443,19 +443,19 @@ fn test_withdraw() { let amount: u256 = 3000; let prev_balance = token.balance_of(contributor); start_cheat_caller_address(campaign.contract_address, creator); - campaign.start(duration); + campaign.launch(duration); // donate start_cheat_caller_address(campaign.contract_address, contributor); - campaign.contribute(amount); - assert_eq!(campaign.get_details().total_contributions, amount); - assert_eq!(campaign.get_contribution(contributor), amount); + campaign.pledge(amount); + assert_eq!(campaign.get_details().total_pledges, amount); + assert_eq!(campaign.get_pledge(contributor), amount); assert_eq!(token.balance_of(contributor), prev_balance - amount); - // withdraw - campaign.withdraw("testing"); - assert_eq!(campaign.get_details().total_contributions, 0); - assert_eq!(campaign.get_contribution(contributor), 0); + // unpledge + campaign.unpledge("testing"); + assert_eq!(campaign.get_details().total_pledges, 0); + assert_eq!(campaign.get_pledge(contributor), 0); assert_eq!(token.balance_of(contributor), prev_balance); spy @@ -463,8 +463,8 @@ fn test_withdraw() { @array![ ( campaign.contract_address, - Campaign::Event::Withdrawn( - Campaign::Withdrawn { contributor, amount, reason: "testing" } + Campaign::Event::Unpledged( + Campaign::Unpledged { contributor, amount, reason: "testing" } ) ) ] From 411e8f7b832d96e5f2f19eabca8dc910fd0740f7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:06:11 +0200 Subject: [PATCH 090/116] target->goal --- .../advanced_factory/src/contract.cairo | 6 ++--- .../advanced_factory/src/tests.cairo | 9 ++++---- .../crowdfunding/src/campaign.cairo | 22 +++++++++---------- .../crowdfunding/src/mock_upgrade.cairo | 20 ++++++++--------- .../applications/crowdfunding/src/tests.cairo | 6 ++--- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo index c56c505c..69eb889c 100644 --- a/listings/applications/advanced_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -7,7 +7,7 @@ pub trait ICampaignFactory { ref self: TContractState, title: ByteArray, description: ByteArray, - target: u256, + goal: u256, token_address: ContractAddress ) -> ContractAddress; fn get_campaign_class_hash(self: @TContractState) -> ClassHash; @@ -92,14 +92,14 @@ pub mod CampaignFactory { ref self: ContractState, title: ByteArray, description: ByteArray, - target: u256, + goal: u256, token_address: ContractAddress, ) -> ContractAddress { let creator = get_caller_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((creator, title, description, target), token_address) + ((creator, title, description, goal), token_address) .serialize(ref constructor_calldata); // Contract deployment diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 8233f664..41966959 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -12,7 +12,7 @@ use snforge_std::{ stop_cheat_caller_address, spy_events, SpyOn, EventSpy, EventAssertions, get_class_hash }; -// Define a target contract to deploy +// Define a goal contract to deploy use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; @@ -63,17 +63,16 @@ fn test_create_campaign() { let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; - let target: u256 = 10000; + let goal: u256 = 10000; let token = contract_address_const::<'token'>(); - let campaign_address = factory - .create_campaign(title.clone(), description.clone(), target, token); + let campaign_address = factory.create_campaign(title.clone(), description.clone(), goal, 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.target, target); + assert_eq!(details.goal, goal); assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, token); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 6bee4adb..f7a5a413 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -15,7 +15,7 @@ pub enum Status { #[derive(Drop, Serde)] pub struct Details { pub creator: ContractAddress, - pub target: u256, + pub goal: u256, pub title: ByteArray, pub end_time: u64, pub description: ByteArray, @@ -69,7 +69,7 @@ pub mod Campaign { end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, - target: u256, + goal: u256, title: ByteArray, description: ByteArray, total_pledges: u256, @@ -169,18 +169,18 @@ pub mod Campaign { creator: ContractAddress, title: ByteArray, description: ByteArray, - target: u256, + goal: 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); + assert(goal > 0, Errors::ZERO_TARGET); self.token.write(IERC20Dispatcher { contract_address: token_address }); self.title.write(title); - self.target.write(target); + self.goal.write(goal); self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); @@ -194,7 +194,7 @@ pub mod Campaign { assert( self.status.read() == Status::ACTIVE && self._is_expired(), Errors::STILL_ACTIVE ); - assert(self._is_target_reached(), Errors::TARGET_NOT_REACHED); + assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); let token = self.token.read(); @@ -218,7 +218,7 @@ pub mod Campaign { self._assert_only_creator(); assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - if !self._is_target_reached() && self._is_expired() { + if !self._is_goal_reached() && self._is_expired() { self.status.write(Status::FAILED); } else { self.status.write(Status::CLOSED); @@ -259,7 +259,7 @@ pub mod Campaign { creator: self.creator.read(), title: self.title.read(), description: self.description.read(), - target: self.target.read(), + goal: self.goal.read(), end_time: self.end_time.read(), status: self.status.read(), token: self.token.read().contract_address, @@ -316,7 +316,7 @@ pub mod Campaign { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); @@ -338,8 +338,8 @@ pub mod Campaign { get_block_timestamp() >= self.end_time.read() } - fn _is_target_reached(self: @ContractState) -> bool { - self.total_pledges.read() >= self.target.read() + fn _is_goal_reached(self: @ContractState) -> bool { + self.total_pledges.read() >= self.goal.read() } fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 696a9113..018da060 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -29,7 +29,7 @@ pub mod MockUpgrade { end_time: u64, token: IERC20Dispatcher, creator: ContractAddress, - target: u256, + goal: u256, title: ByteArray, description: ByteArray, total_pledges: u256, @@ -106,18 +106,18 @@ pub mod MockUpgrade { creator: ContractAddress, title: ByteArray, description: ByteArray, - target: u256, + goal: 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); + assert(goal > 0, Errors::ZERO_TARGET); self.token.write(IERC20Dispatcher { contract_address: token_address }); self.title.write(title); - self.target.write(target); + self.goal.write(goal); self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); @@ -129,7 +129,7 @@ pub mod MockUpgrade { 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); + assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); let this = get_contract_address(); let token = self.token.read(); @@ -153,7 +153,7 @@ pub mod MockUpgrade { self._assert_only_creator(); assert(self._is_active(), Errors::ENDED); - if !self._is_target_reached() && self._is_expired() { + if !self._is_goal_reached() && self._is_expired() { self.status.write(Status::FAILED); } else { self.status.write(Status::CLOSED); @@ -194,7 +194,7 @@ pub mod MockUpgrade { creator: self.creator.read(), title: self.title.read(), description: self.description.read(), - target: self.target.read(), + goal: self.goal.read(), end_time: self.end_time.read(), status: self.status.read(), token: self.token.read().contract_address, @@ -273,7 +273,7 @@ pub mod MockUpgrade { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); 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._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); let contributor = get_caller_address(); @@ -305,8 +305,8 @@ pub mod MockUpgrade { self.status.read() == Status::ACTIVE } - fn _is_target_reached(self: @ContractState) -> bool { - self.total_pledges.read() >= self.target.read() + fn _is_goal_reached(self: @ContractState) -> bool { + self.total_pledges.read() >= self.goal.read() } fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index ce7071e2..078aa55a 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -22,12 +22,12 @@ fn deploy( contract: ContractClass, title: ByteArray, description: ByteArray, - target: u256, + goal: u256, token: ContractAddress ) -> ICampaignDispatcher { let creator = contract_address_const::<'creator'>(); let mut calldata: Array:: = array![]; - ((creator, title, description, target), token).serialize(ref calldata); + ((creator, title, description, goal), token).serialize(ref calldata); let contract_address = contract.precalculate_address(@calldata); let owner = contract_address_const::<'owner'>(); @@ -94,7 +94,7 @@ fn test_deploy() { 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.goal, 10000); assert_eq!(details.end_time, 0); assert_eq!(details.status, Status::DRAFT); assert_eq!(details.token, contract_address_const::<'token'>()); From d8809b78b6aaa6bd190b0d53ae5c49418ebed51e Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:07:56 +0200 Subject: [PATCH 091/116] remove comment --- listings/applications/crowdfunding/src/campaign.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index f7a5a413..ce6aa615 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -171,7 +171,6 @@ pub mod Campaign { description: ByteArray, goal: u256, token_address: ContractAddress, - // TODO: add recepient address? ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); From 097e9b63dc07216e683f7a14edd04d40bec755f1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:18:05 +0200 Subject: [PATCH 092/116] err CLOSED->CANCELED + check active in unpledge --- listings/applications/crowdfunding/src/campaign.cairo | 9 ++++----- .../applications/crowdfunding/src/mock_upgrade.cairo | 6 +++--- listings/applications/crowdfunding/src/tests.cairo | 8 ++++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index ce6aa615..3a7c75bf 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -6,7 +6,7 @@ use starknet::{ClassHash, ContractAddress}; #[derive(Drop, Debug, Serde, PartialEq, starknet::Store)] pub enum Status { ACTIVE, - CLOSED, + CANCELED, DRAFT, SUCCESSFUL, FAILED, @@ -145,7 +145,7 @@ pub mod Campaign { pub const NOT_DRAFT: felt252 = 'Campaign not draft'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const STILL_DRAFT: felt252 = 'Campaign not yet active'; - pub const CLOSED: felt252 = 'Campaign closed'; + 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'; @@ -220,7 +220,7 @@ pub mod Campaign { if !self._is_goal_reached() && self._is_expired() { self.status.write(Status::FAILED); } else { - self.status.write(Status::CLOSED); + self.status.write(Status::CANCELED); } self._refund_all(reason.clone()); @@ -313,8 +313,7 @@ pub mod Campaign { fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); - assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); - assert(self.status.read() != Status::CLOSED, Errors::CLOSED); + 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); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 018da060..1b071841 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -156,7 +156,7 @@ pub mod MockUpgrade { if !self._is_goal_reached() && self._is_expired() { self.status.write(Status::FAILED); } else { - self.status.write(Status::CLOSED); + self.status.write(Status::CANCELED); } self._refund_all(reason.clone()); @@ -241,7 +241,7 @@ pub mod MockUpgrade { /// - 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? + /// - 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) { @@ -272,7 +272,7 @@ pub mod MockUpgrade { fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); - assert(self.status.read() != Status::CLOSED, Errors::CLOSED); + assert(self.status.read() != Status::CANCELED, Errors::CANCELED); assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 078aa55a..bcdc475a 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -286,12 +286,12 @@ fn test_upgrade_class_hash() { } #[test] -fn test_close() { +fn test_cancel() { let contract_class = declare("Campaign").unwrap(); let token_class = declare("ERC20").unwrap(); let duration: u64 = 60; - // test closed campaign + // test canceled campaign let (campaign, token) = deploy_with_token(contract_class, token_class); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let creator = contract_address_const::<'creator'>(); @@ -320,7 +320,7 @@ fn test_close() { 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_pledges, total_pledges); - assert_eq!(campaign.get_details().status, Status::CLOSED); + assert_eq!(campaign.get_details().status, Status::CANCELED); spy .assert_emitted( @@ -332,7 +332,7 @@ fn test_close() { ( campaign.contract_address, Campaign::Event::Canceled( - Campaign::Canceled { reason: "testing", status: Status::CLOSED } + Campaign::Canceled { reason: "testing", status: Status::CANCELED } ) ) ] From 59e51179e43cf797d54940833301225e0b821749 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:24:09 +0200 Subject: [PATCH 093/116] contributor->pledger --- .../crowdfunding/src/campaign.cairo | 51 +++--- .../crowdfunding/src/campaign/pledges.cairo | 74 ++++---- .../crowdfunding/src/mock_upgrade.cairo | 59 ++++--- .../applications/crowdfunding/src/tests.cairo | 164 +++++++++--------- 4 files changed, 172 insertions(+), 176 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 3a7c75bf..9ece6bcc 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -29,11 +29,11 @@ pub trait ICampaign { fn claim(ref self: TContractState); fn cancel(ref self: TContractState, reason: ByteArray); fn pledge(ref self: TContractState, amount: u256); - fn get_pledge(self: @TContractState, contributor: ContractAddress) -> u256; + fn get_pledge(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledges(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; fn launch(ref self: TContractState, duration: u64); - fn refund(ref self: TContractState, contributor: ContractAddress, reason: ByteArray); + fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); fn unpledge(ref self: TContractState, reason: ByteArray); } @@ -109,14 +109,14 @@ pub mod Campaign { #[derive(Drop, starknet::Event)] pub struct PledgeMade { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, } #[derive(Drop, starknet::Event)] pub struct Refunded { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, pub reason: ByteArray, } @@ -129,7 +129,7 @@ pub mod Campaign { #[derive(Drop, starknet::Event)] pub struct Unpledged { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, pub reason: ByteArray, } @@ -234,19 +234,19 @@ pub mod Campaign { assert(self.status.read() == Status::ACTIVE && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); - let contributor = get_caller_address(); + let pledger = get_caller_address(); let this = get_contract_address(); - let success = self.token.read().transfer_from(contributor, this, amount); + let success = self.token.read().transfer_from(pledger, this, amount); assert(success, Errors::TRANSFER_FAILED); - self.pledges.add(contributor, amount); + self.pledges.add(pledger, amount); self.total_pledges.write(self.total_pledges.read() + amount); - self.emit(Event::PledgeMade(PledgeMade { contributor, amount })); + self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } - fn get_pledge(self: @ContractState, contributor: ContractAddress) -> u256 { - self.pledges.get(contributor) + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) } fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { @@ -277,16 +277,16 @@ pub mod Campaign { self.emit(Event::Launched(Launched {})); } - fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { + fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); + assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self.status.read() == Status::ACTIVE, Errors::ENDED); - assert(self.pledges.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); - let amount = self._refund(contributor); + let amount = self._refund(pledger); - self.emit(Event::Refunded(Refunded { contributor, amount, reason })) + self.emit(Event::Refunded(Refunded { pledger, amount, reason })) } fn upgrade(ref self: ContractState, impl_hash: ClassHash, new_duration: Option) { @@ -317,10 +317,10 @@ pub mod Campaign { assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); - let contributor = get_caller_address(); - let amount = self._refund(contributor); + let pledger = get_caller_address(); + let amount = self._refund(pledger); - self.emit(Event::Unpledged(Unpledged { contributor, amount, reason })); + self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); } } @@ -340,8 +340,8 @@ pub mod Campaign { self.total_pledges.read() >= self.goal.read() } - fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { - let amount = self.pledges.remove(contributor); + fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { + let amount = self.pledges.remove(pledger); // if the campaign is "failed", then there's no need to set total_pledges to 0, as // the campaign has ended and the field can be used as a testament to how much was raised @@ -349,7 +349,7 @@ pub mod Campaign { self.total_pledges.write(self.total_pledges.read() - amount); } - let success = self.token.read().transfer(contributor, amount); + let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); amount @@ -357,10 +357,9 @@ pub mod Campaign { fn _refund_all(ref self: ContractState, reason: ByteArray) { let mut pledges = self.pledges.get_pledges_as_arr(); - while let Option::Some((contributor, _)) = pledges - .pop_front() { - self._refund(contributor); - }; + while let Option::Some((pledger, _)) = pledges.pop_front() { + self._refund(pledger); + }; self.emit(Event::RefundedAll(RefundedAll { reason })); } } diff --git a/listings/applications/crowdfunding/src/campaign/pledges.cairo b/listings/applications/crowdfunding/src/campaign/pledges.cairo index ee99be74..859286f3 100644 --- a/listings/applications/crowdfunding/src/campaign/pledges.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledges.cairo @@ -2,10 +2,10 @@ use starknet::ContractAddress; #[starknet::interface] pub trait IPledgeable { - fn add(ref self: TContractState, contributor: ContractAddress, amount: u256); - fn get(self: @TContractState, contributor: ContractAddress) -> u256; + fn add(ref self: TContractState, pledger: ContractAddress, amount: u256); + fn get(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledges_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; - fn remove(ref self: TContractState, contributor: ContractAddress) -> u256; + fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; } #[starknet::component] @@ -16,9 +16,9 @@ pub mod pledgeable_component { #[storage] struct Storage { - index_to_contributor: LegacyMap, - contributor_to_amount_index: LegacyMap>, - contributor_count: u32, + index_to_pledger: LegacyMap, + pledger_to_amount_index: LegacyMap>, + pledger_count: u32, } #[event] @@ -29,26 +29,24 @@ pub mod pledgeable_component { pub impl PledgeableImpl< TContractState, +HasComponent > of super::IPledgeable> { - fn add( - ref self: ComponentState, contributor: ContractAddress, amount: u256 - ) { + fn add(ref self: ComponentState, pledger: ContractAddress, amount: u256) { let amount_index_option: Option<(u256, u32)> = self - .contributor_to_amount_index - .read(contributor); + .pledger_to_amount_index + .read(pledger); if let Option::Some((old_amount, index)) = amount_index_option { self - .contributor_to_amount_index - .write(contributor, Option::Some((old_amount + amount, index))); + .pledger_to_amount_index + .write(pledger, Option::Some((old_amount + amount, index))); } else { - let index = self.contributor_count.read(); - self.index_to_contributor.write(index, contributor); - self.contributor_to_amount_index.write(contributor, Option::Some((amount, index))); - self.contributor_count.write(index + 1); + let index = self.pledger_count.read(); + self.index_to_pledger.write(index, pledger); + self.pledger_to_amount_index.write(pledger, Option::Some((amount, index))); + self.pledger_count.write(index + 1); } } - fn get(self: @ComponentState, contributor: ContractAddress) -> u256 { - let val: Option<(u256, u32)> = self.contributor_to_amount_index.read(contributor); + fn get(self: @ComponentState, pledger: ContractAddress) -> u256 { + let val: Option<(u256, u32)> = self.pledger_to_amount_index.read(pledger); match val { Option::Some((amount, _)) => amount, Option::None => 0, @@ -60,47 +58,47 @@ pub mod pledgeable_component { ) -> Array<(ContractAddress, u256)> { let mut result = array![]; - let mut index = self.contributor_count.read(); + let mut index = self.pledger_count.read(); while index != 0 { index -= 1; - let contributor = self.index_to_contributor.read(index); + let pledger = self.index_to_pledger.read(index); let amount_index_option: Option<(u256, u32)> = self - .contributor_to_amount_index - .read(contributor); + .pledger_to_amount_index + .read(pledger); let amount = match amount_index_option { Option::Some((amount, _)) => amount, Option::None => 0 }; - result.append((contributor, amount)); + result.append((pledger, amount)); }; result } - fn remove(ref self: ComponentState, contributor: ContractAddress) -> u256 { + fn remove(ref self: ComponentState, pledger: ContractAddress) -> u256 { let amount_index_option: Option<(u256, u32)> = self - .contributor_to_amount_index - .read(contributor); + .pledger_to_amount_index + .read(pledger); if let Option::Some((amount, index)) = amount_index_option { - self.contributor_to_amount_index.write(contributor, Option::None); - let contributor_count = self.contributor_count.read() - 1; - self.contributor_count.write(contributor_count); - if contributor_count != 0 { - let last_contributor = self.index_to_contributor.read(contributor_count); + self.pledger_to_amount_index.write(pledger, Option::None); + let pledger_count = self.pledger_count.read() - 1; + self.pledger_count.write(pledger_count); + if pledger_count != 0 { + let last_pledger = self.index_to_pledger.read(pledger_count); let last_amount_index: Option<(u256, u32)> = self - .contributor_to_amount_index - .read(last_contributor); + .pledger_to_amount_index + .read(last_pledger); let last_amount = match last_amount_index { Option::Some((last_amount, _)) => last_amount, Option::None => 0 }; self - .contributor_to_amount_index - .write(last_contributor, Option::Some((last_amount, index))); - self.index_to_contributor.write(index, last_contributor); + .pledger_to_amount_index + .write(last_pledger, Option::Some((last_amount, index))); + self.index_to_pledger.write(index, last_pledger); } - self.index_to_contributor.write(contributor_count, Zero::zero()); + self.index_to_pledger.write(pledger_count, Zero::zero()); amount } else { diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 1b071841..9700a2ba 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -59,7 +59,7 @@ pub mod MockUpgrade { #[derive(Drop, starknet::Event)] pub struct PledgeMade { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, } @@ -77,7 +77,7 @@ pub mod MockUpgrade { #[derive(Drop, starknet::Event)] pub struct Refunded { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, pub reason: ByteArray, } @@ -95,7 +95,7 @@ pub mod MockUpgrade { #[derive(Drop, starknet::Event)] pub struct Unpledged { #[key] - pub contributor: ContractAddress, + pub pledger: ContractAddress, pub amount: u256, pub reason: ByteArray, } @@ -170,19 +170,19 @@ pub mod MockUpgrade { assert(self._is_active() && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); - let contributor = get_caller_address(); + let pledger = get_caller_address(); let this = get_contract_address(); - let success = self.token.read().transfer_from(contributor, this, amount); + let success = self.token.read().transfer_from(pledger, this, amount); assert(success, Errors::TRANSFER_FAILED); - self.pledges.add(contributor, amount); + self.pledges.add(pledger, amount); self.total_pledges.write(self.total_pledges.read() + amount); - self.emit(Event::PledgeMade(PledgeMade { contributor, amount })); + self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } - fn get_pledge(self: @ContractState, contributor: ContractAddress) -> u256 { - self.pledges.get(contributor) + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) } fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { @@ -213,32 +213,32 @@ pub mod MockUpgrade { self.emit(Event::Launched(Launched {})); } - fn refund(ref self: ContractState, contributor: ContractAddress, reason: ByteArray) { + fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - assert(contributor.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); + assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); assert(self._is_active(), Errors::ENDED); - assert(self.pledges.get(contributor) != 0, Errors::NOTHING_TO_REFUND); + assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); - let amount = self._refund(contributor); + let amount = self._refund(pledger); - self.emit(Event::Refunded(Refunded { contributor, amount, reason })) + 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 contributors, + /// 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 contributors already trust the campaign creator that they'll do what they promised in the campaign. + /// 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 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. + /// 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: - /// - 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 + /// - 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? @@ -276,16 +276,16 @@ pub mod MockUpgrade { assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_WITHDRAW); - let contributor = get_caller_address(); - let amount = self.pledges.remove(contributor); + let pledger = get_caller_address(); + let amount = self.pledges.remove(pledger); // no need to set total_pledges 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); + let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); - self.emit(Event::Unpledged(Unpledged { contributor, amount, reason })); + self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); } } @@ -309,8 +309,8 @@ pub mod MockUpgrade { self.total_pledges.read() >= self.goal.read() } - fn _refund(ref self: ContractState, contributor: ContractAddress) -> u256 { - let amount = self.pledges.remove(contributor); + fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { + let amount = self.pledges.remove(pledger); // if the campaign is "failed", then there's no need to set total_pledges to 0, as // the campaign has ended and the field can be used as a testament to how much was raised @@ -318,7 +318,7 @@ pub mod MockUpgrade { self.total_pledges.write(self.total_pledges.read() - amount); } - let success = self.token.read().transfer(contributor, amount); + let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); amount @@ -326,10 +326,9 @@ pub mod MockUpgrade { fn _refund_all(ref self: ContractState, reason: ByteArray) { let mut pledges = self.pledges.get_pledges_as_arr(); - while let Option::Some((contributor, _)) = pledges - .pop_front() { - self._refund(contributor); - }; + while let Option::Some((pledger, _)) = pledges.pop_front() { + self._refund(pledger); + }; self.emit(Event::RefundedAll(RefundedAll { reason })); } } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index bcdc475a..be2b4b26 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -54,26 +54,26 @@ fn deploy_with_token( (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'>(); + // transfer amounts to some pledgers + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); 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); + token_dispatcher.transfer(pledger_1, 10000); + token_dispatcher.transfer(pledger_2, 10000); + token_dispatcher.transfer(pledger_3, 10000); // deploy the actual Campaign contract let campaign_dispatcher = deploy(contract, "title 1", "description 1", 10000, token_address); - // approve the pledges for each contributor - start_cheat_caller_address(token_address, contributor_1); + // approve the pledges for each pledger + start_cheat_caller_address(token_address, pledger_1); token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); - start_cheat_caller_address(token_address, contributor_2); + start_cheat_caller_address(token_address, pledger_2); token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); - start_cheat_caller_address(token_address, contributor_3); + start_cheat_caller_address(token_address, pledger_3); token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); // NOTE: don't forget to stop the caller address cheat on the ERC20 contract!! @@ -114,9 +114,9 @@ fn test_successful_campaign() { 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 pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); @@ -132,12 +132,12 @@ fn test_successful_campaign() { ); // 1st donation - start_cheat_caller_address(campaign.contract_address, contributor_1); - let mut prev_balance = token.balance_of(contributor_1); + start_cheat_caller_address(campaign.contract_address, pledger_1); + let mut prev_balance = token.balance_of(pledger_1); campaign.pledge(3000); assert_eq!(campaign.get_details().total_pledges, 3000); - assert_eq!(campaign.get_pledge(contributor_1), 3000); - assert_eq!(token.balance_of(contributor_1), prev_balance - 3000); + assert_eq!(campaign.get_pledge(pledger_1), 3000); + assert_eq!(token.balance_of(pledger_1), prev_balance - 3000); spy .assert_emitted( @@ -145,19 +145,19 @@ fn test_successful_campaign() { ( campaign.contract_address, Campaign::Event::PledgeMade( - Campaign::PledgeMade { contributor: contributor_1, amount: 3000 } + Campaign::PledgeMade { pledger: pledger_1, amount: 3000 } ) ) ] ); // 2nd donation - start_cheat_caller_address(campaign.contract_address, contributor_2); - prev_balance = token.balance_of(contributor_2); + start_cheat_caller_address(campaign.contract_address, pledger_2); + prev_balance = token.balance_of(pledger_2); campaign.pledge(500); assert_eq!(campaign.get_details().total_pledges, 3500); - assert_eq!(campaign.get_pledge(contributor_2), 500); - assert_eq!(token.balance_of(contributor_2), prev_balance - 500); + assert_eq!(campaign.get_pledge(pledger_2), 500); + assert_eq!(token.balance_of(pledger_2), prev_balance - 500); spy .assert_emitted( @@ -165,19 +165,19 @@ fn test_successful_campaign() { ( campaign.contract_address, Campaign::Event::PledgeMade( - Campaign::PledgeMade { contributor: contributor_2, amount: 500 } + Campaign::PledgeMade { pledger: pledger_2, amount: 500 } ) ) ] ); // 3rd donation - start_cheat_caller_address(campaign.contract_address, contributor_3); - prev_balance = token.balance_of(contributor_3); + start_cheat_caller_address(campaign.contract_address, pledger_3); + prev_balance = token.balance_of(pledger_3); campaign.pledge(7000); assert_eq!(campaign.get_details().total_pledges, 10500); - assert_eq!(campaign.get_pledge(contributor_3), 7000); - assert_eq!(token.balance_of(contributor_3), prev_balance - 7000); + assert_eq!(campaign.get_pledge(pledger_3), 7000); + assert_eq!(token.balance_of(pledger_3), prev_balance - 7000); spy .assert_emitted( @@ -185,7 +185,7 @@ fn test_successful_campaign() { ( campaign.contract_address, Campaign::Event::PledgeMade( - Campaign::PledgeMade { contributor: contributor_3, amount: 7000 } + Campaign::PledgeMade { pledger: pledger_3, amount: 7000 } ) ) ] @@ -242,29 +242,29 @@ fn test_upgrade_class_hash() { 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); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + let prev_balance_pledger_1 = token.balance_of(pledger_1); + let prev_balance_pledger_2 = token.balance_of(pledger_2); + let prev_balance_pledger_3 = token.balance_of(pledger_3); start_cheat_caller_address(campaign.contract_address, creator); campaign.launch(duration); - start_cheat_caller_address(campaign.contract_address, contributor_1); + start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); - start_cheat_caller_address(campaign.contract_address, contributor_2); + start_cheat_caller_address(campaign.contract_address, pledger_2); campaign.pledge(1000); - start_cheat_caller_address(campaign.contract_address, contributor_3); + start_cheat_caller_address(campaign.contract_address, pledger_3); campaign.pledge(2000); start_cheat_caller_address(campaign.contract_address, owner); campaign.upgrade(new_class_hash, Option::Some(duration)); stop_cheat_caller_address(campaign.contract_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!(prev_balance_pledger_1, token.balance_of(pledger_1)); + assert_eq!(prev_balance_pledger_2, token.balance_of(pledger_2)); + assert_eq!(prev_balance_pledger_3, token.balance_of(pledger_3)); assert_eq!(campaign.get_details().total_pledges, 0); assert_eq!(campaign.get_details().end_time, get_block_timestamp() + duration); @@ -295,20 +295,20 @@ fn test_cancel() { let (campaign, token) = deploy_with_token(contract_class, token_class); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); 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); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + let prev_balance_pledger_1 = token.balance_of(pledger_1); + let prev_balance_pledger_2 = token.balance_of(pledger_2); + let prev_balance_pledger_3 = token.balance_of(pledger_3); start_cheat_caller_address(campaign.contract_address, creator); campaign.launch(duration); - start_cheat_caller_address(campaign.contract_address, contributor_1); + start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); - start_cheat_caller_address(campaign.contract_address, contributor_2); + start_cheat_caller_address(campaign.contract_address, pledger_2); campaign.pledge(1000); - start_cheat_caller_address(campaign.contract_address, contributor_3); + start_cheat_caller_address(campaign.contract_address, pledger_3); campaign.pledge(2000); let total_pledges = campaign.get_details().total_pledges; @@ -316,9 +316,9 @@ fn test_cancel() { campaign.cancel("testing"); stop_cheat_caller_address(campaign.contract_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!(prev_balance_pledger_1, token.balance_of(pledger_1)); + assert_eq!(prev_balance_pledger_2, token.balance_of(pledger_2)); + assert_eq!(prev_balance_pledger_3, token.balance_of(pledger_3)); assert_eq!(campaign.get_details().total_pledges, total_pledges); assert_eq!(campaign.get_details().status, Status::CANCELED); @@ -342,20 +342,20 @@ fn test_cancel() { let (campaign, token) = deploy_with_token(contract_class, token_class); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); 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); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + let prev_balance_pledger_1 = token.balance_of(pledger_1); + let prev_balance_pledger_2 = token.balance_of(pledger_2); + let prev_balance_pledger_3 = token.balance_of(pledger_3); start_cheat_caller_address(campaign.contract_address, creator); campaign.launch(duration); - start_cheat_caller_address(campaign.contract_address, contributor_1); + start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); - start_cheat_caller_address(campaign.contract_address, contributor_2); + start_cheat_caller_address(campaign.contract_address, pledger_2); campaign.pledge(1000); - start_cheat_caller_address(campaign.contract_address, contributor_3); + start_cheat_caller_address(campaign.contract_address, pledger_3); campaign.pledge(2000); let total_pledges = campaign.get_details().total_pledges; @@ -365,9 +365,9 @@ fn test_cancel() { campaign.cancel("testing"); stop_cheat_caller_address(campaign.contract_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!(prev_balance_pledger_1, token.balance_of(pledger_1)); + assert_eq!(prev_balance_pledger_2, token.balance_of(pledger_2)); + assert_eq!(prev_balance_pledger_3, token.balance_of(pledger_3)); assert_eq!(campaign.get_details().total_pledges, total_pledges); assert_eq!(campaign.get_details().status, Status::FAILED); @@ -397,25 +397,25 @@ fn test_refund() { ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let creator = contract_address_const::<'creator'>(); - let contributor = contract_address_const::<'contributor_1'>(); + let pledger = contract_address_const::<'pledger_1'>(); let amount: u256 = 3000; - let prev_balance = token.balance_of(contributor); + let prev_balance = token.balance_of(pledger); // donate start_cheat_caller_address(campaign.contract_address, creator); campaign.launch(duration); - start_cheat_caller_address(campaign.contract_address, contributor); + start_cheat_caller_address(campaign.contract_address, pledger); campaign.pledge(amount); assert_eq!(campaign.get_details().total_pledges, amount); - assert_eq!(campaign.get_pledge(contributor), amount); - assert_eq!(token.balance_of(contributor), prev_balance - amount); + assert_eq!(campaign.get_pledge(pledger), amount); + assert_eq!(token.balance_of(pledger), prev_balance - amount); // refund start_cheat_caller_address(campaign.contract_address, creator); - campaign.refund(contributor, "testing"); + campaign.refund(pledger, "testing"); assert_eq!(campaign.get_details().total_pledges, 0); - assert_eq!(campaign.get_pledge(contributor), 0); - assert_eq!(token.balance_of(contributor), prev_balance); + assert_eq!(campaign.get_pledge(pledger), 0); + assert_eq!(token.balance_of(pledger), prev_balance); spy .assert_emitted( @@ -423,7 +423,7 @@ fn test_refund() { ( campaign.contract_address, Campaign::Event::Refunded( - Campaign::Refunded { contributor, amount, reason: "testing" } + Campaign::Refunded { pledger, amount, reason: "testing" } ) ) ] @@ -439,24 +439,24 @@ fn test_unpledge() { ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let creator = contract_address_const::<'creator'>(); - let contributor = contract_address_const::<'contributor_1'>(); + let pledger = contract_address_const::<'pledger_1'>(); let amount: u256 = 3000; - let prev_balance = token.balance_of(contributor); + let prev_balance = token.balance_of(pledger); start_cheat_caller_address(campaign.contract_address, creator); campaign.launch(duration); // donate - start_cheat_caller_address(campaign.contract_address, contributor); + start_cheat_caller_address(campaign.contract_address, pledger); campaign.pledge(amount); assert_eq!(campaign.get_details().total_pledges, amount); - assert_eq!(campaign.get_pledge(contributor), amount); - assert_eq!(token.balance_of(contributor), prev_balance - amount); + assert_eq!(campaign.get_pledge(pledger), amount); + assert_eq!(token.balance_of(pledger), prev_balance - amount); // unpledge campaign.unpledge("testing"); assert_eq!(campaign.get_details().total_pledges, 0); - assert_eq!(campaign.get_pledge(contributor), 0); - assert_eq!(token.balance_of(contributor), prev_balance); + assert_eq!(campaign.get_pledge(pledger), 0); + assert_eq!(token.balance_of(pledger), prev_balance); spy .assert_emitted( @@ -464,7 +464,7 @@ fn test_unpledge() { ( campaign.contract_address, Campaign::Event::Unpledged( - Campaign::Unpledged { contributor, amount, reason: "testing" } + Campaign::Unpledged { pledger, amount, reason: "testing" } ) ) ] From 123229ca1941588a050c93ff320652051d3c3ca0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:49:37 +0200 Subject: [PATCH 094/116] add campaign doc content --- src/applications/crowdfunding.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md index 25f1bcf2..3cf9bbec 100644 --- a/src/applications/crowdfunding.md +++ b/src/applications/crowdfunding.md @@ -1,7 +1,17 @@ # Crowdfunding Campaign -This is the Crowdfunding Campaign contract. +Crowdfunding is a method of raising capital through the collective effort of many individuals. It allows project creators to raise funds from a large number of people, usually through small contributions. + +1. Contract admin creates a campaign in some user's name (i.e. creator). +2. Users can pledge, transferring their token to a campaign. +3. Users can "unpledge", retrieving their tokens. +4. The creator can at any point refund any of the users +5. Once the total amount pledged is more than the campaign goal, the campaign funds are "locked" in the contract, meaning the users can no longer unpledge +6. After the campaign ends, the campaign creator can claim the funds if the campaign goal is reached. +7. Otherwise, campaign did not reach it's goal, the creator can mark the campaign as "failed" and refund all of the pledgers. +8. The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers. +9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state ```rust -{{#include ../../../listings/applications/crowdfunding/src/contract.cairo:contract}} +{{#include ../../../listings/applications/crowdfunding/src/campaign.cairo:contract}} ``` From a97f44918f0cda626f6d27430ef61d98aaa86ce9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 08:50:05 +0200 Subject: [PATCH 095/116] remove draft status --- .../advanced_factory/src/contract.cairo | 4 +- .../advanced_factory/src/tests.cairo | 17 +++---- .../crowdfunding/src/campaign.cairo | 32 ++++--------- .../crowdfunding/src/mock_upgrade.cairo | 40 +++++----------- .../applications/crowdfunding/src/tests.cairo | 46 ++++--------------- 5 files changed, 40 insertions(+), 99 deletions(-) diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo index 69eb889c..7d777bae 100644 --- a/listings/applications/advanced_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -8,6 +8,7 @@ pub trait ICampaignFactory { title: ByteArray, description: ByteArray, goal: u256, + duration: u64, token_address: ContractAddress ) -> ContractAddress; fn get_campaign_class_hash(self: @TContractState) -> ClassHash; @@ -93,13 +94,14 @@ pub mod CampaignFactory { title: ByteArray, description: ByteArray, goal: u256, + duration: u64, token_address: ContractAddress, ) -> ContractAddress { let creator = get_caller_address(); // Create contructor arguments let mut constructor_calldata: Array:: = array![]; - ((creator, title, description, goal), token_address) + ((creator, title, description, goal), duration, token_address) .serialize(ref constructor_calldata); // Contract deployment diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 41966959..0fd5c37e 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -64,17 +64,19 @@ fn test_create_campaign() { let title: ByteArray = "New campaign"; let description: ByteArray = "Some description"; let goal: u256 = 10000; + let duration: u64 = 60; let token = contract_address_const::<'token'>(); - let campaign_address = factory.create_campaign(title.clone(), description.clone(), goal, token); + let campaign_address = factory + .create_campaign(title.clone(), description.clone(), goal, duration, 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, 0); - assert_eq!(details.status, Status::DRAFT); + assert_eq!(details.end_time, get_block_timestamp() + duration); + assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, token); assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, campaign_creator); @@ -108,19 +110,14 @@ fn test_uprade_campaign_class_hash() { // deploy a pending campaign with the old class hash 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, token); + let pending_campaign = factory.create_campaign("title 1", "description 1", 10000, 60, token); assert_eq!(old_class_hash, get_class_hash(pending_campaign)); // deploy an active campaign with the old class hash 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, token); - stop_cheat_caller_address(factory.contract_address); - - start_cheat_caller_address(active_campaign, active_campaign_creator); - ICampaignDispatcher { contract_address: active_campaign }.launch(60); - stop_cheat_caller_address(active_campaign); + let active_campaign = factory.create_campaign("title 2", "description 2", 20000, 100, token); assert_eq!(old_class_hash, get_class_hash(active_campaign)); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 9ece6bcc..ce6bc490 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -7,7 +7,6 @@ use starknet::{ClassHash, ContractAddress}; pub enum Status { ACTIVE, CANCELED, - DRAFT, SUCCESSFUL, FAILED, } @@ -32,7 +31,6 @@ pub trait ICampaign { fn get_pledge(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledges(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_details(self: @TContractState) -> Details; - fn launch(ref self: TContractState, duration: u64); fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_duration: Option); fn unpledge(ref self: TContractState, reason: ByteArray); @@ -142,9 +140,7 @@ pub mod Campaign { pub mod Errors { pub const NOT_CREATOR: felt252 = 'Not creator'; pub const ENDED: felt252 = 'Campaign already ended'; - pub const NOT_DRAFT: felt252 = 'Campaign not draft'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; - pub const STILL_DRAFT: felt252 = 'Campaign not yet active'; pub const CANCELED: felt252 = 'Campaign canceled'; pub const FAILED: felt252 = 'Campaign failed'; pub const CLASS_HASH_ZERO: felt252 = 'Class hash cannot be zero'; @@ -170,11 +166,13 @@ pub mod Campaign { title: ByteArray, description: ByteArray, goal: u256, + duration: 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 }); @@ -183,7 +181,8 @@ pub mod Campaign { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - self.status.write(Status::DRAFT) + self.status.write(Status::ACTIVE); + self.end_time.write(get_block_timestamp() + duration); } #[abi(embed_v0)] @@ -230,7 +229,7 @@ pub mod Campaign { } fn pledge(ref self: ContractState, amount: u256) { - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + // start time check assert(self.status.read() == Status::ACTIVE && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -266,21 +265,10 @@ pub mod Campaign { } } - fn launch(ref self: ContractState, duration: u64) { - self._assert_only_creator(); - assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); - assert(duration > 0, Errors::ZERO_DURATION); - - self.end_time.write(get_block_timestamp() + duration); - self.status.write(Status::ACTIVE); - - self.emit(Event::Launched(Launched {})); - } - fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + // start time check assert(self.status.read() == Status::ACTIVE, Errors::ENDED); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); @@ -292,10 +280,8 @@ pub mod 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::DRAFT, - Errors::ENDED - ); + // 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 { @@ -312,7 +298,7 @@ pub mod Campaign { } fn unpledge(ref self: ContractState, reason: ByteArray) { - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + // 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); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 9700a2ba..9a46c450 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -107,12 +107,13 @@ pub mod MockUpgrade { title: ByteArray, description: ByteArray, goal: u256, + duration: u64, token_address: ContractAddress, - // TODO: add recepient address ) { 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 }); @@ -121,7 +122,8 @@ pub mod MockUpgrade { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - self.status.write(Status::DRAFT) + self.end_time.write(get_block_timestamp() + duration); + self.status.write(Status::ACTIVE); } #[abi(embed_v0)] @@ -166,7 +168,7 @@ pub mod MockUpgrade { } fn pledge(ref self: ContractState, amount: u256) { - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + // start time check assert(self._is_active() && !self._is_expired(), Errors::ENDED); assert(amount > 0, Errors::ZERO_DONATION); @@ -202,21 +204,10 @@ pub mod MockUpgrade { } } - fn launch(ref self: ContractState, duration: u64) { - self._assert_only_creator(); - assert(self.status.read() == Status::DRAFT, Errors::NOT_DRAFT); - assert(duration > 0, Errors::ZERO_DURATION); - - self.end_time.write(get_block_timestamp() + duration); - self.status.write(Status::ACTIVE); - - self.emit(Event::Launched(Launched {})); - } - fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_CONTRIBUTOR); - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); + // start time check assert(self._is_active(), Errors::ENDED); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); @@ -247,10 +238,8 @@ pub mod MockUpgrade { 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::DRAFT, - Errors::ENDED - ); + // 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 { @@ -270,20 +259,13 @@ pub mod MockUpgrade { } fn unpledge(ref self: ContractState, reason: ByteArray) { - assert(self.status.read() != Status::DRAFT, Errors::STILL_DRAFT); - assert(self.status.read() != Status::SUCCESSFUL, Errors::ENDED); - assert(self.status.read() != Status::CANCELED, Errors::CANCELED); + // 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.pledges.remove(pledger); - - // no need to set total_pledges 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(pledger, amount); - assert(success, Errors::TRANSFER_FAILED); + let amount = self._refund(pledger); self.emit(Event::Unpledged(Unpledged { pledger, amount, reason })); } diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index be2b4b26..7c9b40c6 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -15,19 +15,18 @@ use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; -const ERC20_SUPPLY: u256 = 10000; - /// Deploy a campaign contract with the provided data fn deploy( contract: ContractClass, title: ByteArray, description: ByteArray, goal: u256, + duration: u64, token: ContractAddress ) -> ICampaignDispatcher { let creator = contract_address_const::<'creator'>(); let mut calldata: Array:: = array![]; - ((creator, title, description, goal), token).serialize(ref calldata); + ((creator, title, description, goal), duration, token).serialize(ref calldata); let contract_address = contract.precalculate_address(@calldata); let owner = contract_address_const::<'owner'>(); @@ -66,7 +65,9 @@ fn deploy_with_token( token_dispatcher.transfer(pledger_3, 10000); // deploy the actual Campaign contract - let campaign_dispatcher = deploy(contract, "title 1", "description 1", 10000, token_address); + let campaign_dispatcher = deploy( + contract, "title 1", "description 1", 10000, 60, token_address + ); // approve the pledges for each pledger start_cheat_caller_address(token_address, pledger_1); @@ -88,15 +89,15 @@ fn deploy_with_token( fn test_deploy() { let contract = declare("Campaign").unwrap(); let campaign = deploy( - contract, "title 1", "description 1", 10000, contract_address_const::<'token'>() + contract, "title 1", "description 1", 10000, 60, 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, 0); - assert_eq!(details.status, Status::DRAFT); + assert_eq!(details.end_time, get_block_timestamp() + 60); + assert_eq!(details.status, Status::ACTIVE); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, contract_address_const::<'creator'>()); @@ -111,7 +112,6 @@ fn test_successful_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 pledger_1 = contract_address_const::<'pledger_1'>(); @@ -120,17 +120,6 @@ fn test_successful_campaign() { let mut spy = spy_events(SpyOn::One(campaign.contract_address)); - // start campaign - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(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::Launched(Campaign::Launched {}))] - ); - // 1st donation start_cheat_caller_address(campaign.contract_address, pledger_1); let mut prev_balance = token.balance_of(pledger_1); @@ -192,7 +181,7 @@ fn test_successful_campaign() { ); // claim - cheat_block_timestamp_global(get_block_timestamp() + duration); + cheat_block_timestamp_global(campaign.get_details().end_time); start_cheat_caller_address(campaign.contract_address, creator); prev_balance = token.balance_of(creator); campaign.claim(); @@ -241,7 +230,6 @@ fn test_upgrade_class_hash() { 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 pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); let pledger_3 = contract_address_const::<'pledger_3'>(); @@ -249,8 +237,6 @@ fn test_upgrade_class_hash() { let prev_balance_pledger_2 = token.balance_of(pledger_2); let prev_balance_pledger_3 = token.balance_of(pledger_3); - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, pledger_2); @@ -289,7 +275,6 @@ fn test_upgrade_class_hash() { fn test_cancel() { let contract_class = declare("Campaign").unwrap(); let token_class = declare("ERC20").unwrap(); - let duration: u64 = 60; // test canceled campaign let (campaign, token) = deploy_with_token(contract_class, token_class); @@ -302,8 +287,6 @@ fn test_cancel() { let prev_balance_pledger_2 = token.balance_of(pledger_2); let prev_balance_pledger_3 = token.balance_of(pledger_3); - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, pledger_2); @@ -349,8 +332,6 @@ fn test_cancel() { let prev_balance_pledger_2 = token.balance_of(pledger_2); let prev_balance_pledger_3 = token.balance_of(pledger_3); - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, pledger_1); campaign.pledge(3000); start_cheat_caller_address(campaign.contract_address, pledger_2); @@ -359,7 +340,7 @@ fn test_cancel() { campaign.pledge(2000); let total_pledges = campaign.get_details().total_pledges; - cheat_block_timestamp_global(duration); + cheat_block_timestamp_global(campaign.get_details().end_time); start_cheat_caller_address(campaign.contract_address, creator); campaign.cancel("testing"); @@ -391,7 +372,6 @@ fn test_cancel() { #[test] fn test_refund() { // setup - let duration: u64 = 60; let (campaign, token) = deploy_with_token( declare("Campaign").unwrap(), declare("ERC20").unwrap() ); @@ -402,8 +382,6 @@ fn test_refund() { let prev_balance = token.balance_of(pledger); // donate - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(duration); start_cheat_caller_address(campaign.contract_address, pledger); campaign.pledge(amount); assert_eq!(campaign.get_details().total_pledges, amount); @@ -433,17 +411,13 @@ fn test_refund() { #[test] fn test_unpledge() { // setup - let duration: u64 = 60; let (campaign, token) = deploy_with_token( declare("Campaign").unwrap(), declare("ERC20").unwrap() ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); - let creator = contract_address_const::<'creator'>(); let pledger = contract_address_const::<'pledger_1'>(); let amount: u256 = 3000; let prev_balance = token.balance_of(pledger); - start_cheat_caller_address(campaign.contract_address, creator); - campaign.launch(duration); // donate start_cheat_caller_address(campaign.contract_address, pledger); From f3d5a9d4effb94543baddf48ddc2c48bad1343ee Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 09:34:38 +0200 Subject: [PATCH 096/116] 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); From 8f78dc4c80ca18f010ce815627f397a417164e72 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 10:24:46 +0200 Subject: [PATCH 097/116] remove Status --- .../advanced_factory/src/tests.cairo | 4 +- .../crowdfunding/src/campaign.cairo | 71 ++++++--------- .../crowdfunding/src/mock_upgrade.cairo | 60 ++++++------- .../applications/crowdfunding/src/tests.cairo | 88 ++++++++++++------- 4 files changed, 112 insertions(+), 111 deletions(-) diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 6526fdd7..47908b20 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -14,7 +14,6 @@ use snforge_std::{ // Define a goal contract to deploy use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; @@ -78,7 +77,8 @@ fn test_create_campaign() { assert_eq!(details.goal, goal); assert_eq!(details.start_time, start_time); assert_eq!(details.end_time, end_time); - assert_eq!(details.status, Status::ACTIVE); + assert_eq!(details.claimed, false); + assert_eq!(details.canceled, false); assert_eq!(details.token, token); assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, campaign_creator); diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index ae4500ad..9c1b73b1 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -3,14 +3,6 @@ pub mod pledges; // ANCHOR: contract use starknet::{ClassHash, ContractAddress}; -#[derive(Drop, Debug, Serde, PartialEq, starknet::Store)] -pub enum Status { - ACTIVE, - CANCELED, - SUCCESSFUL, - FAILED, -} - #[derive(Drop, Serde)] pub struct Details { pub creator: ContractAddress, @@ -19,7 +11,8 @@ pub struct Details { pub start_time: u64, pub end_time: u64, pub description: ByteArray, - pub status: Status, + pub claimed: bool, + pub canceled: bool, pub token: ContractAddress, pub total_pledges: u256, } @@ -48,7 +41,7 @@ pub mod Campaign { }; use components::ownable::ownable_component; use super::pledges::pledgeable_component; - use super::{Details, Status}; + use super::{Details}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); @@ -73,7 +66,8 @@ pub mod Campaign { title: ByteArray, description: ByteArray, total_pledges: u256, - status: Status + claimed: bool, + canceled: bool, } #[event] @@ -95,7 +89,6 @@ pub mod Campaign { #[derive(Drop, starknet::Event)] pub struct Canceled { pub reason: ByteArray, - pub status: Status, } #[derive(Drop, starknet::Event)] @@ -142,6 +135,7 @@ pub mod Campaign { pub mod Errors { pub const NOT_CREATOR: felt252 = 'Not creator'; pub const ENDED: felt252 = 'Campaign already ended'; + pub const CLAIMED: felt252 = 'Campaign already claimed'; pub const NOT_STARTED: felt252 = 'Campaign not started'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; pub const CANCELED: felt252 = 'Campaign canceled'; @@ -190,7 +184,6 @@ pub mod Campaign { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - 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 }); @@ -200,33 +193,29 @@ pub mod Campaign { impl Campaign of super::ICampaign { fn cancel(ref self: ContractState, reason: ByteArray) { self._assert_only_creator(); - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + assert(!self.canceled.read(), Errors::CANCELED); + assert(!self.claimed.read(), Errors::CLAIMED); - if !self._is_goal_reached() && self._is_expired() { - self.status.write(Status::FAILED); - } else { - self.status.write(Status::CANCELED); - } + self.canceled.write(true); self._refund_all(reason.clone()); - let status = self.status.read(); - self.emit(Event::Canceled(Canceled { reason, status })); + self.emit(Event::Canceled(Canceled { reason })); } fn claim(ref self: ContractState) { self._assert_only_creator(); - self._assert_active(); - assert(self._is_expired(), Errors::STILL_ACTIVE); + assert(self._is_started(), Errors::NOT_STARTED); + assert(self._is_ended(), Errors::STILL_ACTIVE); assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); + assert(!self.claimed.read(), Errors::CLAIMED); 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); + self.claimed.write(true); // no need to reset the pledges, as the campaign has ended // and the data can be used as a testament to how much was raised @@ -246,7 +235,8 @@ pub mod Campaign { goal: self.goal.read(), start_time: self.start_time.read(), end_time: self.end_time.read(), - status: self.status.read(), + claimed: self.claimed.read(), + canceled: self.canceled.read(), token: self.token.read().contract_address, total_pledges: self.total_pledges.read(), } @@ -261,8 +251,9 @@ pub mod Campaign { } fn pledge(ref self: ContractState, amount: u256) { - self._assert_active(); - assert(!self._is_expired(), Errors::ENDED); + assert(self._is_started(), Errors::NOT_STARTED); + assert(!self._is_ended(), Errors::ENDED); + assert(!self.canceled.read(), Errors::CANCELED); assert(amount > 0, Errors::ZERO_DONATION); let pledger = get_caller_address(); @@ -278,7 +269,9 @@ pub mod Campaign { fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - self._assert_active(); + assert(self._is_started(), Errors::NOT_STARTED); + assert(!self.claimed.read(), Errors::CLAIMED); + assert(!self.canceled.read(), Errors::CANCELED); assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); @@ -288,7 +281,7 @@ pub mod Campaign { } fn unpledge(ref self: ContractState, reason: ByteArray) { - self._assert_active(); + assert(self._is_started(), Errors::NOT_STARTED); assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); @@ -302,8 +295,8 @@ pub mod Campaign { self.ownable._assert_only_owner(); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // only active campaigns have funds to refund and duration to update - if get_block_timestamp() >= self.start_time.read() { + // only active campaigns have funds to refund and an end time to update + if self._is_started() { if let Option::Some(end_time) = new_end_time { assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW); assert( @@ -328,15 +321,13 @@ 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_started(self: @ContractState) -> bool { + get_block_timestamp() >= self.start_time.read() } - fn _is_expired(self: @ContractState) -> bool { + fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } - fn _is_goal_reached(self: @ContractState) -> bool { self.total_pledges.read() >= self.goal.read() } @@ -344,11 +335,7 @@ pub mod Campaign { fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); - // if the campaign is "failed", then there's no need to set total_pledges to 0, as - // the campaign has ended and the field can be used as a testament to how much was raised - if self.status.read() == Status::ACTIVE { - self.total_pledges.write(self.total_pledges.read() - amount); - } + self.total_pledges.write(self.total_pledges.read() - amount); let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 965c98e0..3bfcc71d 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -9,7 +9,7 @@ pub mod MockUpgrade { }; use components::ownable::ownable_component; use crowdfunding::campaign::pledges::pledgeable_component; - use crowdfunding::campaign::{ICampaign, Details, Status, Campaign::Errors}; + use crowdfunding::campaign::{ICampaign, Details, Campaign::Errors}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); @@ -34,7 +34,8 @@ pub mod MockUpgrade { title: ByteArray, description: ByteArray, total_pledges: u256, - status: Status + claimed: bool, + canceled: bool, } #[event] @@ -56,7 +57,6 @@ pub mod MockUpgrade { #[derive(Drop, starknet::Event)] pub struct Canceled { pub reason: ByteArray, - pub status: Status, } #[derive(Drop, starknet::Event)] @@ -125,7 +125,6 @@ pub mod MockUpgrade { self.description.write(description); self.creator.write(creator); self.ownable._init(get_caller_address()); - 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 }); @@ -135,33 +134,29 @@ pub mod MockUpgrade { impl MockUpgrade of ICampaign { fn cancel(ref self: ContractState, reason: ByteArray) { self._assert_only_creator(); - assert(self.status.read() == Status::ACTIVE, Errors::ENDED); + assert(!self.canceled.read(), Errors::CANCELED); + assert(!self.claimed.read(), Errors::CLAIMED); - if !self._is_goal_reached() && self._is_expired() { - self.status.write(Status::FAILED); - } else { - self.status.write(Status::CANCELED); - } + self.canceled.write(true); self._refund_all(reason.clone()); - let status = self.status.read(); - self.emit(Event::Canceled(Canceled { reason, status })); + self.emit(Event::Canceled(Canceled { reason })); } fn claim(ref self: ContractState) { self._assert_only_creator(); - self._assert_active(); - assert(self._is_expired(), Errors::STILL_ACTIVE); + assert(self._is_started(), Errors::NOT_STARTED); + assert(self._is_ended(), Errors::STILL_ACTIVE); assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); + assert(!self.claimed.read(), Errors::CLAIMED); 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); + self.claimed.write(true); // no need to reset the pledges, as the campaign has ended // and the data can be used as a testament to how much was raised @@ -181,7 +176,8 @@ pub mod MockUpgrade { goal: self.goal.read(), start_time: self.start_time.read(), end_time: self.end_time.read(), - status: self.status.read(), + claimed: self.claimed.read(), + canceled: self.canceled.read(), token: self.token.read().contract_address, total_pledges: self.total_pledges.read(), } @@ -196,8 +192,9 @@ pub mod MockUpgrade { } fn pledge(ref self: ContractState, amount: u256) { - self._assert_active(); - assert(!self._is_expired(), Errors::ENDED); + assert(self._is_started(), Errors::NOT_STARTED); + assert(!self._is_ended(), Errors::ENDED); + assert(!self.canceled.read(), Errors::CANCELED); assert(amount > 0, Errors::ZERO_DONATION); let pledger = get_caller_address(); @@ -213,7 +210,9 @@ pub mod MockUpgrade { fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { self._assert_only_creator(); - self._assert_active(); + assert(self._is_started(), Errors::NOT_STARTED); + assert(!self.claimed.read(), Errors::CLAIMED); + assert(!self.canceled.read(), Errors::CANCELED); assert(pledger.is_non_zero(), Errors::ZERO_ADDRESS_PLEDGER); assert(self.pledges.get(pledger) != 0, Errors::NOTHING_TO_REFUND); @@ -223,7 +222,7 @@ pub mod MockUpgrade { } fn unpledge(ref self: ContractState, reason: ByteArray) { - self._assert_active(); + assert(self._is_started(), Errors::NOT_STARTED); assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); @@ -237,8 +236,8 @@ pub mod MockUpgrade { self.ownable._assert_only_owner(); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // only active campaigns have funds to refund and duration to update - if self.status.read() == Status::ACTIVE { + // only active campaigns have funds to refund and an end time to update + if self._is_started() { if let Option::Some(end_time) = new_end_time { assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW); assert( @@ -263,15 +262,13 @@ pub mod MockUpgrade { 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_started(self: @ContractState) -> bool { + get_block_timestamp() >= self.start_time.read() } - fn _is_expired(self: @ContractState) -> bool { + fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } - fn _is_goal_reached(self: @ContractState) -> bool { self.total_pledges.read() >= self.goal.read() } @@ -279,11 +276,7 @@ pub mod MockUpgrade { fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); - // if the campaign is "failed", then there's no need to set total_pledges to 0, as - // the campaign has ended and the field can be used as a testament to how much was raised - if self.status.read() == Status::ACTIVE { - self.total_pledges.write(self.total_pledges.read() - amount); - } + self.total_pledges.write(self.total_pledges.read() - amount); let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); @@ -300,3 +293,4 @@ pub mod MockUpgrade { } } } + diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 3f8b684d..f569f105 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -11,7 +11,6 @@ use snforge_std::{ }; use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; -use crowdfunding::campaign::Status; use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -109,7 +108,8 @@ fn test_deploy() { assert_eq!(details.goal, 10000); assert_eq!(details.start_time, start_time); assert_eq!(details.end_time, end_time); - assert_eq!(details.status, Status::ACTIVE); + assert_eq!(details.claimed, false); + assert_eq!(details.canceled, false); assert_eq!(details.token, contract_address_const::<'token'>()); assert_eq!(details.total_pledges, 0); assert_eq!(details.creator, contract_address_const::<'creator'>()); @@ -198,7 +198,7 @@ fn test_successful_campaign() { 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); + assert!(campaign.get_details().claimed); spy .assert_emitted( @@ -295,17 +295,23 @@ fn test_cancel() { let pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); let pledger_3 = contract_address_const::<'pledger_3'>(); + let pledge_1: u256 = 3000; + let pledge_2: u256 = 3000; + let pledge_3: u256 = 3000; let prev_balance_pledger_1 = token.balance_of(pledger_1); let prev_balance_pledger_2 = token.balance_of(pledger_2); let prev_balance_pledger_3 = token.balance_of(pledger_3); start_cheat_caller_address(campaign.contract_address, pledger_1); - campaign.pledge(3000); + campaign.pledge(pledge_1); start_cheat_caller_address(campaign.contract_address, pledger_2); - campaign.pledge(1000); + campaign.pledge(pledge_2); start_cheat_caller_address(campaign.contract_address, pledger_3); - campaign.pledge(2000); - let total_pledges = campaign.get_details().total_pledges; + campaign.pledge(pledge_3); + assert_eq!(campaign.get_details().total_pledges, pledge_1 + pledge_2 + pledge_3); + assert_eq!(token.balance_of(pledger_1), prev_balance_pledger_1 - pledge_1); + assert_eq!(token.balance_of(pledger_2), prev_balance_pledger_2 - pledge_2); + assert_eq!(token.balance_of(pledger_3), prev_balance_pledger_3 - pledge_3); start_cheat_caller_address(campaign.contract_address, creator); campaign.cancel("testing"); @@ -314,8 +320,8 @@ fn test_cancel() { assert_eq!(prev_balance_pledger_1, token.balance_of(pledger_1)); assert_eq!(prev_balance_pledger_2, token.balance_of(pledger_2)); assert_eq!(prev_balance_pledger_3, token.balance_of(pledger_3)); - assert_eq!(campaign.get_details().total_pledges, total_pledges); - assert_eq!(campaign.get_details().status, Status::CANCELED); + assert_eq!(campaign.get_details().total_pledges, 0); + assert!(campaign.get_details().canceled); spy .assert_emitted( @@ -326,9 +332,7 @@ fn test_cancel() { ), ( campaign.contract_address, - Campaign::Event::Canceled( - Campaign::Canceled { reason: "testing", status: Status::CANCELED } - ) + Campaign::Event::Canceled(Campaign::Canceled { reason: "testing" }) ) ] ); @@ -340,17 +344,23 @@ fn test_cancel() { let pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); let pledger_3 = contract_address_const::<'pledger_3'>(); + let pledge_1: u256 = 3000; + let pledge_2: u256 = 3000; + let pledge_3: u256 = 3000; let prev_balance_pledger_1 = token.balance_of(pledger_1); let prev_balance_pledger_2 = token.balance_of(pledger_2); let prev_balance_pledger_3 = token.balance_of(pledger_3); start_cheat_caller_address(campaign.contract_address, pledger_1); - campaign.pledge(3000); + campaign.pledge(pledge_1); start_cheat_caller_address(campaign.contract_address, pledger_2); - campaign.pledge(1000); + campaign.pledge(pledge_2); start_cheat_caller_address(campaign.contract_address, pledger_3); - campaign.pledge(2000); - let total_pledges = campaign.get_details().total_pledges; + campaign.pledge(pledge_3); + assert_eq!(campaign.get_details().total_pledges, pledge_1 + pledge_2 + pledge_3); + assert_eq!(token.balance_of(pledger_1), prev_balance_pledger_1 - pledge_1); + assert_eq!(token.balance_of(pledger_2), prev_balance_pledger_2 - pledge_2); + assert_eq!(token.balance_of(pledger_3), prev_balance_pledger_3 - pledge_3); cheat_block_timestamp_global(campaign.get_details().end_time); @@ -361,8 +371,8 @@ fn test_cancel() { assert_eq!(prev_balance_pledger_1, token.balance_of(pledger_1)); assert_eq!(prev_balance_pledger_2, token.balance_of(pledger_2)); assert_eq!(prev_balance_pledger_3, token.balance_of(pledger_3)); - assert_eq!(campaign.get_details().total_pledges, total_pledges); - assert_eq!(campaign.get_details().status, Status::FAILED); + assert_eq!(campaign.get_details().total_pledges, 0); + assert!(campaign.get_details().canceled); spy .assert_emitted( @@ -373,9 +383,7 @@ fn test_cancel() { ), ( campaign.contract_address, - Campaign::Event::Canceled( - Campaign::Canceled { reason: "testing", status: Status::FAILED } - ) + Campaign::Event::Canceled(Campaign::Canceled { reason: "testing" }) ) ] ); @@ -389,23 +397,33 @@ fn test_refund() { ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let creator = contract_address_const::<'creator'>(); - let pledger = contract_address_const::<'pledger_1'>(); - let amount: u256 = 3000; - let prev_balance = token.balance_of(pledger); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let amount_1: u256 = 3000; + let amount_2: u256 = 1500; + let prev_balance_1 = token.balance_of(pledger_1); + let prev_balance_2 = token.balance_of(pledger_2); // donate - start_cheat_caller_address(campaign.contract_address, pledger); - campaign.pledge(amount); - assert_eq!(campaign.get_details().total_pledges, amount); - assert_eq!(campaign.get_pledge(pledger), amount); - assert_eq!(token.balance_of(pledger), prev_balance - amount); + start_cheat_caller_address(campaign.contract_address, pledger_1); + campaign.pledge(amount_1); + assert_eq!(campaign.get_details().total_pledges, amount_1); + assert_eq!(campaign.get_pledge(pledger_1), amount_1); + assert_eq!(token.balance_of(pledger_1), prev_balance_1 - amount_1); + + start_cheat_caller_address(campaign.contract_address, pledger_2); + campaign.pledge(amount_2); + assert_eq!(campaign.get_details().total_pledges, amount_1 + amount_2); + assert_eq!(campaign.get_pledge(pledger_2), amount_2); + assert_eq!(token.balance_of(pledger_2), prev_balance_2 - amount_2); // refund start_cheat_caller_address(campaign.contract_address, creator); - campaign.refund(pledger, "testing"); - assert_eq!(campaign.get_details().total_pledges, 0); - assert_eq!(campaign.get_pledge(pledger), 0); - assert_eq!(token.balance_of(pledger), prev_balance); + campaign.refund(pledger_1, "testing"); + assert_eq!(campaign.get_details().total_pledges, amount_2); + assert_eq!(campaign.get_pledge(pledger_2), amount_2); + assert_eq!(token.balance_of(pledger_2), prev_balance_2 - amount_2); + assert_eq!(token.balance_of(pledger_1), prev_balance_1); spy .assert_emitted( @@ -413,7 +431,9 @@ fn test_refund() { ( campaign.contract_address, Campaign::Event::Refunded( - Campaign::Refunded { pledger, amount, reason: "testing" } + Campaign::Refunded { + pledger: pledger_1, amount: amount_1, reason: "testing" + } ) ) ] From 315169c9101477f5d1d76a4e8f931daaf1dd006e Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 11:37:29 +0200 Subject: [PATCH 098/116] update doc for campaign --- listings/applications/crowdfunding/src/campaign.cairo | 1 + src/applications/crowdfunding.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 9c1b73b1..3d4cf435 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -328,6 +328,7 @@ pub mod Campaign { fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } + fn _is_goal_reached(self: @ContractState) -> bool { self.total_pledges.read() >= self.goal.read() } diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md index 3cf9bbec..c44b52d1 100644 --- a/src/applications/crowdfunding.md +++ b/src/applications/crowdfunding.md @@ -5,12 +5,12 @@ Crowdfunding is a method of raising capital through the collective effort of man 1. Contract admin creates a campaign in some user's name (i.e. creator). 2. Users can pledge, transferring their token to a campaign. 3. Users can "unpledge", retrieving their tokens. -4. The creator can at any point refund any of the users -5. Once the total amount pledged is more than the campaign goal, the campaign funds are "locked" in the contract, meaning the users can no longer unpledge +4. The creator can at any point refund any of the users. +5. Once the total amount pledged is more than the campaign goal, the campaign funds are "locked" in the contract, meaning the users can no longer unpledge; they can still pledge though. 6. After the campaign ends, the campaign creator can claim the funds if the campaign goal is reached. -7. Otherwise, campaign did not reach it's goal, the creator can mark the campaign as "failed" and refund all of the pledgers. +7. Otherwise, campaign did not reach it's goal, pledgers can retrieve their funds. 8. The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers. -9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state +9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state. ```rust {{#include ../../../listings/applications/crowdfunding/src/campaign.cairo:contract}} From adaa0b9a794b503fac2d5ced75094194aee21dda Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 11:47:42 +0200 Subject: [PATCH 099/116] move total_pledges to pledgeable --- .../crowdfunding/src/campaign.cairo | 8 ++---- .../crowdfunding/src/campaign/pledges.cairo | 26 ++++++++++++++----- .../crowdfunding/src/mock_upgrade.cairo | 10 +++---- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 3d4cf435..ba631f60 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -65,7 +65,6 @@ pub mod Campaign { goal: u256, title: ByteArray, description: ByteArray, - total_pledges: u256, claimed: bool, canceled: bool, } @@ -238,7 +237,7 @@ pub mod Campaign { claimed: self.claimed.read(), canceled: self.canceled.read(), token: self.token.read().contract_address, - total_pledges: self.total_pledges.read(), + total_pledges: self.pledges.get_total(), } } @@ -262,7 +261,6 @@ pub mod Campaign { assert(success, Errors::TRANSFER_FAILED); self.pledges.add(pledger, amount); - self.total_pledges.write(self.total_pledges.read() + amount); self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } @@ -330,14 +328,12 @@ pub mod Campaign { } fn _is_goal_reached(self: @ContractState) -> bool { - self.total_pledges.read() >= self.goal.read() + self.pledges.get_total() >= self.goal.read() } fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); - self.total_pledges.write(self.total_pledges.read() - amount); - let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); diff --git a/listings/applications/crowdfunding/src/campaign/pledges.cairo b/listings/applications/crowdfunding/src/campaign/pledges.cairo index 859286f3..e0203d25 100644 --- a/listings/applications/crowdfunding/src/campaign/pledges.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledges.cairo @@ -5,6 +5,7 @@ pub trait IPledgeable { fn add(ref self: TContractState, pledger: ContractAddress, amount: u256); fn get(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledges_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; + fn get_total(self: @TContractState) -> u256; fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; } @@ -19,6 +20,7 @@ pub mod pledgeable_component { index_to_pledger: LegacyMap, pledger_to_amount_index: LegacyMap>, pledger_count: u32, + total_amount: u256, } #[event] @@ -43,6 +45,7 @@ pub mod pledgeable_component { self.pledger_to_amount_index.write(pledger, Option::Some((amount, index))); self.pledger_count.write(index + 1); } + self.total_amount.write(self.total_amount.read() + amount); } fn get(self: @ComponentState, pledger: ContractAddress) -> u256 { @@ -75,16 +78,21 @@ pub mod pledgeable_component { result } + fn get_total(self: @ComponentState) -> u256 { + self.total_amount.read() + } + fn remove(ref self: ComponentState, pledger: ContractAddress) -> u256 { let amount_index_option: Option<(u256, u32)> = self .pledger_to_amount_index .read(pledger); - if let Option::Some((amount, index)) = amount_index_option { + + let amount = if let Option::Some((amount, index)) = amount_index_option { self.pledger_to_amount_index.write(pledger, Option::None); - let pledger_count = self.pledger_count.read() - 1; - self.pledger_count.write(pledger_count); - if pledger_count != 0 { - let last_pledger = self.index_to_pledger.read(pledger_count); + let new_pledger_count = self.pledger_count.read() - 1; + self.pledger_count.write(new_pledger_count); + if new_pledger_count != 0 { + let last_pledger = self.index_to_pledger.read(new_pledger_count); let last_amount_index: Option<(u256, u32)> = self .pledger_to_amount_index .read(last_pledger); @@ -98,12 +106,16 @@ pub mod pledgeable_component { self.index_to_pledger.write(index, last_pledger); } - self.index_to_pledger.write(pledger_count, Zero::zero()); + self.index_to_pledger.write(new_pledger_count, Zero::zero()); amount } else { 0 - } + }; + + self.total_amount.write(self.total_amount.read() - amount); + + amount } } } diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 3bfcc71d..2c3a7309 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -33,7 +33,6 @@ pub mod MockUpgrade { goal: u256, title: ByteArray, description: ByteArray, - total_pledges: u256, claimed: bool, canceled: bool, } @@ -179,7 +178,7 @@ pub mod MockUpgrade { claimed: self.claimed.read(), canceled: self.canceled.read(), token: self.token.read().contract_address, - total_pledges: self.total_pledges.read(), + total_pledges: self.pledges.get_total(), } } @@ -203,7 +202,6 @@ pub mod MockUpgrade { assert(success, Errors::TRANSFER_FAILED); self.pledges.add(pledger, amount); - self.total_pledges.write(self.total_pledges.read() + amount); self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); } @@ -269,15 +267,14 @@ pub mod MockUpgrade { fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } + fn _is_goal_reached(self: @ContractState) -> bool { - self.total_pledges.read() >= self.goal.read() + self.pledges.get_total() >= self.goal.read() } fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); - self.total_pledges.write(self.total_pledges.read() - amount); - let success = self.token.read().transfer(pledger, amount); assert(success, Errors::TRANSFER_FAILED); @@ -293,4 +290,3 @@ pub mod MockUpgrade { } } } - From 5fcb4e2c24f8f26ccd9e13cdc493de08411792ea Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 11:58:45 +0200 Subject: [PATCH 100/116] reorder alphabetically --- .../crowdfunding/src/campaign.cairo | 87 ++++++++++--------- .../crowdfunding/src/mock_upgrade.cairo | 6 +- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index ba631f60..0b8a1705 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -5,14 +5,14 @@ use starknet::{ClassHash, ContractAddress}; #[derive(Drop, Serde)] pub struct Details { + pub canceled: bool, + pub claimed: bool, pub creator: ContractAddress, + pub description: ByteArray, + pub end_time: u64, pub goal: u256, - pub title: ByteArray, pub start_time: u64, - pub end_time: u64, - pub description: ByteArray, - pub claimed: bool, - pub canceled: bool, + pub title: ByteArray, pub token: ContractAddress, pub total_pledges: u256, } @@ -54,29 +54,29 @@ pub mod Campaign { #[storage] struct Storage { + canceled: bool, + claimed: bool, + creator: ContractAddress, + description: ByteArray, + end_time: u64, + goal: u256, #[substorage(v0)] ownable: ownable_component::Storage, #[substorage(v0)] pledges: pledgeable_component::Storage, start_time: u64, - end_time: u64, - token: IERC20Dispatcher, - creator: ContractAddress, - goal: u256, title: ByteArray, - description: ByteArray, - claimed: bool, - canceled: bool, + token: IERC20Dispatcher, } #[event] #[derive(Drop, starknet::Event)] pub enum Event { - #[flat] - OwnableEvent: ownable_component::Event, Claimed: Claimed, Canceled: Canceled, Launched: Launched, + #[flat] + OwnableEvent: ownable_component::Event, PledgeableEvent: pledgeable_component::Event, PledgeMade: PledgeMade, Refunded: Refunded, @@ -132,30 +132,30 @@ pub mod Campaign { } pub mod Errors { - pub const NOT_CREATOR: felt252 = 'Not creator'; - pub const ENDED: felt252 = 'Campaign already ended'; - pub const CLAIMED: felt252 = 'Campaign already claimed'; - 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 START_TIME_IN_PAST: felt252 = 'Start time < now'; - pub const END_BEFORE_START: felt252 = 'End time < start time'; + pub const CLAIMED: felt252 = 'Campaign already claimed'; + pub const CLASS_HASH_ZERO: felt252 = 'Class hash zero'; + pub const CREATOR_ZERO: felt252 = 'Creator address zero'; + pub const ENDED: felt252 = 'Campaign already ended'; pub const END_BEFORE_NOW: felt252 = 'End time < now'; + pub const END_BEFORE_START: felt252 = 'End time < start time'; pub const END_BIGGER_THAN_MAX: felt252 = 'End time > max duration'; - pub const ZERO_FUNDS: felt252 = 'No funds to claim'; - 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_UNPLEDGE: felt252 = 'Nothing to unpledge'; pub const NOTHING_TO_REFUND: felt252 = 'Nothing to refund'; + pub const NOTHING_TO_UNPLEDGE: felt252 = 'Nothing to unpledge'; + pub const NOT_CREATOR: felt252 = 'Not creator'; + pub const NOT_STARTED: felt252 = 'Campaign not started'; + pub const PLEDGES_LOCKED: felt252 = 'Goal reached, pledges locked'; + pub const START_TIME_IN_PAST: felt252 = 'Start time < now'; + pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; + pub const TARGET_NOT_REACHED: felt252 = 'Goal not reached'; + pub const TITLE_EMPTY: felt252 = 'Title empty'; + pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; + pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller address zero'; + pub const ZERO_ADDRESS_PLEDGER: felt252 = 'Pledger address zero'; + pub const ZERO_ADDRESS_TOKEN: felt252 = 'Token address zerp'; + pub const ZERO_DONATION: felt252 = 'Donation must be > 0'; + pub const ZERO_GOAL: felt252 = 'Goal must be > 0'; + pub const ZERO_PLEDGES: felt252 = 'No pledges to claim'; } const NINETY_DAYS: u64 = consteval_int!(90 * 24 * 60 * 60); @@ -173,19 +173,20 @@ pub mod Campaign { ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); - assert(goal > 0, Errors::ZERO_TARGET); + assert(goal > 0, Errors::ZERO_GOAL); 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); + assert(token_address.is_non_zero(), Errors::ZERO_ADDRESS_TOKEN); + self.creator.write(creator); self.title.write(title); self.goal.write(goal); self.description.write(description); - self.creator.write(creator); - self.ownable._init(get_caller_address()); self.start_time.write(start_time); self.end_time.write(end_time); self.token.write(IERC20Dispatcher { contract_address: token_address }); + self.ownable._init(get_caller_address()); } #[abi(embed_v0)] @@ -212,7 +213,7 @@ pub mod Campaign { let this = get_contract_address(); let token = self.token.read(); let amount = token.balance_of(this); - assert(amount > 0, Errors::ZERO_FUNDS); + assert(amount > 0, Errors::ZERO_PLEDGES); self.claimed.write(true); @@ -280,7 +281,7 @@ pub mod Campaign { fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self._is_started(), Errors::NOT_STARTED); - assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); + assert(!self._is_goal_reached(), Errors::PLEDGES_LOCKED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); let pledger = get_caller_address(); @@ -319,10 +320,6 @@ pub mod Campaign { assert(caller == self.creator.read(), Errors::NOT_CREATOR); } - fn _is_started(self: @ContractState) -> bool { - get_block_timestamp() >= self.start_time.read() - } - fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } @@ -331,6 +328,10 @@ pub mod Campaign { self.pledges.get_total() >= self.goal.read() } + fn _is_started(self: @ContractState) -> bool { + get_block_timestamp() >= self.start_time.read() + } + fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 2c3a7309..e12a2790 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -114,7 +114,7 @@ pub mod MockUpgrade { ) { assert(creator.is_non_zero(), Errors::CREATOR_ZERO); assert(title.len() > 0, Errors::TITLE_EMPTY); - assert(goal > 0, Errors::ZERO_TARGET); + assert(goal > 0, Errors::ZERO_GOAL); 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); @@ -153,7 +153,7 @@ pub mod MockUpgrade { let this = get_contract_address(); let token = self.token.read(); let amount = token.balance_of(this); - assert(amount > 0, Errors::ZERO_FUNDS); + assert(amount > 0, Errors::ZERO_PLEDGES); self.claimed.write(true); @@ -221,7 +221,7 @@ pub mod MockUpgrade { fn unpledge(ref self: ContractState, reason: ByteArray) { assert(self._is_started(), Errors::NOT_STARTED); - assert(!self._is_goal_reached(), Errors::TARGET_ALREADY_REACHED); + assert(!self._is_goal_reached(), Errors::PLEDGES_LOCKED); assert(self.pledges.get(get_caller_address()) != 0, Errors::NOTHING_TO_UNPLEDGE); let pledger = get_caller_address(); From f0a658631917275d4c7bbcfaba25506d231a0841 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 12:01:54 +0200 Subject: [PATCH 101/116] remove Launched event + upgrade mock --- .../crowdfunding/src/campaign.cairo | 6 +--- .../crowdfunding/src/mock_upgrade.cairo | 35 +++++++++---------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 0b8a1705..3ea712d0 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -1,6 +1,6 @@ +// ANCHOR: contract pub mod pledges; -// ANCHOR: contract use starknet::{ClassHash, ContractAddress}; #[derive(Drop, Serde)] @@ -74,7 +74,6 @@ pub mod Campaign { pub enum Event { Claimed: Claimed, Canceled: Canceled, - Launched: Launched, #[flat] OwnableEvent: ownable_component::Event, PledgeableEvent: pledgeable_component::Event, @@ -95,9 +94,6 @@ pub mod Campaign { pub amount: u256, } - #[derive(Drop, starknet::Event)] - pub struct Launched {} - #[derive(Drop, starknet::Event)] pub struct PledgeMade { #[key] diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index e12a2790..0ef1e791 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -22,29 +22,28 @@ pub mod MockUpgrade { #[storage] struct Storage { + canceled: bool, + claimed: bool, + creator: ContractAddress, + description: ByteArray, + end_time: u64, + goal: u256, #[substorage(v0)] ownable: ownable_component::Storage, #[substorage(v0)] pledges: pledgeable_component::Storage, start_time: u64, - end_time: u64, - token: IERC20Dispatcher, - creator: ContractAddress, - goal: u256, title: ByteArray, - description: ByteArray, - claimed: bool, - canceled: bool, + token: IERC20Dispatcher, } #[event] #[derive(Drop, starknet::Event)] pub enum Event { - #[flat] - OwnableEvent: ownable_component::Event, Claimed: Claimed, Canceled: Canceled, - Launched: Launched, + #[flat] + OwnableEvent: ownable_component::Event, PledgeableEvent: pledgeable_component::Event, PledgeMade: PledgeMade, Refunded: Refunded, @@ -63,9 +62,6 @@ pub mod MockUpgrade { pub amount: u256, } - #[derive(Drop, starknet::Event)] - pub struct Launched {} - #[derive(Drop, starknet::Event)] pub struct PledgeMade { #[key] @@ -118,15 +114,16 @@ pub mod MockUpgrade { 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); + assert(token_address.is_non_zero(), Errors::ZERO_ADDRESS_TOKEN); + self.creator.write(creator); self.title.write(title); self.goal.write(goal); self.description.write(description); - self.creator.write(creator); - self.ownable._init(get_caller_address()); self.start_time.write(start_time); self.end_time.write(end_time); self.token.write(IERC20Dispatcher { contract_address: token_address }); + self.ownable._init(get_caller_address()); } #[abi(embed_v0)] @@ -260,10 +257,6 @@ pub mod MockUpgrade { assert(caller == self.creator.read(), Errors::NOT_CREATOR); } - fn _is_started(self: @ContractState) -> bool { - get_block_timestamp() >= self.start_time.read() - } - fn _is_ended(self: @ContractState) -> bool { get_block_timestamp() >= self.end_time.read() } @@ -272,6 +265,10 @@ pub mod MockUpgrade { self.pledges.get_total() >= self.goal.read() } + fn _is_started(self: @ContractState) -> bool { + get_block_timestamp() >= self.start_time.read() + } + fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); From 377c02d7b84d0432ca968e0c28f1a3f9a2e91394 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 12:04:23 +0200 Subject: [PATCH 102/116] TARGET->GOAL --- listings/applications/crowdfunding/src/campaign.cairo | 4 ++-- listings/applications/crowdfunding/src/mock_upgrade.cairo | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 3ea712d0..2f5ddbb7 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -143,7 +143,7 @@ pub mod Campaign { pub const PLEDGES_LOCKED: felt252 = 'Goal reached, pledges locked'; pub const START_TIME_IN_PAST: felt252 = 'Start time < now'; pub const STILL_ACTIVE: felt252 = 'Campaign not ended'; - pub const TARGET_NOT_REACHED: felt252 = 'Goal not reached'; + pub const GOAL_NOT_REACHED: felt252 = 'Goal not reached'; pub const TITLE_EMPTY: felt252 = 'Title empty'; pub const TRANSFER_FAILED: felt252 = 'Transfer failed'; pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller address zero'; @@ -203,7 +203,7 @@ pub mod Campaign { self._assert_only_creator(); assert(self._is_started(), Errors::NOT_STARTED); assert(self._is_ended(), Errors::STILL_ACTIVE); - assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); + assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED); assert(!self.claimed.read(), Errors::CLAIMED); let this = get_contract_address(); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 0ef1e791..210f5a7a 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -144,7 +144,7 @@ pub mod MockUpgrade { self._assert_only_creator(); assert(self._is_started(), Errors::NOT_STARTED); assert(self._is_ended(), Errors::STILL_ACTIVE); - assert(self._is_goal_reached(), Errors::TARGET_NOT_REACHED); + assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED); assert(!self.claimed.read(), Errors::CLAIMED); let this = get_contract_address(); From e25df4ef29e7486628c540dbda737d17e0aa4b6c Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 12:09:41 +0200 Subject: [PATCH 103/116] reorder params in Details --- listings/applications/crowdfunding/src/campaign.cairo | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 2f5ddbb7..945770c2 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -203,8 +203,9 @@ pub mod Campaign { self._assert_only_creator(); assert(self._is_started(), Errors::NOT_STARTED); assert(self._is_ended(), Errors::STILL_ACTIVE); - assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED); assert(!self.claimed.read(), Errors::CLAIMED); + assert(self._is_goal_reached(), Errors::GOAL_NOT_REACHED); + // no need to check if canceled; if it was, then the goal wouldn't have been reached let this = get_contract_address(); let token = self.token.read(); @@ -225,14 +226,14 @@ pub mod Campaign { fn get_details(self: @ContractState) -> Details { Details { + canceled: self.canceled.read(), + claimed: self.claimed.read(), creator: self.creator.read(), - title: self.title.read(), description: self.description.read(), + end_time: self.end_time.read(), goal: self.goal.read(), start_time: self.start_time.read(), - end_time: self.end_time.read(), - claimed: self.claimed.read(), - canceled: self.canceled.read(), + title: self.title.read(), token: self.token.read().contract_address, total_pledges: self.pledges.get_total(), } From 36ff85c79948f0bab6a2a2dcf085257631464401 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 12:33:17 +0200 Subject: [PATCH 104/116] add inline to _refund --- listings/applications/crowdfunding/src/campaign.cairo | 1 + 1 file changed, 1 insertion(+) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 945770c2..bee7e1f3 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -329,6 +329,7 @@ pub mod Campaign { get_block_timestamp() >= self.start_time.read() } + #[inline(always)] fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { let amount = self.pledges.remove(pledger); From af94299b52593983675f2e3bc522bec5b826ad0c Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 13:14:43 +0200 Subject: [PATCH 105/116] add new pledgeable tests --- listings/applications/crowdfunding/Scarb.toml | 2 +- .../crowdfunding/src/campaign.cairo | 6 +- .../{pledges.cairo => pledgeable.cairo} | 110 ++++++++++++++++++ .../crowdfunding/src/mock_upgrade.cairo | 2 +- src/applications/crowdfunding.md | 11 +- 5 files changed, 125 insertions(+), 6 deletions(-) rename listings/applications/crowdfunding/src/campaign/{pledges.cairo => pledgeable.cairo} (56%) diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml index 2d781ca8..e3f947a6 100644 --- a/listings/applications/crowdfunding/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -3,7 +3,7 @@ name = "crowdfunding" version.workspace = true edition = "2023_11" -[lib] +# [lib] [dependencies] starknet.workspace = true diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index bee7e1f3..0d342b5f 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -1,6 +1,6 @@ -// ANCHOR: contract -pub mod pledges; +pub mod pledgeable; +// ANCHOR: contract use starknet::{ClassHash, ContractAddress}; #[derive(Drop, Serde)] @@ -40,7 +40,7 @@ pub mod Campaign { get_caller_address, get_contract_address, class_hash::class_hash_const }; use components::ownable::ownable_component; - use super::pledges::pledgeable_component; + use super::pledgeable::pledgeable_component; use super::{Details}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); diff --git a/listings/applications/crowdfunding/src/campaign/pledges.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo similarity index 56% rename from listings/applications/crowdfunding/src/campaign/pledges.cairo rename to listings/applications/crowdfunding/src/campaign/pledgeable.cairo index e0203d25..078d2865 100644 --- a/listings/applications/crowdfunding/src/campaign/pledges.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -1,3 +1,4 @@ +// ANCHOR: component use starknet::ContractAddress; #[starknet::interface] @@ -119,4 +120,113 @@ pub mod pledgeable_component { } } } +// ANCHOR_END: component +#[cfg(test)] +mod tests { + #[starknet::contract] + mod MockContract { + use super::super::pledgeable_component; + + component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + pledges: pledgeable_component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + PledgeableEvent: pledgeable_component::Event + } + + #[abi(embed_v0)] + impl Pledgeable = pledgeable_component::Pledgeable; + } + + use super::{pledgeable_component, IPledgeableDispatcher, IPledgeableDispatcherTrait}; + use super::pledgeable_component::PledgeableImpl; + use starknet::{contract_address_const}; + + type TestingState = pledgeable_component::ComponentState; + + // You can derive even `Default` on this type alias + impl TestingStateDefault of Default { + fn default() -> TestingState { + pledgeable_component::component_state_for_testing() + } + } + + #[test] + fn test_add() { + let mut pledgeable: TestingState = Default::default(); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + + assert_eq!(pledgeable.get_total(), 0); + assert_eq!(pledgeable.get(pledger_1), 0); + assert_eq!(pledgeable.get(pledger_2), 0); + + pledgeable.add(pledger_1, 1000); + + assert_eq!(pledgeable.get_total(), 1000); + assert_eq!(pledgeable.get(pledger_1), 1000); + assert_eq!(pledgeable.get(pledger_2), 0); + + pledgeable.add(pledger_1, 1000); + + assert_eq!(pledgeable.get_total(), 2000); + assert_eq!(pledgeable.get(pledger_1), 2000); + assert_eq!(pledgeable.get(pledger_2), 0); + + pledgeable.add(pledger_2, 500); + + assert_eq!(pledgeable.get_total(), 2500); + assert_eq!(pledgeable.get(pledger_1), 2000); + assert_eq!(pledgeable.get(pledger_2), 500); + } + + #[test] + fn test_remove() { + let mut pledgeable: TestingState = Default::default(); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + + pledgeable.add(pledger_1, 2000); + pledgeable.add(pledger_2, 3000); + // pledger_3 not added + + assert_eq!(pledgeable.get_total(), 5000); + assert_eq!(pledgeable.get(pledger_1), 2000); + assert_eq!(pledgeable.get(pledger_2), 3000); + assert_eq!(pledgeable.get(pledger_3), 0); + + let amount = pledgeable.remove(pledger_1); + + assert_eq!(amount, 2000); + assert_eq!(pledgeable.get_total(), 3000); + assert_eq!(pledgeable.get(pledger_1), 0); + assert_eq!(pledgeable.get(pledger_2), 3000); + assert_eq!(pledgeable.get(pledger_3), 0); + + let amount = pledgeable.remove(pledger_2); + + assert_eq!(amount, 3000); + assert_eq!(pledgeable.get_total(), 0); + assert_eq!(pledgeable.get(pledger_1), 0); + assert_eq!(pledgeable.get(pledger_2), 0); + assert_eq!(pledgeable.get(pledger_3), 0); + + // should do nothing and return 0 + let amount = pledgeable.remove(pledger_3); + + assert_eq!(amount, 0); + assert_eq!(pledgeable.get_total(), 0); + assert_eq!(pledgeable.get(pledger_1), 0); + assert_eq!(pledgeable.get(pledger_2), 0); + assert_eq!(pledgeable.get(pledger_3), 0); + } +} diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index 210f5a7a..d884a734 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -8,7 +8,7 @@ pub mod MockUpgrade { get_caller_address, get_contract_address, class_hash::class_hash_const }; use components::ownable::ownable_component; - use crowdfunding::campaign::pledges::pledgeable_component; + use crowdfunding::campaign::pledgeable::pledgeable_component; use crowdfunding::campaign::{ICampaign, Details, Campaign::Errors}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md index c44b52d1..9e8518e6 100644 --- a/src/applications/crowdfunding.md +++ b/src/applications/crowdfunding.md @@ -12,6 +12,15 @@ Crowdfunding is a method of raising capital through the collective effort of man 8. The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers. 9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state. +Because contract upgrades need to be able to refund all of the pledges, we need to be able to iterate over all of the pledgers and their amounts. Since iteration is not supported by `LegacyMap`, we need to create a custom storage type that will encompass pledge management. We use a component for this purpose. + +```rust +{{#include ../../listings/applications/crowdfunding/src/campaign/pledgeable.cairo:component}} +``` + +Now we can create the `Campaign` contract. + + ```rust -{{#include ../../../listings/applications/crowdfunding/src/campaign.cairo:contract}} +{{#include ../../listings/applications/crowdfunding/src/campaign.cairo:contract}} ``` From ccd236280e046b76c8e5ce69b3b1433e04ca82c1 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 14 Jun 2024 16:52:44 +0200 Subject: [PATCH 106/116] add getX tests + add get_pledge_count --- .../src/campaign/pledgeable.cairo | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index 078d2865..b2062ba6 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -5,6 +5,7 @@ use starknet::ContractAddress; pub trait IPledgeable { fn add(ref self: TContractState, pledger: ContractAddress, amount: u256); fn get(self: @TContractState, pledger: ContractAddress) -> u256; + fn get_pledger_count(self: @TContractState) -> u32; fn get_pledges_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; fn get_total(self: @TContractState) -> u256; fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; @@ -57,6 +58,10 @@ pub mod pledgeable_component { } } + fn get_pledger_count(self: @ComponentState) -> u32 { + self.pledger_count.read() + } + fn get_pledges_as_arr( self: @ComponentState ) -> Array<(ContractAddress, u256)> { @@ -147,7 +152,7 @@ mod tests { } use super::{pledgeable_component, IPledgeableDispatcher, IPledgeableDispatcherTrait}; - use super::pledgeable_component::PledgeableImpl; + use super::pledgeable_component::{PledgeableImpl}; use starknet::{contract_address_const}; type TestingState = pledgeable_component::ComponentState; @@ -165,24 +170,31 @@ mod tests { let pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); + assert_eq!(pledgeable.get_pledger_count(), 0); assert_eq!(pledgeable.get_total(), 0); assert_eq!(pledgeable.get(pledger_1), 0); assert_eq!(pledgeable.get(pledger_2), 0); + // 1st pledge pledgeable.add(pledger_1, 1000); + assert_eq!(pledgeable.get_pledger_count(), 1); assert_eq!(pledgeable.get_total(), 1000); assert_eq!(pledgeable.get(pledger_1), 1000); assert_eq!(pledgeable.get(pledger_2), 0); + // 2nd pledge should be added onto 1st pledgeable.add(pledger_1, 1000); + assert_eq!(pledgeable.get_pledger_count(), 1); assert_eq!(pledgeable.get_total(), 2000); assert_eq!(pledgeable.get(pledger_1), 2000); assert_eq!(pledgeable.get(pledger_2), 0); + // different pledger stored separately pledgeable.add(pledger_2, 500); + assert_eq!(pledgeable.get_pledger_count(), 2); assert_eq!(pledgeable.get_total(), 2500); assert_eq!(pledgeable.get(pledger_1), 2000); assert_eq!(pledgeable.get(pledger_2), 500); @@ -199,6 +211,7 @@ mod tests { pledgeable.add(pledger_2, 3000); // pledger_3 not added + assert_eq!(pledgeable.get_pledger_count(), 2); assert_eq!(pledgeable.get_total(), 5000); assert_eq!(pledgeable.get(pledger_1), 2000); assert_eq!(pledgeable.get(pledger_2), 3000); @@ -207,6 +220,7 @@ mod tests { let amount = pledgeable.remove(pledger_1); assert_eq!(amount, 2000); + assert_eq!(pledgeable.get_pledger_count(), 1); assert_eq!(pledgeable.get_total(), 3000); assert_eq!(pledgeable.get(pledger_1), 0); assert_eq!(pledgeable.get(pledger_2), 3000); @@ -215,18 +229,57 @@ mod tests { let amount = pledgeable.remove(pledger_2); assert_eq!(amount, 3000); + assert_eq!(pledgeable.get_pledger_count(), 0); assert_eq!(pledgeable.get_total(), 0); assert_eq!(pledgeable.get(pledger_1), 0); assert_eq!(pledgeable.get(pledger_2), 0); assert_eq!(pledgeable.get(pledger_3), 0); - // should do nothing and return 0 + // pledger_3 not added, so this should do nothing and return 0 let amount = pledgeable.remove(pledger_3); assert_eq!(amount, 0); + assert_eq!(pledgeable.get_pledger_count(), 0); assert_eq!(pledgeable.get_total(), 0); assert_eq!(pledgeable.get(pledger_1), 0); assert_eq!(pledgeable.get(pledger_2), 0); assert_eq!(pledgeable.get(pledger_3), 0); } + + #[test] + fn test_get_pledges_as_arr() { + let mut pledgeable: TestingState = Default::default(); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + + pledgeable.add(pledger_1, 1000); + pledgeable.add(pledger_2, 500); + pledgeable.add(pledger_3, 2500); + // 2nd pledge by pledger_2 *should not* increase the pledge count + pledgeable.add(pledger_2, 1500); + + let pledges_arr = pledgeable.get_pledges_as_arr(); + + assert_eq!(pledges_arr.len(), 3); + assert_eq!((pledger_1, 1000), *pledges_arr[2]); + assert_eq!((pledger_2, 2000), *pledges_arr[1]); + assert_eq!((pledger_3, 2500), *pledges_arr[0]); + } + + #[test] + fn test_get() { + let mut pledgeable: TestingState = Default::default(); + let pledger_1 = contract_address_const::<'pledger_1'>(); + let pledger_2 = contract_address_const::<'pledger_2'>(); + let pledger_3 = contract_address_const::<'pledger_3'>(); + + pledgeable.add(pledger_1, 1000); + pledgeable.add(pledger_2, 500); + // pledger_3 not added + + assert_eq!(pledgeable.get(pledger_1), 1000); + assert_eq!(pledgeable.get(pledger_2), 500); + assert_eq!(pledgeable.get(pledger_3), 0); + } } From e3c9f55a438f1db992d7f7529ef25b871f9a0e69 Mon Sep 17 00:00:00 2001 From: Nenad Date: Sat, 15 Jun 2024 11:21:24 +0200 Subject: [PATCH 107/116] refactor pledger_to_amount_index->pledger_to_amount --- .../src/campaign/pledgeable.cairo | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index b2062ba6..1ad7fedd 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -20,7 +20,7 @@ pub mod pledgeable_component { #[storage] struct Storage { index_to_pledger: LegacyMap, - pledger_to_amount_index: LegacyMap>, + pledger_to_amount: LegacyMap, pledger_count: u32, total_amount: u256, } @@ -29,33 +29,29 @@ pub mod pledgeable_component { #[derive(Drop, starknet::Event)] pub enum Event {} + mod Errors { + pub const INCONSISTENT_STATE: felt252 = 'Non-indexed pledger found'; + } + #[embeddable_as(Pledgeable)] pub impl PledgeableImpl< TContractState, +HasComponent > of super::IPledgeable> { fn add(ref self: ComponentState, pledger: ContractAddress, amount: u256) { - let amount_index_option: Option<(u256, u32)> = self - .pledger_to_amount_index - .read(pledger); - if let Option::Some((old_amount, index)) = amount_index_option { - self - .pledger_to_amount_index - .write(pledger, Option::Some((old_amount + amount, index))); - } else { + let old_amount: u256 = self.pledger_to_amount.read(pledger); + + if old_amount == 0 { let index = self.pledger_count.read(); self.index_to_pledger.write(index, pledger); - self.pledger_to_amount_index.write(pledger, Option::Some((amount, index))); self.pledger_count.write(index + 1); } + + self.pledger_to_amount.write(pledger, old_amount + amount); self.total_amount.write(self.total_amount.read() + amount); } fn get(self: @ComponentState, pledger: ContractAddress) -> u256 { - let val: Option<(u256, u32)> = self.pledger_to_amount_index.read(pledger); - match val { - Option::Some((amount, _)) => amount, - Option::None => 0, - } + self.pledger_to_amount.read(pledger) } fn get_pledger_count(self: @ComponentState) -> u32 { @@ -71,13 +67,7 @@ pub mod pledgeable_component { while index != 0 { index -= 1; let pledger = self.index_to_pledger.read(index); - let amount_index_option: Option<(u256, u32)> = self - .pledger_to_amount_index - .read(pledger); - let amount = match amount_index_option { - Option::Some((amount, _)) => amount, - Option::None => 0 - }; + let amount: u256 = self.pledger_to_amount.read(pledger); result.append((pledger, amount)); }; @@ -89,35 +79,33 @@ pub mod pledgeable_component { } fn remove(ref self: ComponentState, pledger: ContractAddress) -> u256 { - let amount_index_option: Option<(u256, u32)> = self - .pledger_to_amount_index - .read(pledger); - - let amount = if let Option::Some((amount, index)) = amount_index_option { - self.pledger_to_amount_index.write(pledger, Option::None); - let new_pledger_count = self.pledger_count.read() - 1; - self.pledger_count.write(new_pledger_count); - if new_pledger_count != 0 { - let last_pledger = self.index_to_pledger.read(new_pledger_count); - let last_amount_index: Option<(u256, u32)> = self - .pledger_to_amount_index - .read(last_pledger); - let last_amount = match last_amount_index { - Option::Some((last_amount, _)) => last_amount, - Option::None => 0 - }; - self - .pledger_to_amount_index - .write(last_pledger, Option::Some((last_amount, index))); - self.index_to_pledger.write(index, last_pledger); - } - - self.index_to_pledger.write(new_pledger_count, Zero::zero()); - - amount - } else { - 0 - }; + let amount: u256 = self.pledger_to_amount.read(pledger); + + // check if the pledge even exists + if amount == 0 { + return 0; + } + + let last_index = self.pledger_count.read() - 1; + + // if there are other pledgers, we need to update our indices + if last_index != 0 { + let mut pledger_index = last_index; + loop { + if self.index_to_pledger.read(pledger_index) == pledger { + break; + } + assert(pledger_index > 0, Errors::INCONSISTENT_STATE); + pledger_index -= 1; + }; + + self.index_to_pledger.write(pledger_index, self.index_to_pledger.read(last_index)); + } + + // last_index == new pledger count + self.pledger_count.write(last_index); + self.pledger_to_amount.write(pledger, 0); + self.index_to_pledger.write(last_index, Zero::zero()); self.total_amount.write(self.total_amount.read() - amount); From 131346db3b3844953c7b8e6ffbda4d6d152cf458 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 09:02:50 +0200 Subject: [PATCH 108/116] Add tests with 1000 pledgers --- .../crowdfunding/src/campaign.cairo | 10 +- .../src/campaign/pledgeable.cairo | 195 ++++++++++++++++-- .../crowdfunding/src/mock_upgrade.cairo | 8 +- 3 files changed, 191 insertions(+), 22 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 0d342b5f..3fde768e 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -23,7 +23,7 @@ pub trait ICampaign { fn cancel(ref self: TContractState, reason: ByteArray); fn pledge(ref self: TContractState, amount: u256); fn get_pledge(self: @TContractState, pledger: ContractAddress) -> u256; - fn get_pledges(self: @TContractState) -> Array<(ContractAddress, u256)>; + fn get_pledgers(self: @TContractState) -> Array; fn get_details(self: @TContractState) -> Details; fn refund(ref self: TContractState, pledger: ContractAddress, reason: ByteArray); fn upgrade(ref self: TContractState, impl_hash: ClassHash, new_end_time: Option); @@ -243,8 +243,8 @@ pub mod Campaign { self.pledges.get(pledger) } - fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.pledges.get_pledges_as_arr() + fn get_pledgers(self: @ContractState) -> Array { + self.pledges.get_pledgers_as_arr() } fn pledge(ref self: ContractState, amount: u256) { @@ -340,8 +340,8 @@ pub mod Campaign { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut pledges = self.pledges.get_pledges_as_arr(); - while let Option::Some((pledger, _)) = pledges.pop_front() { + let mut pledges = self.pledges.get_pledgers_as_arr(); + while let Option::Some(pledger) = pledges.pop_front() { self._refund(pledger); }; self.emit(Event::RefundedAll(RefundedAll { reason })); diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index 1ad7fedd..a17fdb79 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -6,7 +6,7 @@ pub trait IPledgeable { fn add(ref self: TContractState, pledger: ContractAddress, amount: u256); fn get(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledger_count(self: @TContractState) -> u32; - fn get_pledges_as_arr(self: @TContractState) -> Array<(ContractAddress, u256)>; + fn get_pledgers_as_arr(self: @TContractState) -> Array; fn get_total(self: @TContractState) -> u256; fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; } @@ -58,17 +58,14 @@ pub mod pledgeable_component { self.pledger_count.read() } - fn get_pledges_as_arr( - self: @ComponentState - ) -> Array<(ContractAddress, u256)> { + fn get_pledgers_as_arr(self: @ComponentState) -> Array { let mut result = array![]; let mut index = self.pledger_count.read(); while index != 0 { index -= 1; let pledger = self.index_to_pledger.read(index); - let amount: u256 = self.pledger_to_amount.read(pledger); - result.append((pledger, amount)); + result.append(pledger); }; result @@ -141,7 +138,8 @@ mod tests { use super::{pledgeable_component, IPledgeableDispatcher, IPledgeableDispatcherTrait}; use super::pledgeable_component::{PledgeableImpl}; - use starknet::{contract_address_const}; + use starknet::{ContractAddress, contract_address_const}; + use core::num::traits::Zero; type TestingState = pledgeable_component::ComponentState; @@ -188,6 +186,30 @@ mod tests { assert_eq!(pledgeable.get(pledger_2), 500); } + #[test] + fn test_add_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + // set up 1000 pledgers + let mut pledgers: Array::<(ContractAddress, u256)> = array![]; + let mut i: felt252 = 1000; + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + pledgers.append((pledger, amount)); + i -= 1; + }; + + assert_eq!(pledgers.len(), pledgeable.get_pledger_count()); + + while let Option::Some((pledger, expected_amount)) = pledgers + .pop_front() { + let amount = pledgeable.get(pledger); + assert_eq!(amount, expected_amount); + } + } + #[test] fn test_remove() { let mut pledgeable: TestingState = Default::default(); @@ -235,7 +257,123 @@ mod tests { } #[test] - fn test_get_pledges_as_arr() { + fn test_remove_first_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + // set up 1000 pledgers + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; // actual value set up in the while loop + + let mut i: felt252 = expected_pledger_count.into(); + let first_pledger: ContractAddress = i.try_into().unwrap(); + let first_amount = 100000; + pledgeable.add(first_pledger, first_amount); + expected_total += first_amount; + i -= 1; + + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + i -= 1; + }; + + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + assert_eq!(pledgeable.get(first_pledger), first_amount); + + let removed_amount = pledgeable.remove(first_pledger); + + expected_total -= first_amount; + + assert_eq!(removed_amount, first_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count - 1); + assert_eq!(pledgeable.get(first_pledger), 0); + } + + #[test] + fn test_remove_middle_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + // set up 1000 pledgers + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; // actual value set up in the while loop + + let mut middle_pledger: ContractAddress = Zero::zero(); + let mut middle_amount = 0; + + let mut i: felt252 = expected_pledger_count.into(); + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + + if i == 500 { + middle_pledger = pledger; + middle_amount = amount; + } + + i -= 1; + }; + + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + assert_eq!(pledgeable.get(middle_pledger), middle_amount); + + let removed_amount = pledgeable.remove(middle_pledger); + + expected_total -= middle_amount; + + assert_eq!(removed_amount, middle_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count - 1); + assert_eq!(pledgeable.get(middle_pledger), 0); + } + + #[test] + fn test_remove_last_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + // set up 1000 pledgers + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; // actual value set up in the while loop + + let mut i: felt252 = expected_pledger_count.into(); + let last_pledger: ContractAddress = i.try_into().unwrap(); + let last_amount = 100000; + i -= 1; // leave place for the last pledger + + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + i -= 1; + }; + + // add last pledger + pledgeable.add(last_pledger, last_amount); + expected_total += last_amount; + + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + assert_eq!(pledgeable.get(last_pledger), last_amount); + + let removed_amount = pledgeable.remove(last_pledger); + + expected_total -= last_amount; + + assert_eq!(removed_amount, last_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count - 1); + assert_eq!(pledgeable.get(last_pledger), 0); + } + + #[test] + fn test_get_pledgers_as_arr() { let mut pledgeable: TestingState = Default::default(); let pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); @@ -247,12 +385,43 @@ mod tests { // 2nd pledge by pledger_2 *should not* increase the pledge count pledgeable.add(pledger_2, 1500); - let pledges_arr = pledgeable.get_pledges_as_arr(); + let pledgers_arr = pledgeable.get_pledgers_as_arr(); - assert_eq!(pledges_arr.len(), 3); - assert_eq!((pledger_1, 1000), *pledges_arr[2]); - assert_eq!((pledger_2, 2000), *pledges_arr[1]); - assert_eq!((pledger_3, 2500), *pledges_arr[0]); + assert_eq!(pledgers_arr.len(), 3); + assert_eq!(pledger_3, *pledgers_arr[0]); + assert_eq!(2500, pledgeable.get(*pledgers_arr[0])); + assert_eq!(pledger_2, *pledgers_arr[1]); + assert_eq!(2000, pledgeable.get(*pledgers_arr[1])); + assert_eq!(pledger_1, *pledgers_arr[2]); + assert_eq!(1000, pledgeable.get(*pledgers_arr[2])); + } + + #[test] + fn test_get_pledgers_as_arr_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + // set up 1000 pledgers + let mut pledgers: Array:: = array![]; + let mut i: felt252 = 1000; + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + pledgers.append(pledger); + i -= 1; + }; + + let pledgers_arr: Array:: = pledgeable.get_pledgers_as_arr(); + + assert_eq!(pledgers_arr.len(), pledgers.len()); + + let mut i = 1000; + while let Option::Some(expected_pledger) = pledgers.pop_front() { + i -= 1; + // pledgers are fetched in reversed order + let actual_pledger: ContractAddress = *pledgers_arr.at(i); + assert_eq!(expected_pledger, actual_pledger); + } } #[test] diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index d884a734..c1be549e 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -183,8 +183,8 @@ pub mod MockUpgrade { self.pledges.get(pledger) } - fn get_pledges(self: @ContractState) -> Array<(ContractAddress, u256)> { - self.pledges.get_pledges_as_arr() + fn get_pledgers(self: @ContractState) -> Array { + self.pledges.get_pledgers_as_arr() } fn pledge(ref self: ContractState, amount: u256) { @@ -279,8 +279,8 @@ pub mod MockUpgrade { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut pledges = self.pledges.get_pledges_as_arr(); - while let Option::Some((pledger, _)) = pledges.pop_front() { + let mut pledges = self.pledges.get_pledgers_as_arr(); + while let Option::Some(pledger) = pledges.pop_front() { self._refund(pledger); }; self.emit(Event::RefundedAll(RefundedAll { reason })); From d1477c4d723ddf7e7a5c820aaf6ce85975a78a4d Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 09:57:31 +0200 Subject: [PATCH 109/116] add test for add + update existing pledger --- .../src/campaign/pledgeable.cairo | 133 ++++++++++++++++-- 1 file changed, 123 insertions(+), 10 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index a17fdb79..424415eb 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -191,25 +191,136 @@ mod tests { let mut pledgeable: TestingState = Default::default(); // set up 1000 pledgers + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; // actual value set up in the while loop let mut pledgers: Array::<(ContractAddress, u256)> = array![]; - let mut i: felt252 = 1000; + + let mut i: felt252 = expected_pledger_count.into(); while i != 0 { let pledger: ContractAddress = i.try_into().unwrap(); let amount: u256 = i.into() * 100; pledgeable.add(pledger, amount); pledgers.append((pledger, amount)); + expected_total += amount; i -= 1; }; - assert_eq!(pledgers.len(), pledgeable.get_pledger_count()); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + assert_eq!(pledgeable.get_total(), expected_total); while let Option::Some((pledger, expected_amount)) = pledgers .pop_front() { - let amount = pledgeable.get(pledger); - assert_eq!(amount, expected_amount); + assert_eq!(pledgeable.get(pledger), expected_amount); } } + #[test] + fn test_add_update_first_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; + + // set up 1000 pledgers + let mut i: felt252 = expected_pledger_count.into(); + let first_pledger: ContractAddress = i.try_into().unwrap(); + let first_amount: u256 = i.into() * 100; + pledgeable.add(first_pledger, first_amount); + expected_total += first_amount; + + i -= 1; + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + i -= 1; + }; + + // first pledger makes another pledge + pledgeable.add(first_pledger, 2000); + expected_total += 2000; + let expected_amount = first_amount + 2000; + + let amount = pledgeable.get(first_pledger); + assert_eq!(amount, expected_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + } + + #[test] + fn test_add_update_middle_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; + + // set up 1000 pledgers + let mut middle_pledger: ContractAddress = Zero::zero(); + let mut middle_amount = 0; + + let mut i: felt252 = 1000; + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + + if i == 500 { + middle_pledger = pledger; + middle_amount = amount; + } + + i -= 1; + }; + + // middle pledger makes another pledge + pledgeable.add(middle_pledger, 2000); + expected_total += 2000; + let expected_amount = middle_amount + 2000; + + let amount = pledgeable.get(middle_pledger); + assert_eq!(amount, expected_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + } + + #[test] + fn test_add_update_last_of_1000_pledgers() { + let mut pledgeable: TestingState = Default::default(); + + let expected_pledger_count: u32 = 1000; + let mut expected_total: u256 = 0; + + // set up 1000 pledgers + let mut i: felt252 = 1000; + // remember last pledger, add it after while loop + let last_pledger: ContractAddress = i.try_into().unwrap(); + let last_amount = 100000; + + i -= 1; // leave place for the last pledger + while i != 0 { + let pledger: ContractAddress = i.try_into().unwrap(); + let amount: u256 = i.into() * 100; + pledgeable.add(pledger, amount); + expected_total += amount; + i -= 1; + }; + // add last pledger + pledgeable.add(last_pledger, last_amount); + expected_total += last_amount; + + // last pledger makes another pledge + pledgeable.add(last_pledger, 2000); + expected_total += 2000; + let expected_amount = last_amount + 2000; + + let amount = pledgeable.get(last_pledger); + assert_eq!(amount, expected_amount); + assert_eq!(pledgeable.get_total(), expected_total); + assert_eq!(pledgeable.get_pledger_count(), expected_pledger_count); + } + #[test] fn test_remove() { let mut pledgeable: TestingState = Default::default(); @@ -416,12 +527,13 @@ mod tests { assert_eq!(pledgers_arr.len(), pledgers.len()); let mut i = 1000; - while let Option::Some(expected_pledger) = pledgers.pop_front() { - i -= 1; - // pledgers are fetched in reversed order - let actual_pledger: ContractAddress = *pledgers_arr.at(i); - assert_eq!(expected_pledger, actual_pledger); - } + while let Option::Some(expected_pledger) = pledgers + .pop_front() { + i -= 1; + // pledgers are fetched in reversed order + let actual_pledger: ContractAddress = *pledgers_arr.at(i); + assert_eq!(expected_pledger, actual_pledger); + } } #[test] @@ -440,3 +552,4 @@ mod tests { assert_eq!(pledgeable.get(pledger_3), 0); } } + From 4eba1ede48310b46e23c86ad1bf9c84fc1200f2f Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 12:26:06 +0200 Subject: [PATCH 110/116] reenable lib --- listings/applications/crowdfunding/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml index e3f947a6..2d781ca8 100644 --- a/listings/applications/crowdfunding/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -3,7 +3,7 @@ name = "crowdfunding" version.workspace = true edition = "2023_11" -# [lib] +[lib] [dependencies] starknet.workspace = true From f7027091b1e7bb331618a8d0256ca6d05d811384 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 12:54:08 +0200 Subject: [PATCH 111/116] Add link to adv. factory in crowdfunding point 9 --- src/applications/crowdfunding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md index 9e8518e6..8d78f05a 100644 --- a/src/applications/crowdfunding.md +++ b/src/applications/crowdfunding.md @@ -10,7 +10,7 @@ Crowdfunding is a method of raising capital through the collective effort of man 6. After the campaign ends, the campaign creator can claim the funds if the campaign goal is reached. 7. Otherwise, campaign did not reach it's goal, pledgers can retrieve their funds. 8. The creator can at any point cancel the campaign for whatever reason and refund all of the pledgers. -9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state. +9. The contract admin can upgrade the contract implementation, refunding all of the users and reseting the campaign state (we will use this in the [Advanced Factory chapter](./advanced_factory.md)). Because contract upgrades need to be able to refund all of the pledges, we need to be able to iterate over all of the pledgers and their amounts. Since iteration is not supported by `LegacyMap`, we need to create a custom storage type that will encompass pledge management. We use a component for this purpose. From ab66865c4ec41d2ada1fa2c7d0247d0faebbc636 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 13:00:44 +0200 Subject: [PATCH 112/116] write the adv. factory chapter --- src/applications/advanced_factory.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/applications/advanced_factory.md b/src/applications/advanced_factory.md index 14c0bf3d..5d2a27fd 100644 --- a/src/applications/advanced_factory.md +++ b/src/applications/advanced_factory.md @@ -1,7 +1,13 @@ # AdvancedFactory: Crowdfunding -This is the CampaignFactory contract that creates new Campaign contract instances. +This is an example of an advanced factory contract that manages crowdfunding Campaign contracts created in the ["Crowdfunding" chapter](./crowdfunding.md). The advanced factory allows for a centralized creation and management of `Campaign` contracts on the Starknet blockchain, ensuring that they adhere to a standard interface and can be easily upgraded. + +Key Features +1. **Campaign Creation**: Users can create new crowdfunding campaigns with specific details such as title, description, goal, and duration. +2. **Campaign Management**: The factory contract stores and manages the campaigns, allowing for upgrades and tracking. +3. **Upgrade Mechanism**: The factory owner can update the implementation of the campaign contract, ensuring that all campaigns benefit from improvements and bug fixes. + - the factory only updates it's `Campaign` class hash and emits an event to notify any listeners, but the `Campaign` creators are in the end responsible for actually upgrading their contracts. ```rust -{{#include ../../../listings/applications/advanced_factory/src/contract.cairo:contract}} +{{#include ../../listings/applications/advanced_factory/src/contract.cairo:contract}} ``` From cfd76941ab30b865fa0c9e2ce6fabb20f9a15a87 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 13:12:25 +0200 Subject: [PATCH 113/116] upgrade_campaign_implementation-> upgrade_campaign + comment updates --- .../applications/advanced_factory/src/contract.cairo | 7 ++----- listings/applications/advanced_factory/src/tests.cairo | 4 ++-- listings/applications/crowdfunding/src/campaign.cairo | 9 ++++----- .../crowdfunding/src/campaign/pledgeable.cairo | 3 ++- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo index b72913f3..c07000f5 100644 --- a/listings/applications/advanced_factory/src/contract.cairo +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -14,7 +14,7 @@ pub trait ICampaignFactory { ) -> 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( + fn upgrade_campaign( ref self: TContractState, campaign_address: ContractAddress, new_end_time: Option ); } @@ -89,7 +89,6 @@ pub mod CampaignFactory { #[abi(embed_v0)] impl CampaignFactory of super::ICampaignFactory { - // ANCHOR: deploy fn create_campaign( ref self: ContractState, title: ByteArray, @@ -119,7 +118,6 @@ pub mod CampaignFactory { contract_address } - // ANCHOR_END: deploy fn get_campaign_class_hash(self: @ContractState) -> ClassHash { self.campaign_class_hash.read() @@ -129,13 +127,12 @@ pub mod CampaignFactory { self.ownable._assert_only_owner(); assert(new_class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // update own campaign class hash value self.campaign_class_hash.write(new_class_hash); self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); } - fn upgrade_campaign_implementation( + fn upgrade_campaign( ref self: ContractState, campaign_address: ContractAddress, new_end_time: Option ) { assert(campaign_address.is_non_zero(), Errors::ZERO_ADDRESS); diff --git a/listings/applications/advanced_factory/src/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo index 47908b20..eb0cc1b5 100644 --- a/listings/applications/advanced_factory/src/tests.cairo +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -160,7 +160,7 @@ fn test_uprade_campaign_class_hash() { // upgrade pending campaign start_cheat_caller_address(factory.contract_address, pending_campaign_creator); - factory.upgrade_campaign_implementation(pending_campaign, Option::None); + factory.upgrade_campaign(pending_campaign, Option::None); assert_eq!(get_class_hash(pending_campaign), new_class_hash); assert_eq!(get_class_hash(active_campaign), old_class_hash); @@ -177,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::None); + factory.upgrade_campaign(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 3fde768e..2ab3f9fb 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -41,7 +41,7 @@ pub mod Campaign { }; use components::ownable::ownable_component; use super::pledgeable::pledgeable_component; - use super::{Details}; + use super::Details; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: pledgeable_component, storage: pledges, event: PledgeableEvent); @@ -199,6 +199,8 @@ pub mod Campaign { self.emit(Event::Canceled(Canceled { reason })); } + /// Sends the funds to the campaign creator. + /// It leaves the pledge data intact as a testament to campaign success fn claim(ref self: ContractState) { self._assert_only_creator(); assert(self._is_started(), Errors::NOT_STARTED); @@ -214,9 +216,6 @@ pub mod Campaign { self.claimed.write(true); - // no need to reset the pledges, 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); @@ -291,7 +290,7 @@ pub mod Campaign { self.ownable._assert_only_owner(); assert(impl_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); - // only active campaigns have funds to refund and an end time to update + // only active campaigns have pledges to refund and an end time to update if self._is_started() { if let Option::Some(end_time) = new_end_time { assert(end_time >= get_block_timestamp(), Errors::END_BEFORE_NOW); diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index 424415eb..2e74d1a0 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -92,7 +92,8 @@ pub mod pledgeable_component { if self.index_to_pledger.read(pledger_index) == pledger { break; } - assert(pledger_index > 0, Errors::INCONSISTENT_STATE); + // if pledger_to_amount contains a pledger, then so does index_to_pledger + // thus this will never underflow pledger_index -= 1; }; From 5c39c5a31f2e47bba521c94e4438969c14cfcc1d Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 17 Jun 2024 13:14:41 +0200 Subject: [PATCH 114/116] rename get_pledgers_as_arr->array --- .../applications/crowdfunding/src/campaign.cairo | 4 ++-- .../crowdfunding/src/campaign/pledgeable.cairo | 12 ++++++------ .../applications/crowdfunding/src/mock_upgrade.cairo | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo index 2ab3f9fb..e5b5faf6 100644 --- a/listings/applications/crowdfunding/src/campaign.cairo +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -243,7 +243,7 @@ pub mod Campaign { } fn get_pledgers(self: @ContractState) -> Array { - self.pledges.get_pledgers_as_arr() + self.pledges.array() } fn pledge(ref self: ContractState, amount: u256) { @@ -339,7 +339,7 @@ pub mod Campaign { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut pledges = self.pledges.get_pledgers_as_arr(); + let mut pledges = self.pledges.array(); while let Option::Some(pledger) = pledges.pop_front() { self._refund(pledger); }; diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo index 2e74d1a0..4000057a 100644 --- a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -6,7 +6,7 @@ pub trait IPledgeable { fn add(ref self: TContractState, pledger: ContractAddress, amount: u256); fn get(self: @TContractState, pledger: ContractAddress) -> u256; fn get_pledger_count(self: @TContractState) -> u32; - fn get_pledgers_as_arr(self: @TContractState) -> Array; + fn array(self: @TContractState) -> Array; fn get_total(self: @TContractState) -> u256; fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; } @@ -58,7 +58,7 @@ pub mod pledgeable_component { self.pledger_count.read() } - fn get_pledgers_as_arr(self: @ComponentState) -> Array { + fn array(self: @ComponentState) -> Array { let mut result = array![]; let mut index = self.pledger_count.read(); @@ -485,7 +485,7 @@ mod tests { } #[test] - fn test_get_pledgers_as_arr() { + fn test_array() { let mut pledgeable: TestingState = Default::default(); let pledger_1 = contract_address_const::<'pledger_1'>(); let pledger_2 = contract_address_const::<'pledger_2'>(); @@ -497,7 +497,7 @@ mod tests { // 2nd pledge by pledger_2 *should not* increase the pledge count pledgeable.add(pledger_2, 1500); - let pledgers_arr = pledgeable.get_pledgers_as_arr(); + let pledgers_arr = pledgeable.array(); assert_eq!(pledgers_arr.len(), 3); assert_eq!(pledger_3, *pledgers_arr[0]); @@ -509,7 +509,7 @@ mod tests { } #[test] - fn test_get_pledgers_as_arr_1000_pledgers() { + fn test_array_1000_pledgers() { let mut pledgeable: TestingState = Default::default(); // set up 1000 pledgers @@ -523,7 +523,7 @@ mod tests { i -= 1; }; - let pledgers_arr: Array:: = pledgeable.get_pledgers_as_arr(); + let pledgers_arr: Array:: = pledgeable.array(); assert_eq!(pledgers_arr.len(), pledgers.len()); diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo index c1be549e..33348d16 100644 --- a/listings/applications/crowdfunding/src/mock_upgrade.cairo +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -184,7 +184,7 @@ pub mod MockUpgrade { } fn get_pledgers(self: @ContractState) -> Array { - self.pledges.get_pledgers_as_arr() + self.pledges.array() } fn pledge(ref self: ContractState, amount: u256) { @@ -279,7 +279,7 @@ pub mod MockUpgrade { } fn _refund_all(ref self: ContractState, reason: ByteArray) { - let mut pledges = self.pledges.get_pledgers_as_arr(); + let mut pledges = self.pledges.array(); while let Option::Some(pledger) = pledges.pop_front() { self._refund(pledger); }; From 9ddcaf6ff6a4ad5f3a93c4d3fbae732344364f91 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 26 Jun 2024 08:39:21 +0200 Subject: [PATCH 115/116] Use ERC20Upgradeable instead of ERC20 preset --- listings/applications/crowdfunding/Scarb.toml | 2 +- listings/applications/crowdfunding/src/tests.cairo | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml index 2d781ca8..074a2713 100644 --- a/listings/applications/crowdfunding/Scarb.toml +++ b/listings/applications/crowdfunding/Scarb.toml @@ -16,4 +16,4 @@ test.workspace = true [[target.starknet-contract]] casm = true -build-external-contracts = ["openzeppelin::presets::erc20::ERC20"] +build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index f569f105..4145458a 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -121,7 +121,7 @@ fn test_deploy() { #[test] fn test_successful_campaign() { - let token_class = declare("ERC20").unwrap(); + let token_class = declare("ERC20Upgradeable").unwrap(); let contract_class = declare("Campaign").unwrap(); let (campaign, token) = deploy_with_token(contract_class, token_class); @@ -218,7 +218,7 @@ fn test_upgrade_class_hash() { // test pending campaign let contract_class = declare("Campaign").unwrap(); - let token_class = declare("ERC20").unwrap(); + let token_class = declare("ERC20Upgradeable").unwrap(); let (campaign, _) = deploy_with_token(contract_class, token_class); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); @@ -286,7 +286,7 @@ fn test_upgrade_class_hash() { #[test] fn test_cancel() { let contract_class = declare("Campaign").unwrap(); - let token_class = declare("ERC20").unwrap(); + let token_class = declare("ERC20Upgradeable").unwrap(); // test canceled campaign let (campaign, token) = deploy_with_token(contract_class, token_class); @@ -393,7 +393,7 @@ fn test_cancel() { fn test_refund() { // setup let (campaign, token) = deploy_with_token( - declare("Campaign").unwrap(), declare("ERC20").unwrap() + declare("Campaign").unwrap(), declare("ERC20Upgradeable").unwrap() ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let creator = contract_address_const::<'creator'>(); @@ -444,7 +444,7 @@ fn test_refund() { fn test_unpledge() { // setup let (campaign, token) = deploy_with_token( - declare("Campaign").unwrap(), declare("ERC20").unwrap() + declare("Campaign").unwrap(), declare("ERC20Upgradeable").unwrap() ); let mut spy = spy_events(SpyOn::One(campaign.contract_address)); let pledger = contract_address_const::<'pledger_1'>(); From 48f78fefbf784dd8cd007a041d7b54c6a8f9db9b Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 26 Jun 2024 08:47:37 +0200 Subject: [PATCH 116/116] Add missing token recipient ctor argument in crowdfunding tests --- listings/applications/crowdfunding/src/tests.cairo | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/listings/applications/crowdfunding/src/tests.cairo b/listings/applications/crowdfunding/src/tests.cairo index 4145458a..fde363c2 100644 --- a/listings/applications/crowdfunding/src/tests.cairo +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -47,10 +47,12 @@ fn deploy_with_token( let token_symbol: ByteArray = "MTKN"; let token_supply: u256 = 100000; let token_owner = contract_address_const::<'token_owner'>(); + let token_recipient = token_owner; // deploy ERC20 token let mut token_constructor_calldata = array![]; - (token_name, token_symbol, token_supply, token_owner).serialize(ref token_constructor_calldata); + ((token_name, token_symbol, token_supply, token_recipient), token_owner) + .serialize(ref token_constructor_calldata); let (token_address, _) = token.deploy(@token_constructor_calldata).unwrap(); // transfer amounts to some pledgers