Skip to content

Commit

Permalink
feat: Advanced factory contract (#219)
Browse files Browse the repository at this point in the history
* add initial factory

* add ownable component

* add caller to CounterCreated event

* turn counter into campaign

* fix Campaign interfaced funcs + implement donate

* add _assert_is_ended + update error messages

* _assert_active->_assert_campaign_active

* _assert_is_ended->_assert_campaign_ended

* implement withdraw

* add missing assert success in donate

* add title & description

* update comment

* implement upgrade

* clean up internal funcs and imports

* move hardcoded errors in Errors mod

* donate -> contribute + event rename

* withdraw -> claim

* add store impl for contract addr. array

* remove store impl

* add dynamic array impl

* remove dyn. array

* remove descr + convert title to felt + convert target to u128

* implement updating class hashes

* Make title ByteArray again + target into u256 + update ctor arg serialization

* refactor serialization + add back description

* remove unused contracts

* add 1 test

* add get_description

* add correct deps

* add alexandria to toml

* format factory.cairo

* add missing snforge workspace

* add missing getters + tests

* add factory deploy tests

* add class hash update test + event assertions

* assert old class hash prior to update

* remove commented out test

* use common alex. storage workspace in using_lists

* add missing newline in toml

* move factory tests to separate file

* add scaffold docs for contracts

* add end_time asserts

* refactor private asserts

* check if target reached before claiming

* add ability to withdraw funds

* make contributions into a component (now iterable)

* refactor 'withhold' - contrs map to amt_idx

* add get_contributors func

* get_contributors -> get_contributions

* total_contributors->contributor_count

* add tests for campaign upgrade and deploy + update all relevant code in factory

* add status to campaign

* add close fn

* pass desired donation token in ctor

* merge all getters into get_details

* return total_contributions in details

* remove rev version from alexandria dep

* verbose names

* reorg. folder structure

* add tag to alexandria dep

* campaign_upgrade.cairo->mock_upgrade.cairo

* add explicit alexandria rev + make crowdfunding contracts standalone chapters

* add status pending

* field rename: factory->creator

* refund users when upgrading campaign

* Make owner the calling address, and creator is the campaign manager

* add get_contributor (amount) func

* Add successful campaign test

* update comment for upgrade

* _refund_all->_withdraw_all

* update checks for withdraw

* rework contribute

* rework all funcs

* unsuccessful -> failed

* calc end_time in start fn

* calc end_time in upgrade fn

* makes upgrades callable only by creators in factory

* fix factory tests

* fix crowdfunding tests

* reduce total contri. when withdraw from act. camp

* add refund fn

* refactor withdraw_all to use _refund

* pending->draft

* fix mock and tests

* add test for close

* add test for withdraw

* upgrade > update end_time only if duration provided

* close->cancel

* rename to more align with Solidity by example

* target->goal

* remove comment

* err CLOSED->CANCELED + check active in unpledge

* contributor->pledger

* add campaign doc content

* remove draft status

* add start_time

* remove Status

* update doc for campaign

* move total_pledges to pledgeable

* reorder alphabetically

* remove Launched event + upgrade mock

* TARGET->GOAL

* reorder params in Details

* add inline to _refund

* add new pledgeable tests

* add getX tests + add get_pledge_count

* refactor pledger_to_amount_index->pledger_to_amount

* Add tests with 1000 pledgers

* add test for add + update existing pledger

* reenable lib

* Add link to adv. factory in crowdfunding point 9

* write the adv. factory chapter

* upgrade_campaign_implementation-> upgrade_campaign + comment updates

* rename get_pledgers_as_arr->array

* Use ERC20Upgradeable instead of ERC20 preset

* Add missing token recipient ctor argument in crowdfunding tests

---------

Co-authored-by: Nenad <nenad@better.giving>
  • Loading branch information
Nenad Misić and Nenad authored Jun 27, 2024
1 parent 1798154 commit 52fbc5c
Show file tree
Hide file tree
Showing 19 changed files with 2,145 additions and 1 deletion.
19 changes: 19 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ starknet = ">=2.6.4"
openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag="v0.14.0" }
components = { path = "listings/applications/components" }
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry.git", tag = "v0.25.0" }
# The latest Alexandria release supports only Cairo v2.6.0, so using explicit rev that supports Cairo v2.6.3
alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev="800f5ad" }

[workspace.package]
description = "Collection of examples of how to use the Cairo programming language to create smart contracts on Starknet."
Expand Down
2 changes: 1 addition & 1 deletion listings/advanced-concepts/using_lists/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions listings/applications/advanced_factory/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
.snfoundry_cache/
18 changes: 18 additions & 0 deletions listings/applications/advanced_factory/Scarb.toml
Original file line number Diff line number Diff line change
@@ -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"]
152 changes: 152 additions & 0 deletions listings/applications/advanced_factory/src/contract.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// ANCHOR: contract
pub use starknet::{ContractAddress, ClassHash};

#[starknet::interface]
pub trait ICampaignFactory<TContractState> {
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<u64>
);
}

#[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<ContractState>;
impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;

#[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<ContractState> {
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::<felt252> = 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<u64>
) {
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


5 changes: 5 additions & 0 deletions listings/applications/advanced_factory/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod contract;
mod mock_upgrade;

#[cfg(test)]
mod tests;
8 changes: 8 additions & 0 deletions listings/applications/advanced_factory/src/mock_upgrade.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[starknet::contract]
pub mod MockContract {
#[storage]
struct Storage {}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {}
}
Loading

0 comments on commit 52fbc5c

Please sign in to comment.