diff --git a/.snfoundry_cache/.prev_tests_failed b/.snfoundry_cache/.prev_tests_failed index e69de29..619f58a 100644 --- a/.snfoundry_cache/.prev_tests_failed +++ b/.snfoundry_cache/.prev_tests_failed @@ -0,0 +1 @@ +tokengiver_integrationtest::test_campaign::test_donate diff --git a/src/base/errors.cairo b/src/base/errors.cairo index 0d1bcf7..ecee6fa 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -7,4 +7,5 @@ pub mod Errors { pub const INITIALIZED: felt252 = 'TGN: already initialized!'; pub const INVALID_OWNER: felt252 = 'TGN: caller is not owner!'; pub const INVALID_CAMPAIGN: felt252 = 'TGN: campaign is not owner!'; + pub const INSUFFICIENT_BALANCE: felt252 = 'TGN: insufficient balance!'; } diff --git a/src/campaign.cairo b/src/campaign.cairo index 68dc65c..83ce1ef 100644 --- a/src/campaign.cairo +++ b/src/campaign.cairo @@ -6,11 +6,10 @@ mod TokengiverCampaign { // IMPORT // ************************************************************************* use core::traits::TryInto; - // use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; use starknet::{ - ContractAddress, get_caller_address, get_block_timestamp, SyscallResultTrait, - storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess}, - syscalls::{library_call_syscall} + ContractAddress, get_caller_address, get_block_timestamp, ClassHash, + syscalls::deploy_syscall, SyscallResultTrait, + storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess} }; use tokengiver::interfaces::ITokenGiverNft::{ ITokenGiverNftDispatcher, ITokenGiverNftDispatcherTrait @@ -21,7 +20,7 @@ mod TokengiverCampaign { use tokengiver::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait}; use tokengiver::interfaces::ICampaign::ICampaign; use tokengiver::base::types::Campaign; - use tokengiver::base::errors::Errors::NOT_CAMPAIGN_OWNER; + use tokengiver::base::errors::Errors::{NOT_CAMPAIGN_OWNER, INSUFFICIENT_BALANCE}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; #[derive(Drop, Copy, Serde, starknet::Store)] @@ -44,6 +43,7 @@ mod TokengiverCampaign { donation_count: Map, donation_details: Map, strk_address: ContractAddress, + token_giver_nft_class_hash: ClassHash, } // ************************************************************************* @@ -54,6 +54,7 @@ mod TokengiverCampaign { pub enum Event { CreateCampaign: CreateCampaign, DonationCreated: DonationCreated, + DeployedTokenGiverNFT: DeployedTokenGiverNFT, } #[derive(Drop, starknet::Event)] @@ -63,6 +64,14 @@ mod TokengiverCampaign { #[key] campaign_address: ContractAddress, token_id: u256, + token_giverNft_contract_address: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + pub struct DeployedTokenGiverNFT { + pub campaign_id: u256, + pub token_giver_nft_contract_address: ContractAddress, + pub block_timestamp: u64, } #[derive(Drop, starknet::Event)] @@ -80,47 +89,56 @@ mod TokengiverCampaign { // CONSTRUCTOR // ************************************************************************* #[constructor] - fn constructor(ref self: ContractState, strk_address: ContractAddress) { + fn constructor( + ref self: ContractState, + token_giver_nft_class_hash: ClassHash, + strk_address: ContractAddress + ) { + self.token_giver_nft_class_hash.write(token_giver_nft_class_hash); self.strk_address.write(strk_address); } // ************************************************************************* // EXTERNAL FUNCTIONS // ************************************************************************* - #[external(v0)] + #[abi(embed_v0)] impl CampaignImpl of ICampaign { fn create_campaign( ref self: ContractState, - token_giverNft_contract_address: ContractAddress, registry_hash: felt252, implementation_hash: felt252, salt: felt252, recipient: ContractAddress ) -> ContractAddress { let count: u16 = self.count.read() + 1; - ITokenGiverNftDispatcher { contract_address: token_giverNft_contract_address } - .mint_token_giver_nft(recipient); - + let token_Nft_address = self.token_giver_nft_class_hash.read(); + let token_giverNft_contract_address = self + .deploy_token_giver_nft(token_Nft_address, count.into()); let token_id = ITokenGiverNftDispatcher { contract_address: token_giverNft_contract_address } .get_user_token_id(recipient); - let campaign_address = IRegistryLibraryDispatcher { class_hash: registry_hash.try_into().unwrap() } .create_account( implementation_hash, token_giverNft_contract_address, token_id, salt ); - let new_campaign = Campaign { campaign_address, campaign_owner: recipient, metadata_URI: "", }; - self.campaign.write(campaign_address, new_campaign); self.campaigns.write(count, campaign_address); self.count.write(count); - self.emit(CreateCampaign { owner: recipient, campaign_address, token_id }); + self + .emit( + CreateCampaign { + owner: recipient, + campaign_address, + token_id, + token_giverNft_contract_address + } + ); campaign_address } @@ -153,6 +171,23 @@ mod TokengiverCampaign { self.withdrawal_balance.write(campaign_address, amount); } + // withdraw function + fn withdraw(ref self: ContractState, campaign_address: ContractAddress, amount: u256) { + let campaign: Campaign = self.campaign.read(campaign_address); + let caller: ContractAddress = get_caller_address(); + + assert(caller == campaign.campaign_owner, NOT_CAMPAIGN_OWNER); + + let available_balance: u256 = self.withdrawal_balance.read(campaign_address); + assert(amount <= available_balance, INSUFFICIENT_BALANCE); + + let token_address = self.strk_address.read(); + let token_dispatcher = IERC20Dispatcher { contract_address: token_address }; + let transfer_result = token_dispatcher.transfer(caller, amount); + assert!(transfer_result, "Transfer failed"); + self.withdrawal_balance.write(campaign_address, available_balance - amount); + } + // ************************************************************************* // GETTERS // ************************************************************************* @@ -249,4 +284,29 @@ mod TokengiverCampaign { ); } } + + #[generate_trait] + impl InternalImpl of InternalTrait { + fn deploy_token_giver_nft( + ref self: ContractState, token_giver_nft_class_hash: ClassHash, campaign_id: u16 + ) -> ContractAddress { + let mut constructor_calldata = array![]; + campaign_id.serialize(ref constructor_calldata); + + let (token_giver_nft_address, _) = deploy_syscall( + token_giver_nft_class_hash, 'salt'.into(), constructor_calldata.span(), false + ) + .unwrap(); + + self + .emit( + DeployedTokenGiverNFT { + campaign_id: campaign_id.into(), + token_giver_nft_contract_address: token_giver_nft_address, + block_timestamp: get_block_timestamp() + } + ); + token_giver_nft_address + } + } } diff --git a/src/interfaces/ICampaign.cairo b/src/interfaces/ICampaign.cairo index a6754ea..30b4a17 100644 --- a/src/interfaces/ICampaign.cairo +++ b/src/interfaces/ICampaign.cairo @@ -8,7 +8,6 @@ use tokengiver::base::types::Campaign; pub trait ICampaign { fn create_campaign( ref self: TState, - token_giverNft_contract_address: ContractAddress, registry_hash: felt252, implementation_hash: felt252, salt: felt252, @@ -21,6 +20,7 @@ pub trait ICampaign { fn set_available_withdrawal(ref self: TState, campaign_address: ContractAddress, amount: u256); fn set_donations(ref self: TState, campaign_address: ContractAddress, amount: u256); fn donate(ref self: TState, campaign_address: ContractAddress, amount: u256, token_id: u256); + fn withdraw(ref self: TState, campaign_address: ContractAddress, amount: u256); // Getters diff --git a/src/presets/erc20.cairo b/src/presets/erc20.cairo index abf3601..4709047 100644 --- a/src/presets/erc20.cairo +++ b/src/presets/erc20.cairo @@ -31,13 +31,6 @@ mod MyToken { let initial_supply = 100_000_000_u256; self.erc20.initializer(name, symbol); - self.mint(recipient, initial_supply); - } - - #[generate_trait] - impl internalImpl of InternalTrait { - fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { - self.erc20.mint(recipient, amount); - } + self.erc20.mint(recipient, initial_supply); } } diff --git a/tests/test_campaign.cairo b/tests/test_campaign.cairo index 54ec637..58fae8e 100644 --- a/tests/test_campaign.cairo +++ b/tests/test_campaign.cairo @@ -1,6 +1,6 @@ use snforge_std::{ declare, start_cheat_caller_address, stop_cheat_caller_address, ContractClassTrait, - DeclareResultTrait, spy_events, EventSpyAssertionsTrait, + DeclareResultTrait, spy_events, EventSpyAssertionsTrait, get_class_hash }; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; @@ -38,8 +38,10 @@ const ADMIN: felt252 = 'ADMIN'; fn __setup__() -> (ContractAddress, ContractAddress) { let class_hash = declare("TokengiverCampaign").unwrap().contract_class(); let strk_address = deploy_erc20(); + let nft_class_hash = __declare_token_giver_NFT__(); let mut calldata = array![]; + nft_class_hash.serialize(ref calldata); strk_address.serialize(ref calldata); let (contract_address, _) = class_hash.deploy(@calldata).unwrap(); @@ -48,14 +50,8 @@ fn __setup__() -> (ContractAddress, ContractAddress) { } -fn __setup_token_giver_NFT__() -> ContractAddress { - // deploy events - let nft_class_hash = declare("TokenGiverNFT").unwrap().contract_class(); - - let mut events_constructor_calldata: Array = array![ADMIN]; - let (nft_contract_address, _) = nft_class_hash.deploy(@events_constructor_calldata).unwrap(); - - return (nft_contract_address); +fn __declare_token_giver_NFT__() -> ClassHash { + *declare("TokenGiverNFT").unwrap().contract_class().class_hash } fn deploy_erc20() -> ContractAddress { @@ -73,25 +69,23 @@ fn deploy_erc20() -> ContractAddress { #[fork("Mainnet")] fn test_donate() { let (token_giver_address, strk_address) = __setup__(); - let token_giverNft_contract_address = __setup_token_giver_NFT__(); let token_giver = ICampaignDispatcher { contract_address: token_giver_address }; let strk_dispatcher = IERC20Dispatcher { contract_address: strk_address }; let random_id = 1; - let mut spy = spy_events(); + + //create campaign start_cheat_caller_address(token_giver_address, RECIPIENT()); + + let campaign_address = token_giver - .create_campaign( - token_giverNft_contract_address, - REGISTRY_HASH(), - IMPLEMENTATION_HASH(), - SALT(), - RECIPIENT() - ); + .create_campaign(REGISTRY_HASH(), IMPLEMENTATION_HASH(), SALT(), RECIPIENT()); + stop_cheat_caller_address(token_giver_address); + /// Transfer STRK to Donor start_cheat_caller_address(strk_address, OWNER()); let amount = 2000000; // @@ -99,20 +93,24 @@ fn test_donate() { assert(strk_dispatcher.balance_of(DONOR()) >= amount, 'strk bal too low'); stop_cheat_caller_address(strk_address); + // approve allowance start_cheat_caller_address(strk_address, DONOR()); strk_dispatcher.approve(token_giver_address, amount); stop_cheat_caller_address(strk_address); + // donate start_cheat_caller_address(token_giver_address, DONOR()); token_giver.donate(campaign_address, amount, random_id); stop_cheat_caller_address(token_giver_address); + assert(strk_dispatcher.balance_of(DONOR()) == 0, 'wrong balance'); assert(token_giver.get_donations(campaign_address) == amount, 'wrong donation amount'); assert(token_giver.get_donation_count(campaign_address) == 1, 'wrong donation amount'); + let expected_event = Event::DonationCreated( DonationCreated { campaign_id: random_id, @@ -122,5 +120,6 @@ fn test_donate() { block_timestamp: get_block_timestamp(), } ); + spy.assert_emitted(@array![(token_giver.contract_address, expected_event)]); }