AdvancedFactory: Crowdfunding

This is an example of an advanced factory contract that manages crowdfunding Campaign contracts created in the "Crowdfunding" chapter. 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.
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);
        }
    }
}
Last change: 2024-06-27, commit: 52fbc5c