diff --git a/.tool-versions b/.tool-versions index c62bafce..dc8930c0 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -scarb 2.6.4 -starknet-foundry 0.24.0 +scarb 2.6.5 +starknet-foundry 0.25.0 diff --git a/Scarb.lock b/Scarb.lock index b2646014..c96e3249 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -1,6 +1,16 @@ # 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" @@ -44,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" @@ -98,13 +117,20 @@ dependencies = [ [[package]] name = "openzeppelin" -version = "0.11.0" -source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.11.0#a83f36b23f1af6e160288962be4a2701c3ecbcda" +version = "0.14.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.14.0#f091c4f51ddeb10297db984acae965328c5a4e5b" [[package]] name = "scarb" version = "0.1.0" +[[package]] +name = "simple_account" +version = "0.1.0" +dependencies = [ + "openzeppelin", +] + [[package]] name = "simple_vault" version = "0.1.0" @@ -114,8 +140,8 @@ dependencies = [ [[package]] name = "snforge_std" -version = "0.24.0" -source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.24.0#95e9fb09cb91b3c05295915179ee1b55bf923653" +version = "0.25.0" +source = "git+https://github.com/foundry-rs/starknet-foundry.git?tag=v0.25.0#5b366e24821e530fea97f11b211d220e8493fbea" [[package]] name = "staking" diff --git a/Scarb.toml b/Scarb.toml index 4c5c042d..8df8fcb2 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -12,10 +12,12 @@ test = "$(git rev-parse --show-toplevel)/scripts/test_resolver.sh" [workspace.tool.snforge] [workspace.dependencies] -starknet = ">=2.6.3" -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.11.0" } +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.24.0" } +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] description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet." diff --git a/listings/advanced-concepts/simple_account/.gitignore b/listings/advanced-concepts/simple_account/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/listings/advanced-concepts/simple_account/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/advanced-concepts/simple_account/Scarb.lock b/listings/advanced-concepts/simple_account/Scarb.lock new file mode 100644 index 00000000..f287d8c3 --- /dev/null +++ b/listings/advanced-concepts/simple_account/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "ecdsa_verification" +version = "0.1.0" diff --git a/listings/advanced-concepts/simple_account/Scarb.toml b/listings/advanced-concepts/simple_account/Scarb.toml new file mode 100644 index 00000000..f6fbac41 --- /dev/null +++ b/listings/advanced-concepts/simple_account/Scarb.toml @@ -0,0 +1,14 @@ +[package] +name = "simple_account" +version.workspace = true +edition = '2023_11' + + +[dependencies] +starknet.workspace = true +openzeppelin.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] diff --git a/listings/advanced-concepts/simple_account/src/lib.cairo b/listings/advanced-concepts/simple_account/src/lib.cairo new file mode 100644 index 00000000..011ae9e3 --- /dev/null +++ b/listings/advanced-concepts/simple_account/src/lib.cairo @@ -0,0 +1,4 @@ +mod simple_account; + +#[cfg(test)] +mod tests; diff --git a/listings/advanced-concepts/simple_account/src/simple_account.cairo b/listings/advanced-concepts/simple_account/src/simple_account.cairo new file mode 100644 index 00000000..6de8b4ae --- /dev/null +++ b/listings/advanced-concepts/simple_account/src/simple_account.cairo @@ -0,0 +1,90 @@ +use starknet::account::Call; + +#[starknet::interface] +trait ISRC6 { + fn execute_calls(self: @TContractState, calls: Array) -> Array>; + fn validate_calls(self: @TContractState, calls: Array) -> felt252; + fn is_valid_signature( + self: @TContractState, hash: felt252, signature: Array + ) -> felt252; +} + +#[starknet::contract] +mod simpleAccount { + use super::ISRC6; + use starknet::account::Call; + use core::num::traits::Zero; + use core::ecdsa::check_ecdsa_signature; + + // Implement SRC5 with openzeppelin + use openzeppelin::account::interface; + use openzeppelin::introspection::src5::SRC5Component; + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + src5: SRC5Component::Storage, + public_key: felt252 + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.src5.register_interface(interface::ISRC6_ID); + self.public_key.write(public_key); + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + SRC5Event: SRC5Component::Event + } + + #[abi(embed_v0)] + impl SRC6 of ISRC6 { + fn execute_calls(self: @ContractState, calls: Array) -> Array> { + assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol'); + let Call { to, selector, calldata } = calls.at(0); + let res = starknet::syscalls::call_contract_syscall(*to, *selector, *calldata).unwrap(); + array![res] + } + + fn validate_calls(self: @ContractState, calls: Array) -> felt252 { + assert(starknet::get_caller_address().is_zero(), 'Not Starknet Protocol'); + let tx_info = starknet::get_tx_info().unbox(); + let tx_hash = tx_info.transaction_hash; + let signature = tx_info.signature; + if self._is_valid_signature(tx_hash, signature) { + starknet::VALIDATED + } else { + 0 + } + } + + fn is_valid_signature( + self: @ContractState, hash: felt252, signature: Array + ) -> felt252 { + if self._is_valid_signature(hash, signature.span()) { + starknet::VALIDATED + } else { + 0 + } + } + } + + #[generate_trait] + impl SignatureVerificationImpl of SignatureVerification { + fn _is_valid_signature( + self: @ContractState, hash: felt252, signature: Span + ) -> bool { + check_ecdsa_signature( + hash, self.public_key.read(), *signature.at(0_u32), *signature.at(1_u32) + ) + } + } +} diff --git a/listings/advanced-concepts/simple_account/src/tests.cairo b/listings/advanced-concepts/simple_account/src/tests.cairo new file mode 100644 index 00000000..aefd6eb5 --- /dev/null +++ b/listings/advanced-concepts/simple_account/src/tests.cairo @@ -0,0 +1,3 @@ +#[cfg(test)] +mod tests { // TODO +} 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 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..5935e01f --- /dev/null +++ b/listings/applications/advanced_factory/Scarb.toml @@ -0,0 +1,18 @@ +[package] +name = "advanced_factory" +version.workspace = true +edition = "2023_11" + +[dependencies] +starknet.workspace = true +components.workspace = true +alexandria_storage.workspace = true +snforge_std.workspace = true +crowdfunding = { path = "../crowdfunding" } + +[scripts] +test.workspace = true + +[[target.starknet-contract]] +casm = true +build-external-contracts = ["crowdfunding::campaign::Campaign"] diff --git a/listings/applications/advanced_factory/src/contract.cairo b/listings/applications/advanced_factory/src/contract.cairo new file mode 100644 index 00000000..c07000f5 --- /dev/null +++ b/listings/applications/advanced_factory/src/contract.cairo @@ -0,0 +1,152 @@ +// ANCHOR: contract +pub use starknet::{ContractAddress, ClassHash}; + +#[starknet::interface] +pub trait ICampaignFactory { + fn create_campaign( + ref self: TContractState, + title: ByteArray, + description: ByteArray, + goal: u256, + 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( + ref self: TContractState, campaign_address: ContractAddress, new_end_time: Option + ); +} + +#[starknet::contract] +pub mod CampaignFactory { + 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 crowdfunding::campaign::{ICampaignDispatcher, ICampaignDispatcherTrait}; + 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 and thei class hashes + campaigns: LegacyMap<(ContractAddress, ContractAddress), ClassHash>, + /// 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, + CampaignClassHashUpgraded: CampaignClassHashUpgraded, + CampaignCreated: CampaignCreated, + ClassHashUpdated: ClassHashUpdated, + } + + #[derive(Drop, starknet::Event)] + pub struct ClassHashUpdated { + pub new_class_hash: ClassHash, + } + + #[derive(Drop, starknet::Event)] + pub struct CampaignClassHashUpgraded { + pub campaign: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct CampaignCreated { + 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] + 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 CampaignFactory of super::ICampaignFactory { + fn create_campaign( + ref self: ContractState, + title: ByteArray, + description: ByteArray, + goal: u256, + 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), start_time, end_time, token_address) + .serialize(ref constructor_calldata); + + // Contract deployment + let (contract_address, _) = deploy_syscall( + self.campaign_class_hash.read(), 0, constructor_calldata.span(), false + ) + .unwrap_syscall(); + + // track new campaign instance + self.campaigns.write((creator, contract_address), self.campaign_class_hash.read()); + + self.emit(Event::CampaignCreated(CampaignCreated { creator, contract_address })); + + contract_address + } + + 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(); + assert(new_class_hash.is_non_zero(), Errors::CLASS_HASH_ZERO); + + self.campaign_class_hash.write(new_class_hash); + + self.emit(Event::ClassHashUpdated(ClassHashUpdated { new_class_hash })); + } + + fn upgrade_campaign( + ref self: ContractState, campaign_address: ContractAddress, new_end_time: 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_end_time); + } + } +} +// ANCHOR_END: contract + + diff --git a/listings/applications/advanced_factory/src/lib.cairo b/listings/applications/advanced_factory/src/lib.cairo new file mode 100644 index 00000000..541355ed --- /dev/null +++ b/listings/applications/advanced_factory/src/lib.cairo @@ -0,0 +1,5 @@ +mod contract; +mod mock_upgrade; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/advanced_factory/src/mock_upgrade.cairo b/listings/applications/advanced_factory/src/mock_upgrade.cairo new file mode 100644 index 00000000..919c40ae --- /dev/null +++ b/listings/applications/advanced_factory/src/mock_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/tests.cairo b/listings/applications/advanced_factory/src/tests.cairo new file mode 100644 index 00000000..eb0cc1b5 --- /dev/null +++ b/listings/applications/advanced_factory/src/tests.cairo @@ -0,0 +1,194 @@ +use core::traits::TryInto; +use core::clone::Clone; +use core::result::ResultTrait; +use advanced_factory::contract::{ + CampaignFactory, ICampaignFactoryDispatcher, ICampaignFactoryDispatcherTrait +}; +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 +}; + +// Define a goal contract to deploy +use crowdfunding::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) -> ICampaignFactoryDispatcher { + let mut constructor_calldata: @Array:: = @array![campaign_class_hash.into()]; + + 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(); + + stop_cheat_caller_address(contract_address); + + ICampaignFactoryDispatcher { contract_address } +} + +/// Deploy a campaign factory contract with default campaign class hash +fn deploy_factory() -> ICampaignFactoryDispatcher { + 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_create_campaign() { + let factory = deploy_factory(); + + let mut spy = spy_events(SpyOn::One(factory.contract_address)); + + 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"; + let goal: u256 = 10000; + 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, 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.start_time, start_time); + assert_eq!(details.end_time, end_time); + 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); + + let campaign_ownable = IOwnableDispatcher { contract_address: campaign_address }; + assert_eq!(campaign_ownable.owner(), factory.contract_address); + + spy + .assert_emitted( + @array![ + ( + factory.contract_address, + CampaignFactory::Event::CampaignCreated( + CampaignFactory::CampaignCreated { + creator: campaign_creator, contract_address: campaign_address + } + ) + ) + ] + ); +} + +#[test] +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'>(); + + // 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, 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, start_time_active, end_time_active, token + ); + + 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 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!(old_class_hash, get_class_hash(pending_campaign)); + assert_eq!(old_class_hash, get_class_hash(active_campaign)); + + spy + .assert_emitted( + @array![ + ( + factory.contract_address, + CampaignFactory::Event::ClassHashUpdated( + CampaignFactory::ClassHashUpdated { new_class_hash } + ) + ) + ] + ); + + // upgrade pending campaign + start_cheat_caller_address(factory.contract_address, pending_campaign_creator); + 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); + + spy + .assert_emitted( + @array![ + ( + 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(active_campaign, Option::None); + + assert_eq!(get_class_hash(pending_campaign), new_class_hash); + assert_eq!(get_class_hash(active_campaign), new_class_hash); + + spy + .assert_emitted( + @array![ + ( + active_campaign, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ) + ] + ); +} diff --git a/listings/applications/components/src/contracts.cairo b/listings/applications/components/src/contracts.cairo deleted file mode 100644 index 48f48bb4..00000000 --- a/listings/applications/components/src/contracts.cairo +++ /dev/null @@ -1,6 +0,0 @@ -mod switch; -mod switch_collision; -mod owned; - -#[cfg(test)] -mod tests; diff --git a/listings/applications/components/src/contracts/owned.cairo b/listings/applications/components/src/contracts/owned.cairo deleted file mode 100644 index 3eacbdd8..00000000 --- a/listings/applications/components/src/contracts/owned.cairo +++ /dev/null @@ -1,174 +0,0 @@ -// ANCHOR: contract -#[starknet::interface] -pub trait IOwned { - fn do_something(ref self: TContractState); -} - -#[starknet::contract] -pub mod OwnedContract { - use components::ownable::{IOwnable, ownable_component, ownable_component::OwnableInternalTrait}; - - 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, - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.ownable._init(starknet::get_caller_address()); - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - OwnableEvent: ownable_component::Event, - } - - #[abi(embed_v0)] - impl Owned of super::IOwned { - fn do_something(ref self: ContractState) { - self.ownable._assert_only_owner(); - // ... - } - } -} -// ANCHOR_END: contract - -#[cfg(test)] -mod tests { - use core::num::traits::Zero; - use super::{OwnedContract, IOwnedDispatcher, IOwnedDispatcherTrait}; - use components::ownable::{IOwnable, IOwnableDispatcher, IOwnableDispatcherTrait}; - - use starknet::{contract_address_const, ContractAddress}; - use starknet::testing::{set_caller_address, set_contract_address}; - use starknet::storage::StorageMemberAccessTrait; - use starknet::SyscallResultTrait; - use starknet::syscalls::deploy_syscall; - - fn deploy() -> (IOwnedDispatcher, IOwnableDispatcher) { - let (contract_address, _) = deploy_syscall( - OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false - ) - .unwrap_syscall(); - - (IOwnedDispatcher { contract_address }, IOwnableDispatcher { contract_address },) - } - - #[test] - #[available_gas(2000000)] - fn test_init() { - let owner = contract_address_const::<'owner'>(); - set_contract_address(owner); - let (_, ownable) = deploy(); - - assert(ownable.owner() == owner, 'wrong_owner'); - } - - #[test] - #[available_gas(2000000)] - fn test_wrong_owner() { - set_contract_address(contract_address_const::<'owner'>()); - let (_, ownable) = deploy(); - - let not_owner = contract_address_const::<'not_owner'>(); - assert(ownable.owner() != not_owner, 'wrong_owner'); - } - - #[test] - #[available_gas(2000000)] - fn test_do_something() { - set_contract_address(contract_address_const::<'owner'>()); - let (contract, _) = deploy(); - - contract.do_something(); - // Should not panic - } - - #[test] - #[available_gas(2000000)] - #[should_panic] - fn test_do_something_not_owner() { - set_contract_address(contract_address_const::<'owner'>()); - let (contract, _) = deploy(); - - set_contract_address(contract_address_const::<'not_owner'>()); - contract.do_something(); - } - - #[test] - #[available_gas(2000000)] - fn test_transfer_ownership() { - set_contract_address(contract_address_const::<'initial'>()); - let (contract, ownable) = deploy(); - - let new_owner = contract_address_const::<'new_owner'>(); - ownable.transfer_ownership(new_owner); - - assert(ownable.owner() == new_owner, 'wrong_owner'); - - set_contract_address(new_owner); - contract.do_something(); - } - - #[test] - #[available_gas(2000000)] - #[should_panic] - fn test_transfer_ownership_not_owner() { - set_contract_address(contract_address_const::<'initial'>()); - let (_, ownable) = deploy(); - - set_contract_address(contract_address_const::<'not_owner'>()); - ownable.transfer_ownership(contract_address_const::<'new_owner'>()); - } - - #[test] - #[available_gas(2000000)] - #[should_panic] - fn test_transfer_ownership_zero_error() { - set_contract_address(contract_address_const::<'initial'>()); - let (_, ownable) = deploy(); - - ownable.transfer_ownership(Zero::zero()); - } - - #[test] - #[available_gas(2000000)] - fn test_renounce_ownership() { - set_contract_address(contract_address_const::<'owner'>()); - let (_, ownable) = deploy(); - - ownable.renounce_ownership(); - assert(ownable.owner() == Zero::zero(), 'not_zero_owner'); - } - - #[test] - #[available_gas(2000000)] - #[should_panic] - fn test_renounce_ownership_not_owner() { - set_contract_address(contract_address_const::<'owner'>()); - let (_, ownable) = deploy(); - - set_contract_address(contract_address_const::<'not_owner'>()); - ownable.renounce_ownership(); - } - - #[test] - #[available_gas(2000000)] - #[should_panic] - fn test_renounce_ownership_previous_owner() { - set_contract_address(contract_address_const::<'owner'>()); - let (contract, ownable) = deploy(); - - ownable.renounce_ownership(); - - contract.do_something(); - } -} diff --git a/listings/applications/components/src/contracts/switch.cairo b/listings/applications/components/src/contracts/switch.cairo deleted file mode 100644 index e6219d12..00000000 --- a/listings/applications/components/src/contracts/switch.cairo +++ /dev/null @@ -1,84 +0,0 @@ -// ANCHOR: contract -#[starknet::contract] -pub mod SwitchContract { - use components::switchable::switchable_component; - - component!(path: switchable_component, storage: switch, event: SwitchableEvent); - - #[abi(embed_v0)] - impl SwitchableImpl = switchable_component::Switchable; - impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - switch: switchable_component::Storage, - } - - #[constructor] - fn constructor(ref self: ContractState) { - self.switch._off(); - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - SwitchableEvent: switchable_component::Event, - } -} -// ANCHOR_END: contract - -#[cfg(test)] -mod tests { - use components::switchable::switchable_component::SwitchableInternalTrait; - use components::switchable::ISwitchable; - - use starknet::storage::StorageMemberAccessTrait; - use super::SwitchContract; - - fn STATE() -> SwitchContract::ContractState { - SwitchContract::contract_state_for_testing() - } - - #[test] - #[available_gas(2000000)] - fn test_init() { - let state = STATE(); - assert(state.is_on() == false, 'The switch should be off'); - } - - #[test] - #[available_gas(2000000)] - fn test_switch() { - let mut state = STATE(); - - state.switch(); - assert(state.is_on() == true, 'The switch should be on'); - - state.switch(); - assert(state.is_on() == false, 'The switch should be off'); - } - - #[test] - #[available_gas(2000000)] - fn test_value() { - let mut state = STATE(); - assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value'); - - state.switch.switch(); - assert(state.is_on() == state.switch.switchable_value.read(), 'Wrong value'); - } - - #[test] - #[available_gas(2000000)] - fn test_internal_off() { - let mut state = STATE(); - - state.switch._off(); - assert(state.is_on() == false, 'The switch should be off'); - - state.switch(); - state.switch._off(); - assert(state.is_on() == false, 'The switch should be off'); - } -} diff --git a/listings/applications/components/src/contracts/switch_collision.cairo b/listings/applications/components/src/contracts/switch_collision.cairo deleted file mode 100644 index fa8f36c9..00000000 --- a/listings/applications/components/src/contracts/switch_collision.cairo +++ /dev/null @@ -1,49 +0,0 @@ -// ANCHOR: interface -#[starknet::interface] -pub trait ISwitchCollision { - fn set(ref self: TContractState, value: bool); - fn get(ref self: TContractState) -> bool; -} -// ANCHOR_END: interface - -#[starknet::contract] -pub mod SwitchCollisionContract { - use components::switchable::switchable_component; - - component!(path: switchable_component, storage: switch, event: SwitchableEvent); - - #[abi(embed_v0)] - impl SwitchableImpl = switchable_component::Switchable; - impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl; - - // ANCHOR: storage - #[storage] - struct Storage { - switchable_value: bool, - #[substorage(v0)] - switch: switchable_component::Storage, - } - // ANCHOR_END: storage - - #[constructor] - fn constructor(ref self: ContractState) { - self.switch._off(); - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - SwitchableEvent: switchable_component::Event, - } - - #[abi(embed_v0)] - impl SwitchCollisionContract of super::ISwitchCollision { - fn set(ref self: ContractState, value: bool) { - self.switchable_value.write(value); - } - - fn get(ref self: ContractState) -> bool { - self.switchable_value.read() - } - } -} diff --git a/listings/applications/components/src/contracts/tests.cairo b/listings/applications/components/src/contracts/tests.cairo deleted file mode 100644 index 8b3bd47d..00000000 --- a/listings/applications/components/src/contracts/tests.cairo +++ /dev/null @@ -1 +0,0 @@ -mod switch_collision_tests; diff --git a/listings/applications/components/src/contracts/tests/switch_collision_tests.cairo b/listings/applications/components/src/contracts/tests/switch_collision_tests.cairo deleted file mode 100644 index 9ffe848e..00000000 --- a/listings/applications/components/src/contracts/tests/switch_collision_tests.cairo +++ /dev/null @@ -1,45 +0,0 @@ -mod switch_collision_tests { - use components::switchable::switchable_component::SwitchableInternalTrait; - use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait}; - - use components::contracts::switch_collision::{ - SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait - }; - - use starknet::storage::StorageMemberAccessTrait; - use starknet::SyscallResultTrait; - use starknet::syscalls::deploy_syscall; - - fn deploy() -> (ISwitchCollisionDispatcher, ISwitchableDispatcher) { - let (contract_address, _) = deploy_syscall( - SwitchCollisionContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false - ) - .unwrap_syscall(); - - ( - ISwitchCollisionDispatcher { contract_address }, - ISwitchableDispatcher { contract_address }, - ) - } - - #[test] - #[available_gas(2000000)] - // ANCHOR: collision - fn test_collision() { - let (mut contract, mut contract_iswitch) = deploy(); - - assert(contract.get() == false, 'value !off'); - assert(contract_iswitch.is_on() == false, 'switch !off'); - - contract_iswitch.switch(); - assert(contract_iswitch.is_on() == true, 'switch !on'); - assert(contract.get() == true, 'value !on'); - - // `collision` between component storage 'value' and contract storage 'value' - assert(contract.get() == contract_iswitch.is_on(), 'value != switch'); - - contract.set(false); - assert(contract.get() == contract_iswitch.is_on(), 'value != switch'); - } -// ANCHOR_END: collision -} diff --git a/listings/applications/components/src/countable.cairo b/listings/applications/components/src/countable.cairo index bafcd6ca..6ca26c63 100644 --- a/listings/applications/components/src/countable.cairo +++ b/listings/applications/components/src/countable.cairo @@ -1,3 +1,4 @@ +// ANCHOR: component #[starknet::interface] pub trait ICountable { fn get(self: @TContractState) -> u32; @@ -24,3 +25,65 @@ pub mod countable_component { } } } +// ANCHOR_END: component + +#[starknet::contract] +mod CountableContract { + use super::countable_component; + + component!(path: countable_component, storage: countable, event: CountableEvent); + + #[storage] + struct Storage { + #[substorage(v0)] + countable: countable_component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + CountableEvent: countable_component::Event + } + + #[abi(embed_v0)] + impl CountableImpl = countable_component::Countable; +} + + +#[cfg(test)] +mod test { + use super::CountableContract; + use super::{ICountableDispatcher, ICountableDispatcherTrait}; + use starknet::syscalls::deploy_syscall; + use starknet::SyscallResultTrait; + + fn deploy_countable() -> ICountableDispatcher { + let (address, _) = deploy_syscall( + CountableContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap_syscall(); + ICountableDispatcher { contract_address: address } + } + + #[test] + fn test_constructor() { + let counter = deploy_countable(); + assert_eq!(counter.get(), 0); + } + + #[test] + fn test_increment() { + let counter = deploy_countable(); + counter.increment(); + assert_eq!(counter.get(), 1); + } + + #[test] + fn test_multiple_increments() { + let counter = deploy_countable(); + counter.increment(); + counter.increment(); + counter.increment(); + assert_eq!(counter.get(), 3); + } +} diff --git a/listings/applications/components/src/lib.cairo b/listings/applications/components/src/lib.cairo index 1c49f9d4..450e211a 100644 --- a/listings/applications/components/src/lib.cairo +++ b/listings/applications/components/src/lib.cairo @@ -3,4 +3,5 @@ pub mod switchable; pub mod countable; pub mod ownable; -mod contracts; +// Not components but using them for specific examples +mod others; diff --git a/listings/applications/components/src/others.cairo b/listings/applications/components/src/others.cairo new file mode 100644 index 00000000..d79c3831 --- /dev/null +++ b/listings/applications/components/src/others.cairo @@ -0,0 +1 @@ +mod switch_collision; diff --git a/listings/applications/components/src/others/switch_collision.cairo b/listings/applications/components/src/others/switch_collision.cairo new file mode 100644 index 00000000..58b2973f --- /dev/null +++ b/listings/applications/components/src/others/switch_collision.cairo @@ -0,0 +1,93 @@ +// ANCHOR: interface +#[starknet::interface] +pub trait ISwitchCollision { + fn set(ref self: TContractState, value: bool); + fn get(ref self: TContractState) -> bool; +} +// ANCHOR_END: interface + +#[starknet::contract] +pub mod SwitchCollisionContract { + use components::switchable::switchable_component; + + component!(path: switchable_component, storage: switch, event: SwitchableEvent); + + #[abi(embed_v0)] + impl SwitchableImpl = switchable_component::Switchable; + impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl; + + // ANCHOR: storage + #[storage] + struct Storage { + switchable_value: bool, + #[substorage(v0)] + switch: switchable_component::Storage, + } + // ANCHOR_END: storage + + #[constructor] + fn constructor(ref self: ContractState) { + self.switch._off(); + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + SwitchableEvent: switchable_component::Event, + } + + #[abi(embed_v0)] + impl SwitchCollisionContract of super::ISwitchCollision { + fn set(ref self: ContractState, value: bool) { + self.switchable_value.write(value); + } + + fn get(ref self: ContractState) -> bool { + self.switchable_value.read() + } + } +} + +#[cfg(test)] +mod switch_collision_tests { + use components::switchable::switchable_component::SwitchableInternalTrait; + use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait}; + use super::{ + SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait + }; + use starknet::storage::StorageMemberAccessTrait; + use starknet::SyscallResultTrait; + use starknet::syscalls::deploy_syscall; + + fn deploy() -> (ISwitchCollisionDispatcher, ISwitchableDispatcher) { + let (contract_address, _) = deploy_syscall( + SwitchCollisionContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap_syscall(); + + ( + ISwitchCollisionDispatcher { contract_address }, + ISwitchableDispatcher { contract_address }, + ) + } + + #[test] + // ANCHOR: collision + fn test_collision() { + let (mut contract, mut contract_iswitch) = deploy(); + + assert_eq!(contract.get(), false); + assert_eq!(contract_iswitch.is_on(), false); + + contract_iswitch.switch(); + assert_eq!(contract_iswitch.is_on(), true); + assert_eq!(contract.get(), true); + + // `collision` between component storage 'value' and contract storage 'value' + assert_eq!(contract.get(), contract_iswitch.is_on()); + + contract.set(false); + assert_eq!(contract.get(), contract_iswitch.is_on()); + } +// ANCHOR_END: collision +} diff --git a/listings/applications/components/src/ownable.cairo b/listings/applications/components/src/ownable.cairo index cca82ca5..68919dd7 100644 --- a/listings/applications/components/src/ownable.cairo +++ b/listings/applications/components/src/ownable.cairo @@ -1,3 +1,4 @@ +// ANCHOR: component use starknet::ContractAddress; #[starknet::interface] @@ -15,8 +16,8 @@ pub mod Errors { #[starknet::component] pub mod ownable_component { - use starknet::{ContractAddress, get_caller_address}; use super::Errors; + use starknet::{ContractAddress, get_caller_address}; use core::num::traits::Zero; #[storage] @@ -24,19 +25,19 @@ pub mod ownable_component { ownable_owner: ContractAddress, } - #[derive(Drop, starknet::Event)] - struct OwnershipTransferredEvent { - previous: ContractAddress, - new: ContractAddress + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct OwnershipTransferredEvent { + pub previous: ContractAddress, + pub new: ContractAddress } - #[derive(Drop, starknet::Event)] - struct OwnershipRenouncedEvent { - previous: ContractAddress + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct OwnershipRenouncedEvent { + pub previous: ContractAddress } #[event] - #[derive(Drop, starknet::Event)] + #[derive(Drop, Debug, PartialEq, starknet::Event)] pub enum Event { OwnershipTransferredEvent: OwnershipTransferredEvent, OwnershipRenouncedEvent: OwnershipRenouncedEvent @@ -93,3 +94,140 @@ pub mod ownable_component { } } } +// ANCHOR_END: component + +// ANCHOR: contract +#[starknet::contract] +pub mod OwnedContract { + use super::{IOwnable, ownable_component, ownable_component::OwnableInternalTrait}; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + + #[abi(embed_v0)] + impl OwnableImpl = ownable_component::Ownable; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: ownable_component::Storage, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.ownable._init(starknet::get_caller_address()); + } + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + OwnableEvent: ownable_component::Event, + } +} +// ANCHOR_END: contract + +#[cfg(test)] +mod test { + use super::OwnedContract; + use super::ownable_component::{Event, OwnershipRenouncedEvent, OwnershipTransferredEvent}; + use super::{IOwnableDispatcher, IOwnableDispatcherTrait}; + use super::Errors; + use starknet::ContractAddress; + use starknet::{syscalls::deploy_syscall, SyscallResultTrait, contract_address_const}; + use starknet::testing::{set_caller_address, set_contract_address}; + use core::traits::TryInto; + use core::num::traits::Zero; + + fn deploy() -> (IOwnableDispatcher, ContractAddress) { + let (contract_address, _) = deploy_syscall( + OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap_syscall(); + + (IOwnableDispatcher { contract_address }, contract_address) + } + + #[test] + fn test_initial_state() { + let owner = contract_address_const::<'owner'>(); + set_contract_address(owner); + let (ownable, _) = deploy(); + + assert_eq!(ownable.owner(), owner); + } + + #[test] + fn test_transfer_ownership() { + let contract_address = contract_address_const::<'owner'>(); + set_contract_address(contract_address); + let (ownable, address) = deploy(); + let new_owner = contract_address_const::<'new_owner'>(); + + ownable.transfer_ownership(new_owner); + assert_eq!(ownable.owner(), new_owner); + assert_eq!( + starknet::testing::pop_log(address), + Option::Some( + OwnedContract::Event::OwnableEvent( + OwnershipTransferredEvent { previous: contract_address, new: new_owner }.into() + ) + ) + ); + } + + #[test] + #[should_panic] + fn test_transfer_ownership_not_owner() { + set_contract_address(contract_address_const::<'initial'>()); + let (ownable, _) = deploy(); + + set_contract_address(contract_address_const::<'not_owner'>()); + ownable.transfer_ownership(contract_address_const::<'new_owner'>()); + } + + #[test] + #[should_panic] + fn test_transfer_ownership_zero_error() { + set_contract_address(contract_address_const::<'initial'>()); + let (ownable, _) = deploy(); + + ownable.transfer_ownership(Zero::zero()); + } + + #[test] + fn test_renounce_ownership() { + let contract_address = contract_address_const::<'owner'>(); + set_contract_address(contract_address); + let (ownable, address) = deploy(); + + ownable.renounce_ownership(); + assert_eq!(ownable.owner(), Zero::zero()); + assert_eq!( + starknet::testing::pop_log(address), + Option::Some( + OwnedContract::Event::OwnableEvent( + OwnershipRenouncedEvent { previous: contract_address }.into() + ) + ) + ); + } + + #[test] + #[should_panic] + fn test_renounce_ownership_not_owner() { + set_contract_address(contract_address_const::<'owner'>()); + let (ownable, _) = deploy(); + + set_contract_address(contract_address_const::<'not_owner'>()); + ownable.renounce_ownership(); + } + + #[test] + #[should_panic] + fn test_renounce_ownership_previous_owner() { + set_contract_address(contract_address_const::<'owner'>()); + let (ownable, _) = deploy(); + + ownable.renounce_ownership(); + ownable.transfer_ownership(contract_address_const::<'new_owner'>()); + } +} diff --git a/listings/applications/components/src/switchable.cairo b/listings/applications/components/src/switchable.cairo index b2ce28ea..e58096df 100644 --- a/listings/applications/components/src/switchable.cairo +++ b/listings/applications/components/src/switchable.cairo @@ -14,11 +14,11 @@ pub mod switchable_component { switchable_value: bool, } - #[derive(Drop, starknet::Event)] - struct SwitchEvent {} + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub struct SwitchEvent {} #[event] - #[derive(Drop, starknet::Event)] + #[derive(Drop, Debug, PartialEq, starknet::Event)] pub enum Event { SwitchEvent: SwitchEvent, } @@ -48,4 +48,84 @@ pub mod switchable_component { } // ANCHOR_END: component +// ANCHOR: contract +#[starknet::contract] +pub mod SwitchContract { + use super::switchable_component; + + component!(path: switchable_component, storage: switch, event: SwitchableEvent); + + #[abi(embed_v0)] + impl SwitchableImpl = switchable_component::Switchable; + + #[storage] + struct Storage { + #[substorage(v0)] + switch: switchable_component::Storage, + } + + #[event] + #[derive(Drop, Debug, PartialEq, starknet::Event)] + pub enum Event { + SwitchableEvent: switchable_component::Event, + } + + // You can optionally use the internal implementation of the component as well + impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl; + + #[constructor] + fn constructor(ref self: ContractState) { + // Internal function call + self.switch._off(); + } +} +// ANCHOR_END: contract + +// ANCHOR: tests +#[cfg(test)] +mod test { + use super::SwitchContract; // Used as a mock contract + use super::switchable_component::{Event, SwitchEvent}; + use super::{ISwitchableDispatcher, ISwitchableDispatcherTrait}; + use starknet::{syscalls::deploy_syscall, contract_address_const, ContractAddress}; + use starknet::SyscallResultTrait; + + fn deploy() -> (ISwitchableDispatcher, ContractAddress) { + let (address, _) = deploy_syscall( + SwitchContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false + ) + .unwrap_syscall(); + (ISwitchableDispatcher { contract_address: address }, address) + } + + #[test] + fn test_constructor() { + let (switchable, _) = deploy(); + assert_eq!(switchable.is_on(), false); + } + + #[test] + fn test_switch() { + let (switchable, contract_address) = deploy(); + switchable.switch(); + assert_eq!(switchable.is_on(), true); + assert_eq!( + starknet::testing::pop_log(contract_address), + Option::Some(SwitchContract::Event::SwitchableEvent(SwitchEvent {}.into())) + ); + } + + #[test] + fn test_multiple_switches() { + let (switchable, _) = deploy(); + switchable.switch(); + assert_eq!(switchable.is_on(), true); + switchable.switch(); + assert_eq!(switchable.is_on(), false); + switchable.switch(); + assert_eq!(switchable.is_on(), true); + } +} +// ANCHOR_END: tests + diff --git a/listings/applications/constant_product_amm/src/tests.cairo b/listings/applications/constant_product_amm/src/tests.cairo index 412d9ce9..a94d111c 100644 --- a/listings/applications/constant_product_amm/src/tests.cairo +++ b/listings/applications/constant_product_amm/src/tests.cairo @@ -1,16 +1,13 @@ #[starknet::contract] pub mod ERC20Token { - use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); + // ERC20 Mixin #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; - #[abi(embed_v0)] - impl ERC20MetadataImpl = ERC20Component::ERC20MetadataImpl; - #[abi(embed_v0)] - impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl ERC20InternalImpl = ERC20Component::InternalImpl; #[storage] @@ -35,7 +32,7 @@ pub mod ERC20Token { symbol: ByteArray ) { self.erc20.initializer(name, symbol); - self.erc20._mint(recipient, initial_supply); + self.erc20.mint(recipient, initial_supply); } } // Wait for OZ #953 fix diff --git a/listings/applications/crowdfunding/.gitignore b/listings/applications/crowdfunding/.gitignore new file mode 100644 index 00000000..73aa31e6 --- /dev/null +++ b/listings/applications/crowdfunding/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/listings/applications/crowdfunding/Scarb.toml b/listings/applications/crowdfunding/Scarb.toml new file mode 100644 index 00000000..074a2713 --- /dev/null +++ b/listings/applications/crowdfunding/Scarb.toml @@ -0,0 +1,19 @@ +[package] +name = "crowdfunding" +version.workspace = true +edition = "2023_11" + +[lib] + +[dependencies] +starknet.workspace = true +openzeppelin.workspace = true +components.workspace = true +snforge_std.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] +casm = true +build-external-contracts = ["openzeppelin::presets::erc20::ERC20Upgradeable"] diff --git a/listings/applications/crowdfunding/src/campaign.cairo b/listings/applications/crowdfunding/src/campaign.cairo new file mode 100644 index 00000000..e5b5faf6 --- /dev/null +++ b/listings/applications/crowdfunding/src/campaign.cairo @@ -0,0 +1,352 @@ +pub mod pledgeable; + +// ANCHOR: contract +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 start_time: u64, + pub title: ByteArray, + pub token: ContractAddress, + pub total_pledges: u256, +} + +#[starknet::interface] +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, pledger: 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); + fn unpledge(ref self: TContractState, reason: ByteArray); +} + +#[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, SyscallResultTrait, get_block_timestamp, contract_address_const, + get_caller_address, get_contract_address, class_hash::class_hash_const + }; + use components::ownable::ownable_component; + use super::pledgeable::pledgeable_component; + use super::Details; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + 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 PledgeableImpl = pledgeable_component::Pledgeable; + + #[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, + title: ByteArray, + token: IERC20Dispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Claimed: Claimed, + Canceled: Canceled, + #[flat] + OwnableEvent: ownable_component::Event, + PledgeableEvent: pledgeable_component::Event, + PledgeMade: PledgeMade, + Refunded: Refunded, + RefundedAll: RefundedAll, + Unpledged: Unpledged, + Upgraded: Upgraded, + } + + #[derive(Drop, starknet::Event)] + pub struct Canceled { + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Claimed { + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct PledgeMade { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Refunded { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct RefundedAll { + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Unpledged { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Upgraded { + pub implementation: ClassHash + } + + pub mod Errors { + pub const CANCELED: felt252 = 'Campaign canceled'; + 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 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 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'; + 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); + + #[constructor] + fn constructor( + ref self: ContractState, + creator: ContractAddress, + title: ByteArray, + description: ByteArray, + goal: u256, + 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_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.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)] + impl Campaign of super::ICampaign { + fn cancel(ref self: ContractState, reason: ByteArray) { + self._assert_only_creator(); + assert(!self.canceled.read(), Errors::CANCELED); + assert(!self.claimed.read(), Errors::CLAIMED); + + self.canceled.write(true); + + self._refund_all(reason.clone()); + + 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); + assert(self._is_ended(), Errors::STILL_ACTIVE); + 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(); + let amount = token.balance_of(this); + assert(amount > 0, Errors::ZERO_PLEDGES); + + self.claimed.write(true); + + let owner = get_caller_address(); + let success = token.transfer(owner, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.emit(Event::Claimed(Claimed { amount })); + } + + fn get_details(self: @ContractState) -> Details { + Details { + canceled: self.canceled.read(), + claimed: self.claimed.read(), + creator: self.creator.read(), + description: self.description.read(), + end_time: self.end_time.read(), + goal: self.goal.read(), + start_time: self.start_time.read(), + title: self.title.read(), + token: self.token.read().contract_address, + total_pledges: self.pledges.get_total(), + } + } + + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) + } + + fn get_pledgers(self: @ContractState) -> Array { + self.pledges.array() + } + + fn pledge(ref self: ContractState, amount: u256) { + 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(); + let this = get_contract_address(); + let success = self.token.read().transfer_from(pledger, this, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.pledges.add(pledger, amount); + + self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); + } + + fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { + self._assert_only_creator(); + 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); + + let amount = self._refund(pledger); + + self.emit(Event::Refunded(Refunded { pledger, amount, reason })) + } + + fn unpledge(ref self: ContractState, reason: ByteArray) { + assert(self._is_started(), Errors::NOT_STARTED); + 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(); + 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); + + // 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); + assert( + end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX + ); + self.end_time.write(end_time); + }; + self._refund_all("contract upgraded"); + } + + starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); + + self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); + } + } + + #[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_ended(self: @ContractState) -> bool { + get_block_timestamp() >= self.end_time.read() + } + + fn _is_goal_reached(self: @ContractState) -> bool { + self.pledges.get_total() >= self.goal.read() + } + + fn _is_started(self: @ContractState) -> bool { + get_block_timestamp() >= self.start_time.read() + } + + #[inline(always)] + fn _refund(ref self: ContractState, pledger: ContractAddress) -> u256 { + let amount = self.pledges.remove(pledger); + + let success = self.token.read().transfer(pledger, amount); + assert(success, Errors::TRANSFER_FAILED); + + amount + } + + fn _refund_all(ref self: ContractState, reason: ByteArray) { + let mut pledges = self.pledges.array(); + while let Option::Some(pledger) = pledges.pop_front() { + self._refund(pledger); + }; + self.emit(Event::RefundedAll(RefundedAll { reason })); + } + } +} +// ANCHOR_END: contract + + diff --git a/listings/applications/crowdfunding/src/campaign/pledgeable.cairo b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo new file mode 100644 index 00000000..4000057a --- /dev/null +++ b/listings/applications/crowdfunding/src/campaign/pledgeable.cairo @@ -0,0 +1,556 @@ +// ANCHOR: component +use starknet::ContractAddress; + +#[starknet::interface] +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 array(self: @TContractState) -> Array; + fn get_total(self: @TContractState) -> u256; + fn remove(ref self: TContractState, pledger: ContractAddress) -> u256; +} + +#[starknet::component] +pub mod pledgeable_component { + use core::array::ArrayTrait; + use starknet::{ContractAddress}; + use core::num::traits::Zero; + + #[storage] + struct Storage { + index_to_pledger: LegacyMap, + pledger_to_amount: LegacyMap, + pledger_count: u32, + total_amount: u256, + } + + #[event] + #[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 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_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 { + self.pledger_to_amount.read(pledger) + } + + fn get_pledger_count(self: @ComponentState) -> u32 { + self.pledger_count.read() + } + + fn array(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); + result.append(pledger); + }; + + result + } + + fn get_total(self: @ComponentState) -> u256 { + self.total_amount.read() + } + + fn remove(ref self: ComponentState, pledger: ContractAddress) -> u256 { + 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; + } + // if pledger_to_amount contains a pledger, then so does index_to_pledger + // thus this will never underflow + 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); + + amount + } + } +} +// 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::{ContractAddress, contract_address_const}; + use core::num::traits::Zero; + + 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_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); + } + + #[test] + fn test_add_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 pledgers: Array::<(ContractAddress, u256)> = array![]; + + 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!(pledgeable.get_pledger_count(), expected_pledger_count); + assert_eq!(pledgeable.get_total(), expected_total); + + while let Option::Some((pledger, expected_amount)) = pledgers + .pop_front() { + 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(); + 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_pledger_count(), 2); + 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_pledger_count(), 1); + 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_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); + + // 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_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_array() { + 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 pledgers_arr = pledgeable.array(); + + 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_array_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.array(); + + 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] + 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); + } +} + diff --git a/listings/applications/crowdfunding/src/lib.cairo b/listings/applications/crowdfunding/src/lib.cairo new file mode 100644 index 00000000..3e5429ad --- /dev/null +++ b/listings/applications/crowdfunding/src/lib.cairo @@ -0,0 +1,5 @@ +pub mod campaign; +mod mock_upgrade; + +#[cfg(test)] +mod tests; diff --git a/listings/applications/crowdfunding/src/mock_upgrade.cairo b/listings/applications/crowdfunding/src/mock_upgrade.cairo new file mode 100644 index 00000000..33348d16 --- /dev/null +++ b/listings/applications/crowdfunding/src/mock_upgrade.cairo @@ -0,0 +1,289 @@ +#[starknet::contract] +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::pledgeable::pledgeable_component; + use crowdfunding::campaign::{ICampaign, Details, Campaign::Errors}; + + component!(path: ownable_component, storage: ownable, event: OwnableEvent); + 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 PledgeableImpl = pledgeable_component::Pledgeable; + + #[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, + title: ByteArray, + token: IERC20Dispatcher, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Claimed: Claimed, + Canceled: Canceled, + #[flat] + OwnableEvent: ownable_component::Event, + PledgeableEvent: pledgeable_component::Event, + PledgeMade: PledgeMade, + Refunded: Refunded, + RefundedAll: RefundedAll, + Unpledged: Unpledged, + Upgraded: Upgraded, + } + + #[derive(Drop, starknet::Event)] + pub struct Canceled { + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Claimed { + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct PledgeMade { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + } + + #[derive(Drop, starknet::Event)] + pub struct Refunded { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct RefundedAll { + pub reason: ByteArray, + } + + #[derive(Drop, starknet::Event)] + pub struct Unpledged { + #[key] + pub pledger: ContractAddress, + pub amount: u256, + 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, + creator: ContractAddress, + title: ByteArray, + description: ByteArray, + goal: u256, + 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_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.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)] + impl MockUpgrade of ICampaign { + fn cancel(ref self: ContractState, reason: ByteArray) { + self._assert_only_creator(); + assert(!self.canceled.read(), Errors::CANCELED); + assert(!self.claimed.read(), Errors::CLAIMED); + + self.canceled.write(true); + + self._refund_all(reason.clone()); + + self.emit(Event::Canceled(Canceled { reason })); + } + + fn claim(ref self: ContractState) { + 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); + + let this = get_contract_address(); + let token = self.token.read(); + let amount = token.balance_of(this); + assert(amount > 0, Errors::ZERO_PLEDGES); + + 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); + + self.emit(Event::Claimed(Claimed { amount })); + } + + 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(), + claimed: self.claimed.read(), + canceled: self.canceled.read(), + token: self.token.read().contract_address, + total_pledges: self.pledges.get_total(), + } + } + + fn get_pledge(self: @ContractState, pledger: ContractAddress) -> u256 { + self.pledges.get(pledger) + } + + fn get_pledgers(self: @ContractState) -> Array { + self.pledges.array() + } + + fn pledge(ref self: ContractState, amount: u256) { + 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(); + let this = get_contract_address(); + let success = self.token.read().transfer_from(pledger, this, amount); + assert(success, Errors::TRANSFER_FAILED); + + self.pledges.add(pledger, amount); + + self.emit(Event::PledgeMade(PledgeMade { pledger, amount })); + } + + fn refund(ref self: ContractState, pledger: ContractAddress, reason: ByteArray) { + self._assert_only_creator(); + 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); + + let amount = self._refund(pledger); + + self.emit(Event::Refunded(Refunded { pledger, amount, reason })) + } + + fn unpledge(ref self: ContractState, reason: ByteArray) { + assert(self._is_started(), Errors::NOT_STARTED); + 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(); + 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); + + // 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( + end_time <= get_block_timestamp() + NINETY_DAYS, Errors::END_BIGGER_THAN_MAX + ); + self.end_time.write(end_time); + }; + self._refund_all("contract upgraded"); + } + + starknet::syscalls::replace_class_syscall(impl_hash).unwrap_syscall(); + + self.emit(Event::Upgraded(Upgraded { implementation: impl_hash })); + } + } + + #[generate_trait] + 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_ended(self: @ContractState) -> bool { + get_block_timestamp() >= self.end_time.read() + } + + fn _is_goal_reached(self: @ContractState) -> bool { + 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); + + let success = self.token.read().transfer(pledger, amount); + assert(success, Errors::TRANSFER_FAILED); + + amount + } + + fn _refund_all(ref self: ContractState, reason: ByteArray) { + let mut pledges = self.pledges.array(); + 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 new file mode 100644 index 00000000..fde363c2 --- /dev/null +++ b/listings/applications/crowdfunding/src/tests.cairo @@ -0,0 +1,480 @@ +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, + cheat_block_timestamp_global +}; + +use crowdfunding::campaign::{Campaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use components::ownable::{IOwnableDispatcher, IOwnableDispatcherTrait}; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +/// Deploy a campaign contract with the provided data +fn deploy( + contract: ContractClass, + title: ByteArray, + description: ByteArray, + goal: u256, + start_time: u64, + end_time: u64, + token: ContractAddress +) -> ICampaignDispatcher { + let creator = contract_address_const::<'creator'>(); + let mut calldata: Array:: = array![]; + ((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'>(); + start_cheat_caller_address(contract_address, owner); + + contract.deploy(@calldata).unwrap(); + + stop_cheat_caller_address(contract_address); + + ICampaignDispatcher { contract_address } +} + +fn deploy_with_token( + contract: ContractClass, token: ContractClass +) -> (ICampaignDispatcher, IERC20Dispatcher) { + // define ERC20 data + let token_name: ByteArray = "My 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_recipient), token_owner) + .serialize(ref token_constructor_calldata); + let (token_address, _) = token.deploy(@token_constructor_calldata).unwrap(); + + // 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(pledger_1, 10000); + token_dispatcher.transfer(pledger_2, 10000); + 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, start_time, end_time, token_address + ); + + // 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, pledger_2); + token_dispatcher.approve(campaign_dispatcher.contract_address, 10000); + 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!! + // Otherwise, any call to this contract from any source will have the cheated + // address as the caller + stop_cheat_caller_address(token_address); + + (campaign_dispatcher, token_dispatcher) +} + +#[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, + 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.start_time, start_time); + assert_eq!(details.end_time, end_time); + 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'>()); + + 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_successful_campaign() { + let token_class = declare("ERC20Upgradeable").unwrap(); + let contract_class = declare("Campaign").unwrap(); + let (campaign, token) = deploy_with_token(contract_class, token_class); + + 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'>(); + + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + + // 1st donation + 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(pledger_1), 3000); + assert_eq!(token.balance_of(pledger_1), prev_balance - 3000); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::PledgeMade( + Campaign::PledgeMade { pledger: pledger_1, amount: 3000 } + ) + ) + ] + ); + + // 2nd donation + 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(pledger_2), 500); + assert_eq!(token.balance_of(pledger_2), prev_balance - 500); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::PledgeMade( + Campaign::PledgeMade { pledger: pledger_2, amount: 500 } + ) + ) + ] + ); + + // 3rd donation + 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(pledger_3), 7000); + assert_eq!(token.balance_of(pledger_3), prev_balance - 7000); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::PledgeMade( + Campaign::PledgeMade { pledger: pledger_3, amount: 7000 } + ) + ) + ] + ); + + // claim + 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(); + assert_eq!(token.balance_of(creator), prev_balance + 10500); + assert!(campaign.get_details().claimed); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Claimed(Campaign::Claimed { amount: 10500 }) + ) + ] + ); +} + +#[test] +fn test_upgrade_class_hash() { + 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("ERC20Upgradeable").unwrap(); + let (campaign, _) = deploy_with_token(contract_class, token_class); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + + start_cheat_caller_address(campaign.contract_address, owner); + 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); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ) + ] + ); + + // 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 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, pledger_1); + campaign.pledge(3000); + start_cheat_caller_address(campaign.contract_address, pledger_2); + campaign.pledge(1000); + 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_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); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Upgraded(Campaign::Upgraded { implementation: new_class_hash }) + ), + ( + campaign.contract_address, + Campaign::Event::RefundedAll( + Campaign::RefundedAll { reason: "contract upgraded" } + ) + ) + ] + ); +} + +#[test] +fn test_cancel() { + let contract_class = declare("Campaign").unwrap(); + let token_class = declare("ERC20Upgradeable").unwrap(); + + // 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'>(); + 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(pledge_1); + start_cheat_caller_address(campaign.contract_address, pledger_2); + campaign.pledge(pledge_2); + start_cheat_caller_address(campaign.contract_address, pledger_3); + 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"); + stop_cheat_caller_address(campaign.contract_address); + + 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!(campaign.get_details().canceled); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::RefundedAll(Campaign::RefundedAll { reason: "testing" }) + ), + ( + campaign.contract_address, + Campaign::Event::Canceled(Campaign::Canceled { reason: "testing" }) + ) + ] + ); + + // 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 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(pledge_1); + start_cheat_caller_address(campaign.contract_address, pledger_2); + campaign.pledge(pledge_2); + start_cheat_caller_address(campaign.contract_address, pledger_3); + 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); + + start_cheat_caller_address(campaign.contract_address, creator); + campaign.cancel("testing"); + stop_cheat_caller_address(campaign.contract_address); + + 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!(campaign.get_details().canceled); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::RefundedAll(Campaign::RefundedAll { reason: "testing" }) + ), + ( + campaign.contract_address, + Campaign::Event::Canceled(Campaign::Canceled { reason: "testing" }) + ) + ] + ); +} + +#[test] +fn test_refund() { + // setup + let (campaign, token) = deploy_with_token( + declare("Campaign").unwrap(), declare("ERC20Upgradeable").unwrap() + ); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + let creator = contract_address_const::<'creator'>(); + 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_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_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( + @array![ + ( + campaign.contract_address, + Campaign::Event::Refunded( + Campaign::Refunded { + pledger: pledger_1, amount: amount_1, reason: "testing" + } + ) + ) + ] + ); +} + +#[test] +fn test_unpledge() { + // setup + let (campaign, token) = deploy_with_token( + declare("Campaign").unwrap(), declare("ERC20Upgradeable").unwrap() + ); + let mut spy = spy_events(SpyOn::One(campaign.contract_address)); + let pledger = contract_address_const::<'pledger_1'>(); + let amount: u256 = 3000; + let prev_balance = token.balance_of(pledger); + + // 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); + + // unpledge + campaign.unpledge("testing"); + assert_eq!(campaign.get_details().total_pledges, 0); + assert_eq!(campaign.get_pledge(pledger), 0); + assert_eq!(token.balance_of(pledger), prev_balance); + + spy + .assert_emitted( + @array![ + ( + campaign.contract_address, + Campaign::Event::Unpledged( + Campaign::Unpledged { pledger, amount, reason: "testing" } + ) + ) + ] + ); +} diff --git a/listings/applications/simple_storage_starknetjs/.env.example b/listings/applications/simple_storage_starknetjs/.env.example new file mode 100644 index 00000000..32869cab --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/.env.example @@ -0,0 +1 @@ +PRIVATE_KEY = "PASTE_PRIVATE_KEY_HERE" \ No newline at end of file diff --git a/listings/applications/simple_storage_starknetjs/.gitignore b/listings/applications/simple_storage_starknetjs/.gitignore new file mode 100644 index 00000000..2eea525d --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/listings/applications/simple_storage_starknetjs/Scarb.lock b/listings/applications/simple_storage_starknetjs/Scarb.lock new file mode 100644 index 00000000..d9817fa8 --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "simple_storage" +version = "0.1.0" diff --git a/listings/applications/simple_storage_starknetjs/Scarb.toml b/listings/applications/simple_storage_starknetjs/Scarb.toml new file mode 100644 index 00000000..f22b7903 --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/Scarb.toml @@ -0,0 +1,14 @@ +[package] +name = "simple_storage" +version.workspace = true +edition = "2023_11" + +[lib] + +[dependencies] +starknet.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] \ No newline at end of file diff --git a/listings/applications/simple_storage_starknetjs/abi.json b/listings/applications/simple_storage_starknetjs/abi.json new file mode 100644 index 00000000..761ae6c2 --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/abi.json @@ -0,0 +1,616 @@ +{ + "sierra_program": [ + "0x1", + "0x5", + "0x0", + "0x2", + "0x6", + "0x3", + "0x98", + "0x68", + "0x18", + "0x52616e6765436865636b", + "0x800000000000000100000000000000000000000000000000", + "0x436f6e7374", + "0x800000000000000000000000000000000000000000000002", + "0x1", + "0x16", + "0x2", + "0x53746f726555313238202d206e6f6e2075313238", + "0x4661696c656420746f20646573657269616c697a6520706172616d202331", + "0x4f7574206f6620676173", + "0x4172726179", + "0x800000000000000300000000000000000000000000000001", + "0x536e617073686f74", + "0x800000000000000700000000000000000000000000000001", + "0x4", + "0x537472756374", + "0x800000000000000700000000000000000000000000000002", + "0x0", + "0x1baeba72e79e9db2587cf44fedb2f3700b2075a5e8e39a562584862c4b71f62", + "0x5", + "0x2ee1e2b1b89f8c495f200e4956278a4d47395fe262f27b52e5865c9524c08c3", + "0x6", + "0x9", + "0x753332", + "0x800000000000000700000000000000000000000000000000", + "0x53746f7261676541646472657373", + "0x53746f726167654261736541646472657373", + "0x4275696c74696e436f737473", + "0x53797374656d", + "0x800000000000000f00000000000000000000000000000001", + "0x16a4c8d7c05909052238a862d8cc3e7975bf05a07b3a69c6b28951083a6d672", + "0x800000000000000300000000000000000000000000000003", + "0xe", + "0x456e756d", + "0x9931c641b913035ae674b400b61a51476d506bbe8bba2ff8a6272790aba9e6", + "0x7", + "0xf", + "0x496e70757420746f6f206c6f6e6720666f7220617267756d656e7473", + "0x75313238", + "0x426f78", + "0x800000000000000700000000000000000000000000000003", + "0x29d7d57c04a880978e7b3689f6218e507f3be17588744b58dc17762447ad0e7", + "0x14", + "0x13", + "0x66656c74323532", + "0x4761734275696c74696e", + "0x35", + "0x7265766f6b655f61705f747261636b696e67", + "0x77697468647261775f676173", + "0x6272616e63685f616c69676e", + "0x7374727563745f6465636f6e737472756374", + "0x656e61626c655f61705f747261636b696e67", + "0x73746f72655f74656d70", + "0x61727261795f736e617073686f745f706f705f66726f6e74", + "0x656e756d5f696e6974", + "0x15", + "0x6a756d70", + "0x7374727563745f636f6e737472756374", + "0x656e756d5f6d61746368", + "0x756e626f78", + "0x72656e616d65", + "0x75313238735f66726f6d5f66656c74323532", + "0x64697361626c655f61705f747261636b696e67", + "0x64726f70", + "0x12", + "0x61727261795f6e6577", + "0x636f6e73745f61735f696d6d656469617465", + "0x11", + "0x61727261795f617070656e64", + "0x10", + "0x17", + "0xd", + "0x6765745f6275696c74696e5f636f737473", + "0xc", + "0x77697468647261775f6761735f616c6c", + "0x73746f726167655f626173655f616464726573735f636f6e7374", + "0x3f21dadc69f28434fd7a4035b4cee8d901bf7ef13138a70c3f754bf85806657", + "0x753132385f746f5f66656c74323532", + "0x73746f726167655f616464726573735f66726f6d5f62617365", + "0x8", + "0xa", + "0x73746f726167655f77726974655f73797363616c6c", + "0x736e617073686f745f74616b65", + "0x3", + "0x73746f726167655f726561645f73797363616c6c", + "0xeb", + "0xffffffffffffffff", + "0x74", + "0xb", + "0x63", + "0x5d", + "0x19", + "0x1a", + "0x1b", + "0x2b", + "0x1c", + "0x1d", + "0x1e", + "0x1f", + "0x20", + "0x21", + "0x22", + "0x23", + "0x24", + "0x25", + "0x4f", + "0x26", + "0x27", + "0x28", + "0x29", + "0x2a", + "0x2c", + "0x2d", + "0x46", + "0x2e", + "0x2f", + "0x30", + "0x31", + "0x32", + "0x33", + "0x34", + "0x36", + "0x37", + "0x38", + "0x39", + "0x3a", + "0x3b", + "0x3c", + "0x3d", + "0x3e", + "0x3f", + "0x40", + "0x67", + "0x41", + "0x42", + "0x43", + "0x44", + "0x45", + "0x47", + "0x48", + "0x49", + "0x4a", + "0x4b", + "0x4c", + "0xdd", + "0x97", + "0xd0", + "0xc3", + "0xb7", + "0xc8", + "0x82", + "0x876", + "0x11100f050e0d06050c0b0a0706050403090706050403080706050403020100", + "0x101f121e10021d191c191b191a191812071705040316051512111014051312", + "0x6050e2815121e10192726070605040325052405231220220f052105151220", + "0x536120505351234160505331232123112302f022e192d2c052b052a122922", + "0x5053b0507380507372c05053a123938050535140505351207380507370605", + "0x542410505402b05054014050540123f123e060505350605053d0605053c38", + "0x50535470505350507460507372505053a2105053a06050545440505430605", + "0x5351705053550050543124f124e4d07054c124b4a05053512494605053548", + "0x40540505431207460507372405053a1605053a0f0505400f05055312525105", + "0x70512125705121212560f05053505050543125516050540070505432c0505", + "0x125705120f12500557055405541212570512071224160758140f0757070512", + "0x55105241212570512071259054a5117075707500516120f0557050f051412", + "0x125705120712124405125112210557054a05171248055705170550124a0557", + "0x210557054605171248055705590550124605570525054a1225055705125912", + "0x57052c0525122c0557054405211212570512071241055a4405570721054812", + "0x1257051207125c0047545b06380757072b0f0744122b0557052b0546122b05", + "0x570512411212570512071260055f5e5d075707480516123805570538051412", + "0x471261055705120612125705060538121257055e052b121257055d052c1212", + "0x5d1264055705125c1263055705626107001262055705620546126205570512", + "0x140557051405601238055705380514126605570565055e1265055705636407", + "0x124112125705120712660714380f0566055705660562120705570507056112", + "0x67143854651267055705670564126705570512631212570560052c12125705", + "0x126e055705060567126d0557051266121257051207126c6b076a6968075707", + "0x126f0557056f056c125f0557055f056b125f0557051269126f0557056d0568", + "0x125705120712757473547271700757076e6f5f0769146d1268055705680514", + "0x7905570578055f1212570577056f12787707570576056e1276055705120612", + "0x557057005601268055705680514127a0557052f0571122f05570579057012", + "0x5c121257051207127a7170680f057a0557057a056212710557057105611270", + "0x1268055705680514127d0557057c055e127c055705757b075d127b05570512", + "0x7127d7473680f057d0557057d056212740557057405611273055705730560", + "0x57057f0546127f0557051273127e0557051206121257050605381212570512", + "0x82055e12820557058081075d1281055705125c12800557057f7e0700127f05", + "0x5621207055705070561126c0557056c0560126b0557056b05141283055705", + "0x1257055c0538121257050005381212570512071283076c6b0f058305570583", + "0x41057412125705120712128505125112840557054705141212570548052c12", + "0x86055705120612125705124112840557050f05141212570548052c12125705", + "0x89055705125c12880557058786070012870557058705461287055705127512", + "0x5705140560128405570584051412720557058a055e128a0557058889075d12", + "0x12125705120712720714840f05720557057205621207055705070561121405", + "0x700128c0557058c0546128c0557051273128b055705120612125705540576", + "0x12900557058f055e128f0557058d8e075d128e055705125c128d0557058c8b", + "0x90055705900562120705570507056112240557052405601216055705160514", + "0x570512071224160791140f0757070512070512125705121212900724160f05", + "0x7125905925117075707500516120f0557050f051412500557055405541212", + "0x557051247124a05570512061212570551052b1212570517052c1212570512", + "0x52125075d1225055705125c1221055705484a070012480557054805461248", + "0x705611214055705140560120f0557050f0514124405570546055e12460557", + "0x12570559052c121257051207124407140f0f05440557054405621207055705", + "0x12063807932b2c07570741140f546512410557054105641241055705126312", + "0x5c056b125c0557051269120005570547056812470557051266121257051207", + "0x5d545707005c072b0f77122c0557052c0514120005570500056c125c055705", + "0x61125d0557055d05601260055705600546121257051207126362615494605e", + "0x512061212570512071268676654956564075707602c0744125e0557055e05", + "0x6f126e6d0757056c056e126c0557056b690700126b05570565056712690557", + "0x1412700557055f0571125f0557056f0570126f0557056e055f121257056d05", + "0x570055705700562125e0557055e0561125d0557055d056012640557056405", + "0x557051206121257056805381212570567053812125705120712705e5d640f", + "0x57056605141274055705737107001273055705730546127305570512781271", + "0x96051251127805570574057912770557055e056112760557055d0560127505", + "0x77055705620561127605570561056012750557052c05141212570512071212", + "0x557052f055e122f0557057879075d1279055705125c127805570563057912", + "0x57057a0562127705570577056112760557057605601275055705750514127a", + "0x546127c0557051273127b0557051206121257051207127a7776750f057a05", + "0x127f0557057d7e075d127e055705125c127d0557057c7b0700127c0557057c", + "0x70557050705611206055705060560123805570538051412800557057f055e", + "0x512061212570554057612125705120712800706380f058005570580056212", + "0x5125c12830557058281070012820557058205461282055705127312810557", + "0x5601216055705160514128705570586055e12860557058384075d12840557", + "0x47120f07870724160f05870557058705621207055705070561122405570524", + "0x9754070512464847120f164847120f1254070512464847120f1648" + ], + "sierra_program_debug_info": { + "type_names": [ + [ + 0, + "RangeCheck" + ], + [ + 1, + "Const" + ], + [ + 2, + "Const" + ], + [ + 3, + "Const" + ], + [ + 4, + "Array" + ], + [ + 5, + "Snapshot>" + ], + [ + 6, + "core::array::Span::" + ], + [ + 7, + "Tuple>" + ], + [ + 8, + "Const" + ], + [ + 9, + "u32" + ], + [ + 10, + "StorageAddress" + ], + [ + 11, + "StorageBaseAddress" + ], + [ + 12, + "BuiltinCosts" + ], + [ + 13, + "System" + ], + [ + 14, + "core::panics::Panic" + ], + [ + 15, + "Tuple>" + ], + [ + 16, + "core::panics::PanicResult::<(core::array::Span::,)>" + ], + [ + 17, + "Const" + ], + [ + 18, + "u128" + ], + [ + 19, + "Unit" + ], + [ + 20, + "Box" + ], + [ + 21, + "core::option::Option::>" + ], + [ + 22, + "felt252" + ], + [ + 23, + "GasBuiltin" + ] + ], + "libfunc_names": [ + [ + 0, + "revoke_ap_tracking" + ], + [ + 1, + "withdraw_gas" + ], + [ + 2, + "branch_align" + ], + [ + 3, + "struct_deconstruct>" + ], + [ + 4, + "enable_ap_tracking" + ], + [ + 5, + "store_temp" + ], + [ + 6, + "array_snapshot_pop_front" + ], + [ + 7, + "enum_init>, 0>" + ], + [ + 8, + "store_temp>>" + ], + [ + 9, + "store_temp>>" + ], + [ + 10, + "jump" + ], + [ + 11, + "struct_construct" + ], + [ + 12, + "enum_init>, 1>" + ], + [ + 13, + "enum_match>>" + ], + [ + 14, + "unbox" + ], + [ + 15, + "rename" + ], + [ + 16, + "store_temp" + ], + [ + 17, + "u128s_from_felt252" + ], + [ + 18, + "disable_ap_tracking" + ], + [ + 19, + "drop>>" + ], + [ + 20, + "drop>" + ], + [ + 21, + "drop" + ], + [ + 22, + "array_new" + ], + [ + 23, + "const_as_immediate>" + ], + [ + 24, + "array_append" + ], + [ + 25, + "struct_construct" + ], + [ + 26, + "struct_construct>>" + ], + [ + 27, + "enum_init,)>, 1>" + ], + [ + 28, + "store_temp" + ], + [ + 29, + "store_temp" + ], + [ + 30, + "store_temp,)>>" + ], + [ + 31, + "get_builtin_costs" + ], + [ + 32, + "store_temp" + ], + [ + 33, + "withdraw_gas_all" + ], + [ + 34, + "storage_base_address_const<1784720371058305772862806735021375459770416459932804101453333227736919991895>" + ], + [ + 35, + "u128_to_felt252" + ], + [ + 36, + "storage_address_from_base" + ], + [ + 37, + "const_as_immediate>" + ], + [ + 38, + "store_temp" + ], + [ + 39, + "store_temp" + ], + [ + 40, + "storage_write_syscall" + ], + [ + 41, + "snapshot_take>" + ], + [ + 42, + "drop>" + ], + [ + 43, + "struct_construct>" + ], + [ + 44, + "struct_construct>>" + ], + [ + 45, + "enum_init,)>, 0>" + ], + [ + 46, + "const_as_immediate>" + ], + [ + 47, + "drop" + ], + [ + 48, + "const_as_immediate>" + ], + [ + 49, + "drop>" + ], + [ + 50, + "storage_read_syscall" + ], + [ + 51, + "const_as_immediate>" + ], + [ + 52, + "store_temp>" + ] + ], + "user_func_names": [ + [ + 0, + "simple_storage::storage::SimpleStorage::__wrapper__SimpleStorage__set" + ], + [ + 1, + "simple_storage::storage::SimpleStorage::__wrapper__SimpleStorage__get" + ] + ] + }, + "contract_class_version": "0.1.0", + "entry_points_by_type": { + "EXTERNAL": [ + { + "selector": "0x17c00f03de8b5bd58d2016b59d251c13056b989171c5852949903bc043bc27", + "function_idx": 1 + }, + { + "selector": "0x2f67e6aeaad1ab7487a680eb9d3363a597afa7a3de33fa9bf3ae6edcb88435d", + "function_idx": 0 + } + ], + "L1_HANDLER": [], + "CONSTRUCTOR": [] + }, + "abi": [ + { + "type": "impl", + "name": "SimpleStorage", + "interface_name": "simple_storage::storage::ISimpleStorage" + }, + { + "type": "interface", + "name": "simple_storage::storage::ISimpleStorage", + "items": [ + { + "type": "function", + "name": "set", + "inputs": [ + { + "name": "x", + "type": "core::integer::u128" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u128" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "simple_storage::storage::SimpleStorage::Event", + "kind": "enum", + "variants": [] + } + ] +} \ No newline at end of file diff --git a/listings/applications/simple_storage_starknetjs/index.js b/listings/applications/simple_storage_starknetjs/index.js new file mode 100644 index 00000000..47f45c76 --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/index.js @@ -0,0 +1,49 @@ +// ANCHOR: imports +import { Account, RpcProvider, json, Contract } from "starknet"; +import fs from "fs"; +import * as dotenv from "dotenv"; +dotenv.config(); +// ANCHOR_END: imports + +// ANCHOR: provider +const provider = new RpcProvider({ + nodeUrl: "https://free-rpc.nethermind.io/sepolia-juno", +}); +// ANCHOR_END: provider + +const accountAddress = + "0x067981c7F9f55BCbdD4e0d0a9C5BBCeA77dAcB42cccbf13554A847d6353F728e"; +// ANCHOR: account +const privateKey = process.env.PRIVATE_KEY; +// "1" is added to show that our account is deployed using Cairo 1.0. +const account = new Account(provider, accountAddress, privateKey, "1"); +// ANCHOR_END: account + +const contractAddress = + "0x01bb7d67375782ab08178b444dbda2b0c1c9ff4469c421124f54e1d8257f2e97"; +// ANCHOR: contract +const compiledContractAbi = json.parse( + fs.readFileSync("./abi.json").toString("ascii") +); +const storageContract = new Contract( + compiledContractAbi.abi, + contractAddress, + provider +); +// ANCHOR_END: contract + +// ANCHOR: get +let getData = await storageContract.get(); +console.log("Stored_data:", getData.toString()); +// ANCHOR_END: get + +// ANCHOR: set +storageContract.connect(account); +const myCall = storageContract.populate("set", [59]); +const res = await storageContract.set(myCall.calldata); +await provider.waitForTransaction(res.transaction_hash); + +// Get the stored data after setting it +getData = await storageContract.get(); +console.log("Stored_data after set():", getData.toString()); +// ANCHOR_END: set diff --git a/listings/applications/simple_storage_starknetjs/src/lib.cairo b/listings/applications/simple_storage_starknetjs/src/lib.cairo new file mode 100644 index 00000000..80aaac2b --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/src/lib.cairo @@ -0,0 +1 @@ +mod storage; diff --git a/listings/applications/simple_storage_starknetjs/src/storage.cairo b/listings/applications/simple_storage_starknetjs/src/storage.cairo new file mode 100644 index 00000000..38349241 --- /dev/null +++ b/listings/applications/simple_storage_starknetjs/src/storage.cairo @@ -0,0 +1,28 @@ +// ANCHOR: contract +#[starknet::interface] +trait ISimpleStorage { + fn set(ref self: T, x: u128); + fn get(self: @T) -> u128; +} + +#[starknet::contract] +mod SimpleStorage { + #[storage] + struct Storage { + stored_data: u128 + } + + #[abi(embed_v0)] + impl SimpleStorage of super::ISimpleStorage { + fn set(ref self: ContractState, x: u128) { + self.stored_data.write(x); + } + + fn get(self: @ContractState) -> u128 { + self.stored_data.read() + } + } +} +// ANCHOR_END: contract + + diff --git a/listings/applications/staking/src/tests/staking_tests.cairo b/listings/applications/staking/src/tests/staking_tests.cairo index d1004867..6ff1e1c8 100644 --- a/listings/applications/staking/src/tests/staking_tests.cairo +++ b/listings/applications/staking/src/tests/staking_tests.cairo @@ -41,9 +41,14 @@ mod tests { fn deploy_erc20( class_hash: felt252, name: ByteArray, symbol: ByteArray ) -> (ContractAddress, IERC20Dispatcher) { + let supply: u256 = 1000000; + let recipient = contract_address_const::<'recipient'>(); + let mut call_data: Array = ArrayTrait::new(); Serde::serialize(@name, ref call_data); Serde::serialize(@symbol, ref call_data); + Serde::serialize(@supply, ref call_data); + Serde::serialize(@recipient, ref call_data); let address = deploy_util(class_hash, call_data); (address, IERC20Dispatcher { contract_address: address }) @@ -65,10 +70,10 @@ mod tests { fn setup() -> Deployment { let (staking_token_address, staking_token) = deploy_erc20( - StakingToken::TEST_CLASS_HASH, "StakingToken", "StakingTKN" + StakingToken::TEST_CLASS_HASH, "StakingToken", "StakingTKN", ); let (reward_token_address, reward_token) = deploy_erc20( - RewardToken::TEST_CLASS_HASH, "RewardToken", "RewardTKN" + RewardToken::TEST_CLASS_HASH, "RewardToken", "RewardTKN", ); let (_, staking_contract) = deploy_staking_contract( @@ -85,7 +90,7 @@ mod tests { let mut state = StakingToken::contract_state_for_testing(); // pretend as if we were in the deployed staking token contract set_contract_address(deploy.staking_token.contract_address); - state.erc20._mint(recipient, amount); + state.erc20.mint(recipient, amount); // approve staking contract to spend user's tokens set_contract_address(recipient); @@ -99,7 +104,7 @@ mod tests { let mut state = RewardToken::contract_state_for_testing(); // pretend as if we were in the deployed reward token contract set_contract_address(reward_token_address); - state.erc20._mint(deployed_contract, amount); + state.erc20.mint(deployed_contract, amount); } #[test] diff --git a/listings/applications/staking/src/tests/tokens.cairo b/listings/applications/staking/src/tests/tokens.cairo index c6ce3a19..ea788565 100644 --- a/listings/applications/staking/src/tests/tokens.cairo +++ b/listings/applications/staking/src/tests/tokens.cairo @@ -1,12 +1,12 @@ #[starknet::contract] pub mod RewardToken { - use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl ERC20InternalImpl = ERC20Component::InternalImpl; #[storage] @@ -23,20 +23,27 @@ pub mod RewardToken { } #[constructor] - fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray) { + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); } } #[starknet::contract] pub mod StakingToken { - use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; use starknet::ContractAddress; component!(path: ERC20Component, storage: erc20, event: ERC20Event); #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; impl ERC20InternalImpl = ERC20Component::InternalImpl; #[storage] @@ -53,7 +60,14 @@ pub mod StakingToken { } #[constructor] - fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray) { + fn constructor( + ref self: ContractState, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress + ) { self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); } } diff --git a/listings/applications/timelock/src/erc721.cairo b/listings/applications/timelock/src/erc721.cairo index be7a023c..fddeda94 100644 --- a/listings/applications/timelock/src/erc721.cairo +++ b/listings/applications/timelock/src/erc721.cairo @@ -2,10 +2,10 @@ pub mod ERC721 { use starknet::ContractAddress; use openzeppelin::introspection::src5::SRC5Component; - use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::token::erc721::{ERC721Component, ERC721HooksEmptyImpl}; - component!(path: SRC5Component, storage: src5, event: SRC5Event); component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); // ERC20Mixin #[abi(embed_v0)] @@ -39,6 +39,6 @@ pub mod ERC721 { token_id: u256 ) { self.erc721.initializer(name, symbol, base_uri); - self.erc721._mint(recipient, token_id); + self.erc721.mint(recipient, token_id); } } diff --git a/po/es.po b/po/es.po index baa154ae..51cdcf9b 100644 --- a/po/es.po +++ b/po/es.po @@ -87,11 +87,11 @@ msgstr "Llamar a otros contratos" #: src/SUMMARY.md:26 msgid "Factory pattern" -msgstr "Patrón de la Factory" +msgstr "Patrón Factory" #: src/SUMMARY.md:27 msgid "Testing contracts" -msgstr "Contratos de Testing" +msgstr "Testing de Contratos" #: src/SUMMARY.md:28 msgid "Cairo cheatsheet" @@ -220,7 +220,7 @@ msgstr "" #: src/starknet-by-example.md:5 msgid "" "Starknet is a permissionless Validity-Rollup that supports general " -"computation. It is currently used as an Ethereum layer-2. Starknet use the " +"computation. It is currently used as an Ethereum layer-2. Starknet uses the " "STARK cryptographic proof system to ensure high safety and scalability." msgstr "" "Starknet es un Validity-Rollup sin permiso que admite el cálculo general. " @@ -234,26 +234,12 @@ msgid "" "Turing-complete programming language designed to write provable programs, " "abstracting the zk-STARK proof system away from the programmer." msgstr "" -"Los smart contracts de Starknet están escritos en el idioma de Cairo. Cairo " -"es un lenguaje de programación completo de Turing diseñado para escribir " +"Los smart contracts de Starknet están escritos en el lenguaje Cairo. Cairo " +"es un lenguaje de programación Turing completo diseñado para escribir " "programas demostrables, abstrayendo el sistema de prueba zk-STARK del " "programador." #: src/starknet-by-example.md:9 -msgid "The current version of this book use:" -msgstr "La versión actual de este libro usa:" - -#: src/starknet-by-example.md:10 -msgid "" -"```\n" -"scarb 2.3.1\n" -"```" -msgstr "" -"```\n" -"scarb 2.3.1\n" -"```" - -#: src/starknet-by-example.md:14 msgid "" "> ⚠️ The examples have not been audited and are not intended for production " "use.\n" @@ -265,11 +251,11 @@ msgstr "" "> Los autores no se hacen responsables de los daños causados por el uso del " "código proporcionado en este libro." -#: src/starknet-by-example.md:17 +#: src/starknet-by-example.md:12 msgid "## For whom is this for?" -msgstr "## ¿Para quién es esto?" +msgstr "## ¿A quién va dirigido?" -#: src/starknet-by-example.md:19 +#: src/starknet-by-example.md:14 msgid "" "Starknet By Example is for anyone who wants to quickly learn how to write " "smart contracts on Starknet using Cairo with some technical background in " @@ -279,7 +265,7 @@ msgstr "" "escribir contratos inteligentes en Starknet usando Cairo con cierta " "experiencia técnica en programación y blockchain." -#: src/starknet-by-example.md:21 +#: src/starknet-by-example.md:16 msgid "" "The first chapters will give you a basic understanding of the Cairo " "programming language and how to write, deploy and use smart contracts on " @@ -292,11 +278,41 @@ msgstr "" "inteligentes en Starknet. Los capítulos posteriores cubrirán temas más " "avanzados y le mostrarán cómo escribir contratos inteligentes más complejos." -#: src/starknet-by-example.md:24 +#: src/starknet-by-example.md:19 +msgid "## How to use this book?" +msgstr "## ¿Como usar este libro?" + +#: src/starknet-by-example.md:21 +msgid "" +"Each chapter is a standalone example that demonstrates a specific feature " +"or common use case of smart contracts on Starknet. If you are new to Starknet, " +"it is recommended to read the chapters in order." +msgstr "" +"Cada capítulo en un ejemplo independiente que muestra una característica " +"específica o un caso de uso común de un contrato inteligente en Starknet. " +"Si eres nuevo en Starknet, es recomendable que leas los capítulos en orden." + +#: src/starknet-by-example.md:23 +msgid "" +"Most examples contain interfaces and tests that are hidden by default. " +"You can hover over the code blocks and click on the \"Show hidden lines\" " +"(eyes icon) to see the hidden code.\n" +msgstr "" +"La mayoria de los ejemplos contienen interfaces y pruebas que están ocultas por defecto " +"Puedes colocar el cursor sobre los bloques de codígo y darle click en \"Mostrar lineas ocultas\" " +"(icono de ojos) para ver el codígo oculto" + +#: src/starknet-by-example.md:25 +msgid "" +"You can run each example online by using the [Starknet Remix Plugin](https://remix.ethereum.org/?#activate=Starknet)." +msgstr "" +"Puedes ejecutar cada uno de los ejemplos en línea usando [El plugin de Starknet en Remix ](https://remix.ethereum.org/?#activate=Starknet)." + +#: src/starknet-by-example.md:27 msgid "## Further reading" msgstr "## Otras lecturas" -#: src/starknet-by-example.md:26 +#: src/starknet-by-example.md:29 msgid "" "If you want to learn more about the Cairo programming language, you can read " "the [Cairo Book](https://book.cairo-lang.org).\n" @@ -309,7 +325,7 @@ msgstr "" "más información sobre Starknet, puede leer la [Documentación de Starknet]" "(https://docs.starknet.io/) y el [Starknet Book](https://book.starknet.io)." -#: src/starknet-by-example.md:29 +#: src/starknet-by-example.md:32 msgid "" "For more resources, check [Awesome Starknet](https://github.com/keep-" "starknet-strange/awesome-starknet)." @@ -317,6 +333,24 @@ msgstr "" "Para obtener más recursos, consulte [Awesome Starknet](https://github.com/" "keep-starknet-strange/awesome-starknet)." +#: src/starknet-by-example.md:34 +msgid "## Versions" +msgstr "## Versiones" + +#: src/starknet-by-example.md:36 +msgid "The current version of this book uses:" +msgstr "La versión actual de este libro usa:" + +#: src/starknet-by-example.md:38 +msgid "" +"```\n" +"scarb 2.6.3\n" +"```" +msgstr "" +"```\n" +"edition = '2023_11'\n" +"```" + #: src/starknet-by-example.md:31 src/getting-started/basics/storage.md:34 #: src/getting-started/basics/constructor.md:27 src/getting-started/basics/variables.md:126 #: src/getting-started/basics/visibility-mutability.md:75 src/getting-started/basics/counter.md:56 diff --git a/scripts/cairo_programs_verifier.sh b/scripts/cairo_programs_verifier.sh index b55cdd5b..394e08e9 100755 --- a/scripts/cairo_programs_verifier.sh +++ b/scripts/cairo_programs_verifier.sh @@ -97,7 +97,7 @@ else if [ -z "$modified_listings" ]; then echo -e "\n${GREEN}No new changes detected${NC}" else - echo -e "\n${GREEN}All $listings_count builds were completed successfully${NC}" + echo -e "\n${GREEN}All builds were completed successfully${NC}" fi rm "$error_file" exit 0 diff --git a/src/SUMMARY.md b/src/SUMMARY.md index ec59eda8..fcdb5c87 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -23,6 +23,7 @@ Summary - [Custom types in entrypoints](./getting-started/basics/custom-types-in-entrypoints.md) - [Documentation](./getting-started/basics/documentation.md) - [Deploy and interact with contracts](./getting-started/interacting/interacting.md) + - [How to deploy](./getting-started/interacting/how_to_deploy.md) - [Contract interfaces and Traits generation](./getting-started/interacting/interfaces-traits.md) - [Calling other contracts](./getting-started/interacting/calling_other_contracts.md) - [Factory pattern](./getting-started/interacting/factory.md) @@ -59,6 +60,9 @@ Summary - [Constant Product AMM](./applications/constant-product-amm.md) - [TimeLock](./applications/timelock.md) - [Staking](./applications/staking.md) +- [Simple Storage with Starknet-js](./applications/simple_storage_starknetjs.md) +- [Crowdfunding Campaign](./applications/crowdfunding.md) +- [AdvancedFactory: Crowdfunding](./applications/advanced_factory.md) @@ -69,10 +73,12 @@ Summary - [Struct as mapping key](./advanced-concepts/struct-mapping-key.md) - [Hashing](./advanced-concepts/hashing.md) - + - [Optimisations](./advanced-concepts/optimisations/optimisations.md) - [Storage Optimisations](./advanced-concepts/optimisations/store_using_packing.md) +- [Account Abstraction](./advanced-concepts/account_abstraction/index.md) + - [Account Contract](./advanced-concepts/account_abstraction/account_contract.md) - [List](./advanced-concepts/list.md) +- [Library Calls](./advanced-concepts/library_calls.md) - [Plugins](./advanced-concepts/plugins.md) - [Signature Verification](./advanced-concepts/signature_verification.md) -- [Library Calls](./advanced-concepts/library_calls.md) diff --git a/src/advanced-concepts/account_abstraction/account_contract.md b/src/advanced-concepts/account_abstraction/account_contract.md new file mode 100644 index 00000000..1cd8dde9 --- /dev/null +++ b/src/advanced-concepts/account_abstraction/account_contract.md @@ -0,0 +1,73 @@ +# Account Contract + +A smart contract must follow the Standard Account Interface specification defined in the [SNIP-6](https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md). +In practice, this means that the contract must implement the `SRC6` and `SRC5` interfaces to be considered an account contract. + +## SNIP-6: SRC6 + SRC5 + +```rust +/// @title Represents a call to a target contract +/// @param to The target contract address +/// @param selector The target function selector +/// @param calldata The serialized function parameters +struct Call { + to: ContractAddress, + selector: felt252, + calldata: Array +} +``` + +The `Call` struct is used to represent a call to a function (`selector`) in a target contract (`to`) with parameters (`calldata`). It is available under the `starknet::account` module. + +```rust +/// @title SRC-6 Standard Account +trait ISRC6 { + /// @notice Execute a transaction through the account + /// @param calls The list of calls to execute + /// @return The list of each call's serialized return value + fn __execute__(calls: Array) -> Array>; + + /// @notice Assert whether the transaction is valid to be executed + /// @param calls The list of calls to execute + /// @return The string 'VALID' represented as felt when is valid + fn __validate__(calls: Array) -> felt252; + + /// @notice Assert whether a given signature for a given hash is valid + /// @param hash The hash of the data + /// @param signature The signature to validate + /// @return The string 'VALID' represented as felt when the signature is valid + fn is_valid_signature(hash: felt252, signature: Array) -> felt252; +} +``` + +A transaction can be represented as a list of calls `Array` to other contracts, with atleast one call. + +- `__execute__`: Executes a transaction after the validation phase. Returns an array of the serialized return of value (`Span`) of each call. + +- `__validate__`: Validates a transaction by verifying some predefined rules, such as the signature of the transaction. Returns the `VALID` short string (as a felt252) if the transaction is valid. + +- `is_valid_signature`: Verify that a given signature is valid. This is mainly used by applications for authentication purposes. + +Both `__execute__` and `__validate__` functions are exclusively called by the Starknet protocol. + + + +```rust +/// @title SRC-5 Standard Interface Detection +trait ISRC5 { + /// @notice Query if a contract implements an interface + /// @param interface_id The interface identifier, as specified in SRC-5 + /// @return `true` if the contract implements `interface_id`, `false` otherwise + fn supports_interface(interface_id: felt252) -> bool; +} +``` + +The interface identifiers of both `SRC5` and `SRC6` must be published with `supports_interface`. + +## Minimal account contract Executing Transactions + +In this example, we will implement a minimal account contract that can validate and execute transactions. + +```rust +{{#rustdoc_include ../../../listings/advanced-concepts/simple_account/src/simple_account.cairo}} +``` \ No newline at end of file diff --git a/src/advanced-concepts/account_abstraction/index.md b/src/advanced-concepts/account_abstraction/index.md new file mode 100644 index 00000000..f345ab99 --- /dev/null +++ b/src/advanced-concepts/account_abstraction/index.md @@ -0,0 +1,13 @@ +# Account Abstraction + +An account is an unique entity that can send transactions, users usually use wallets to manage their accounts. + +Historically, in Ethereum, all accounts were Externally Owned Accounts (_EOA_) and were controlled by private keys. This is a simple and secure way to manage accounts, but it has limitations as the account logic is hardcoded in the protocol. + +Account Abstraction (_AA_) is the concept behind abstracting parts of the account logic to allow for a more flexible account system. +This replaces EOA with Account Contracts, which are smart contracts that implement the account logic. This opens up a lot of possibilities that can significantly improve the user experience when dealing with accounts. + +On Starknet, Account Abstraction is natively supported, and all accounts are Account Contracts. + +In this section we will how to implement an Account. + diff --git a/src/applications/advanced_factory.md b/src/applications/advanced_factory.md new file mode 100644 index 00000000..5d2a27fd --- /dev/null +++ b/src/applications/advanced_factory.md @@ -0,0 +1,13 @@ +# AdvancedFactory: Crowdfunding + +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}} +``` diff --git a/src/applications/crowdfunding.md b/src/applications/crowdfunding.md new file mode 100644 index 00000000..8d78f05a --- /dev/null +++ b/src/applications/crowdfunding.md @@ -0,0 +1,26 @@ +# Crowdfunding Campaign + +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; 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, 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 (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. + +```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}} +``` diff --git a/src/applications/simple_storage_starknetjs.md b/src/applications/simple_storage_starknetjs.md new file mode 100644 index 00000000..db574e41 --- /dev/null +++ b/src/applications/simple_storage_starknetjs.md @@ -0,0 +1,100 @@ +# Simple Storage (Starknet-js + Cairo) + +In this example, we will use a SimpleStorage Cairo contract deployed on Starknet Sepolia Testnet and show how you can interact with the contract using Starknet-js. + +## Writing SimpleStorage contract in Cairo + +The SimpleStorage contract has only one purpose: storing a number. We want the users to interact with the stored number by **writing** to the currently stored number and **reading** the number in the contract. + +We will use the following SimpleStorage contract. In the [Storage Variables](../getting-started/basics/variables.md) page, you can find explanations for each component of the contract: + +```rs +{{#rustdoc_include ../../listings/applications/simple_storage_starknetjs/src/storage.cairo:contract}} +``` + +Because we want to interact with the get and set functions of the SimpleStorage contract using Starknet-js, we define the function signatures in `#[starknet::interface]`. The functions are defined under the macro `#[abi(embed_v0)]` where external functions are written. + +Only deployed instances of the contract can be interacted with. You can refer to the [How to Deploy](../getting-started/interacting/how_to_deploy.md) page. Note down the address of your contract, as it is needed for the following part. + +## Interacting with SimpleStorage contract + +We will interact with the SimpleStorage contract using Starknet-js. Firstly, create a new folder and inside the directory of the new folder, initialize the npm package (click Enter several items, you can skip adding the package info): + +```console +$ npm init +``` + +Now, `package.json` file is created. Change the type of the package to a module. + +```json +"type": "module" +``` + +Let's add Starknet-js as a dependency: + +```console +$ npm install starknet@next +``` + +Create a file named `index.js` where we will write JavaScript code to interact with our contract. Let's start our code by importing from Starknet-js, and from other libraries we will need: + +```js +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:imports}} +``` + +Let's create our provider object, and add our account address as a constant variable. We need the provider in order to send our queries and transactions to a Starknet node that is connected to the Starknet network: + +```js +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:provider}} +const accountAddress = // 'PASTE_ACCOUNT_ADDRESS_HERE'; +``` + +The next step is creating an `Account` object that will be used to sign transactions, so we need to import the account private key. You can access it directly from your keystore with the following command using Starkli: + +```console +$ starkli signer keystore inspect-private /path/to/starkli-wallet/keystore.json --raw +``` + +Create a `.env` file in your project folder, and paste your private key as shown in the following line: +```bash +{{#include ../../listings/applications/simple_storage_starknetjs/.env.example}} +``` + +> Warning: Using `.env` files is not recommended for production environments, please use `.env` files only for development purposes! It is HIGHLY recommended to add `.gitignore`, and include your .env file there if you will be pushing your project to GitHub. + +Now, import your private key from the environment variables and create your Account object. +```js +const accountAddress = // 'PASTE_ACCOUNT_PUBLIC_ADDRESS_HERE'; +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:account}} +``` + +Now, let's create a Contract object in order to interact with our contract. In order to create the Contract object, we need the ABI and the address of our contract. The ABI contains information about what kind of data structures and functions there are in our contract so that we can interact with them using SDKs like Starknet-js. + +We will copy `./target/simple_storage_SimpleStorage.contract_class.json` to `abi.json` in the Scarb project folder. The beginning of the content of the ABI file should look like this: + +```json +{"sierra_program":["0x1","0x5","0x0","0x2","0x6","0x3","0x98","0x68","0x18", //... +``` + +We can then create the Account object and the Contract object in our `index.js` file: + +```js +const contractAddress = 'PASTE_CONTRACT_ADDRESS_HERE'; +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:contract}} +``` + +The setup is finished! By calling the `fn get(self: @ContractState) -> u128` function, we will be able to read the `stored_data` variable from the contract: + +```js +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:get}} +``` + +In order to run your code, run the command `node index.js` in your project directory. After a short amount of time, you should see a "0" as the stored data. + +Now, we will set a new number to the `stored_data` variable by calling the `fn set(self: @mut ContractState, new_data: u128)` function. This is an `INVOKE` transaction, so we need to sign the transaction with our account's private key and pass along the calldata. + +The transaction is signed and broadcasted to the network and it can takes a few seconds for the transaction to be confirmed. + +```js +{{#include ../../listings/applications/simple_storage_starknetjs/index.js:set}} +``` diff --git a/src/components/collisions.md b/src/components/collisions.md index 5f8266be..98bf9448 100644 --- a/src/components/collisions.md +++ b/src/components/collisions.md @@ -16,17 +16,17 @@ Here's an example of a collision on the `switchable_value` storage variable of t Interface: ```rust -{{#include ../../listings/applications/components/src/contracts/switch_collision.cairo:interface}} +{{#include ../../listings/applications/components/src/others/switch_collision.cairo:interface}} ``` -Here's the storage of the contract (you can expand the code snippet to see the full contract): +Here's the storage of the contract (you can expand the code snippet to see the full contract and tests): ```rust -{{#rustdoc_include ../../listings/applications/components/src/contracts/switch_collision.cairo:storage}} +{{#rustdoc_include ../../listings/applications/components/src/others/switch_collision.cairo:storage}} ``` Both the contract and the component have a `switchable_value` storage variable, so they collide: ```rust -{{#rustdoc_include ../../listings/applications/components/src/contracts/tests/switch_collision_tests.cairo:collision}} +{{#include ../../listings/applications/components/src/others/switch_collision.cairo:collision}} ``` diff --git a/src/components/dependencies.md b/src/components/dependencies.md index 08f20ec2..31431aca 100644 --- a/src/components/dependencies.md +++ b/src/components/dependencies.md @@ -5,7 +5,7 @@ A component with a dependency on a trait `T` can be used in a contract as long a We will use a new `Countable` component as an example: ```rust -{{#include ../../listings/applications/components/src/countable.cairo}} +{{#rustdoc_include ../../listings/applications/components/src/countable.cairo:component}} ``` We want to add a way to enable or disable the counter, in a way that calling `increment` on a disabled counter will not increment it. diff --git a/src/components/how_to.md b/src/components/how_to.md index 30f8cbea..819954f1 100644 --- a/src/components/how_to.md +++ b/src/components/how_to.md @@ -42,7 +42,18 @@ Now that we have a component, we can use it in a contract. The following contract incorporates the `Switchable` component: ```rust -{{#rustdoc_include ../../listings/applications/components/src/contracts/switch.cairo:contract}} +{{#include ../../listings/applications/components/src/switchable.cairo:contract}} +``` + +## How to test a component + +In order to effectively test a component, you need to test it in the context of a contract. +A common practice is to declare a `Mock` contract that has the only purpose of testing the component. + +To test the `Switchable` component, we can use the previous `SwitchableContract`: + +```rust +{{#include ../../listings/applications/components/src/switchable.cairo:tests}} ``` ## Deep dive into components diff --git a/src/components/ownable.md b/src/components/ownable.md index 003ccb9a..20d8f313 100644 --- a/src/components/ownable.md +++ b/src/components/ownable.md @@ -5,11 +5,11 @@ The following `Ownable` component is a simple component that allows the contract It can also be used to renounce ownership of a contract, meaning that no one will be able to satisfy the `_assert_is_owner` function. ```rust -{{#include ../../listings/applications/components/src/ownable.cairo}} +{{#include ../../listings/applications/components/src/ownable.cairo:component}} ``` A mock contract that uses the `Ownable` component: ```rust -{{#rustdoc_include ../../listings/applications/components/src/contracts/owned.cairo:contract}} +{{#rustdoc_include ../../listings/applications/components/src/ownable.cairo:contract}} ``` diff --git a/src/getting-started/interacting/how_to_deploy.md b/src/getting-started/interacting/how_to_deploy.md new file mode 100644 index 00000000..232d97a0 --- /dev/null +++ b/src/getting-started/interacting/how_to_deploy.md @@ -0,0 +1,79 @@ +## Declaring and Deploying Your Contract + +We will use Starkli to declare and deploy a smart contract on Starknet. Make sure that [Starkli](https://github.com/xJonathanLEI/starkli) is installed on your device. You can check out the [starkli book](https://book.starkli.rs/) for more information. + +We will need an account, so first we will create one. If you already have one, you can skip this step and move directly to the part where we declare our contract. + +### Creating a new account: + +You should move to the directory where you want to access your account keystores, and then create a new folder for the wallet. + +```console +$ mkdir ./starkli-wallet +``` + +Create a new signer. You will be instructed to enter a password to encrypt your private key: + +```console +$ starkli signer keystore new ./starkli-wallet/keystore.json +``` + +After this command, the path of the encrypted keystore file is shown which will be needed during the declaration and deployment of the contract. + +Export the keystore path in order not to call `--keystore` in every command: + +```console +$ export STARKNET_KEYSTORE="./starkli-wallet/keystore.json" +``` + +Initialize the account with the following command using OpenZeppelin's class deployed on Starknet. + +```console +$ starkli account oz init ./starkli-wallet/account.json +``` + +After this command, the address of the account is shown once it is deployed along with the deploy command. Deploy the account: + +```console +$ starkli account deploy ./starkli-wallet/account.json +``` + +This command wants you to fund the address (given in the instructions below the command) in order to deploy the account on the Starknet Sepolia Testnet. We need Starknet Sepolia testnet ethers which could be obtained from [this faucet](https://starknet-faucet.vercel.app/). + +Once the transaction is confirmed on the faucet page, press ENTER, and the account will be deployed on Starknet Sepolia! Try to find your account on [Voyager Sepolia](https://sepolia.voyager.online/)! + +### Declaring & Deploying your Contract: + +Firstly, you need to declare your contract which will create a class on Starknet Sepolia. Note that we will use the Sierra program in `./target/ProjectName_ContractName.contract_class.json` in your Scarb folder. + +> If you are deploying a contract code that is already used, you can skip the declaration step because the class hash is already declared on the network. One example of this is when you are deploying common contract instances such as ERC20 or ERC721 contracts. + +**Note:** The command below is written to run in the directory of the Scarb folder. + +```console +$ starkli declare \ + --keystore /path/to/starkli-wallet/keystore.json \ + --account /path/to/starkli-wallet/account.json \ + --watch ./target/dev/simple_storage_SimpleStorage.contract_class.json +``` + +After this command, the class hash for your contract is declared. You should be able to find the hash under the command: + +```console +Class hash declared: +0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4 +``` + +Check the [Voyager Class Page](https://sepolia.voyager.online/class/0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4). +Now, it's time to deploy the contract. Add the clash hash given above after `--watch`: + +```console +$ starkli deploy \ + --keystore /path/to/starkli-wallet/keystore.json \ + --account /path/to/starkli-wallet/account.json \ + --watch 0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4 +``` + +You should now see the address of the deployed contract. Congratulations, you have deployed your contract on Starknet Sepolia Testnet! +Check the [Voyager Contract Page](https://sepolia.voyager.online/contract/0x01bb7d67375782ab08178b444dbda2b0c1c9ff4469c421124f54e1d8257f2e97) to see your contract! +Additionally, you can also find all contract instances of a given class on the Voyager Class Page as well, for example, [this page](https://sepolia.voyager.online/class/0x05c8c21062a74e3c8f2015311d7431e820a08a6b0a9571422b607429112d2eb4#contracts4).