diff --git a/.gitignore b/.gitignore index 44d3398..5635fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .snfoundry_cache target .DS_Store -Scarb.lock \ No newline at end of file +Scarb.lock +snfoundry_cache/https___starknet_mainnet_public_blastapi_io_rpc_v0_7_631894_v3.json \ No newline at end of file diff --git a/.snfoundry_cache/.prev_tests_failed b/.snfoundry_cache/.prev_tests_failed new file mode 100644 index 0000000..e69de29 diff --git a/Scarb.lock b/Scarb.lock new file mode 100644 index 0000000..0bbc0d0 --- /dev/null +++ b/Scarb.lock @@ -0,0 +1,115 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "openzeppelin" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" + +[[package]] +name = "openzeppelin_presets" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" + +[[package]] +name = "openzeppelin_token" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" + +[[package]] +name = "openzeppelin_utils" +version = "0.15.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.15.0#f57642960f1c8cffafefb88bfff418eca8510634" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.31.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67" + +[[package]] +name = "snforge_std" +version = "0.31.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67" +dependencies = [ + "snforge_scarb_plugin", +] + +[[package]] +name = "token_bound_accounts" +version = "0.3.0" +source = "git+https://github.com/Starknet-Africa-Edu/TBA?tag=v0.3.0#1f8b5e3c45422fb188ef2cf874b46d02f642973b" + +[[package]] +name = "tokengiver" +version = "0.1.0" +dependencies = [ + "openzeppelin", + "snforge_std", + "token_bound_accounts", +] diff --git a/Scarb.toml b/Scarb.toml index f6d368c..0578744 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -30,4 +30,9 @@ test = "snforge test" name = "SEPOLIA_LATEST" url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_7" block_id.tag = "latest" -# block_id.number = "325081" \ No newline at end of file +# block_id.number = "325081" + +[[tool.snforge.fork]] +name = "Mainnet" +url = "https://starknet-mainnet.public.blastapi.io/rpc/v0_7" +block_id.number = "631894" \ No newline at end of file diff --git a/src/base/types.cairo b/src/base/types.cairo index ae91d5e..8301618 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -15,4 +15,5 @@ pub struct Campaign { pub campaign_address: ContractAddress, pub campaign_owner: ContractAddress, pub metadata_URI: ByteArray, + pub token_id: u256, } diff --git a/src/campaign.cairo b/src/campaign.cairo index 212cf53..6ddf5b6 100644 --- a/src/campaign.cairo +++ b/src/campaign.cairo @@ -8,7 +8,7 @@ mod TokengiverCampaign { use core::traits::TryInto; use starknet::{ ContractAddress, get_caller_address, get_block_timestamp, ClassHash, - syscalls::deploy_syscall, + syscalls::deploy_syscall, SyscallResultTrait, storage::{Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess} }; use tokengiver::interfaces::ITokenGiverNft::{ @@ -23,7 +23,6 @@ mod TokengiverCampaign { use tokengiver::base::errors::Errors::{NOT_CAMPAIGN_OWNER, INSUFFICIENT_BALANCE}; use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; - #[derive(Drop, Copy, Serde, starknet::Store)] pub struct DonationDetails { token_id: u256, @@ -43,8 +42,8 @@ mod TokengiverCampaign { donations: Map, donation_count: Map, donation_details: Map, - erc20_token: ContractAddress, - token_giver_nft_class_hash: ClassHash, + strk_address: ContractAddress, + token_giver_nft_address: ContractAddress, } // ************************************************************************* @@ -65,7 +64,7 @@ mod TokengiverCampaign { #[key] campaign_address: ContractAddress, token_id: u256, - token_giverNft_contract_address: ContractAddress, + token_giver_nft_address: ContractAddress, } #[derive(Drop, starknet::Event)] @@ -90,8 +89,13 @@ mod TokengiverCampaign { // CONSTRUCTOR // ************************************************************************* #[constructor] - fn constructor(ref self: ContractState, token_giver_nft_class_hash: ClassHash) { - self.token_giver_nft_class_hash.write(token_giver_nft_class_hash); + fn constructor( + ref self: ContractState, + token_giver_nft_address: ContractAddress, + strk_address: ContractAddress + ) { + self.token_giver_nft_address.write(token_giver_nft_address); + self.strk_address.write(strk_address); } // ************************************************************************* @@ -104,28 +108,24 @@ mod TokengiverCampaign { registry_hash: felt252, implementation_hash: felt252, salt: felt252, - recipient: ContractAddress ) -> ContractAddress { let caller = get_caller_address(); - let count: u16 = self.count.read() + 1; + let nft_address = self.token_giver_nft_address.read(); + let token_giver_nft = ITokenGiverNftDispatcher { contract_address: nft_address }; - let token_giverNft_contract_address = self - .deploy_token_giver_nft(self.token_giver_nft_class_hash.read(), caller); - - let token_id = ITokenGiverNftDispatcher { - contract_address: token_giverNft_contract_address - } - .get_user_token_id(recipient); + /// mint token giver NFT + let token_id = token_giver_nft.mint_token_giver_nft(caller); + /// create TBA account + let count: u16 = self.count.read() + 1; let campaign_address = IRegistryLibraryDispatcher { class_hash: registry_hash.try_into().unwrap() } - .create_account( - implementation_hash, token_giverNft_contract_address, token_id, salt - ); + .create_account(implementation_hash, nft_address, token_id, salt); + /// create campaign let new_campaign = Campaign { - campaign_address, campaign_owner: recipient, metadata_URI: "", + campaign_address, campaign_owner: caller, metadata_URI: "", token_id, }; self.campaign.write(campaign_address, new_campaign); @@ -134,10 +134,10 @@ mod TokengiverCampaign { self .emit( CreateCampaign { - owner: recipient, + owner: caller, campaign_address, token_id, - token_giverNft_contract_address + token_giver_nft_address: nft_address } ); @@ -183,7 +183,7 @@ mod TokengiverCampaign { let available_balance: u256 = self.withdrawal_balance.read(campaign_address); assert(amount <= available_balance, INSUFFICIENT_BALANCE); - let token_address = self.erc20_token.read(); + 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"); @@ -257,7 +257,7 @@ mod TokengiverCampaign { ) { let donor = get_caller_address(); - let token_address = self.erc20_token.read(); + let token_address = self.strk_address.read(); IERC20Dispatcher { contract_address: token_address } .transfer_from(donor, campaign_address, amount); @@ -286,31 +286,4 @@ mod TokengiverCampaign { ); } } - - #[generate_trait] - impl InternalImpl of InternalTrait { - fn deploy_token_giver_nft( - ref self: ContractState, token_giver_nft_class_hash: ClassHash, admin: ContractAddress - ) -> ContractAddress { - let mut constructor_calldata = array![admin.into()]; - - let (token_giver_nft_address, _) = deploy_syscall( - token_giver_nft_class_hash, - get_block_timestamp().try_into().unwrap(), - constructor_calldata.span(), - false - ) - .unwrap(); - - // self - // .emit( - // DeployedTokenGiverNFT { - // campaign_id: campaign_id, - // 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 30b4a17..5b5740c 100644 --- a/src/interfaces/ICampaign.cairo +++ b/src/interfaces/ICampaign.cairo @@ -7,11 +7,7 @@ use tokengiver::base::types::Campaign; #[starknet::interface] pub trait ICampaign { fn create_campaign( - ref self: TState, - registry_hash: felt252, - implementation_hash: felt252, - salt: felt252, - recipient: ContractAddress + ref self: TState, registry_hash: felt252, implementation_hash: felt252, salt: felt252, ) -> ContractAddress; fn set_campaign_metadata_uri( ref self: TState, campaign_address: ContractAddress, metadata_uri: ByteArray diff --git a/src/interfaces/ITokenGiverNft.cairo b/src/interfaces/ITokenGiverNft.cairo index 89c01ad..1c7dfd2 100644 --- a/src/interfaces/ITokenGiverNft.cairo +++ b/src/interfaces/ITokenGiverNft.cairo @@ -4,7 +4,7 @@ use starknet::ContractAddress; // ************************************************************************* #[starknet::interface] pub trait ITokenGiverNft { - fn mint_token_giver_nft(ref self: TState, address: ContractAddress); + fn mint_token_giver_nft(ref self: TState, address: ContractAddress) -> u256; fn get_last_minted_id(self: @TState) -> u256; fn get_user_token_id(self: @TState, user: ContractAddress) -> u256; fn get_token_mint_timestamp(self: @TState, token_id: u256) -> u64; diff --git a/src/presets.cairo b/src/presets.cairo index 292c7c4..1a787b0 100644 --- a/src/presets.cairo +++ b/src/presets.cairo @@ -1 +1,2 @@ mod campaign; +mod erc20; diff --git a/src/presets/erc20.cairo b/src/presets/erc20.cairo new file mode 100644 index 0000000..4709047 --- /dev/null +++ b/src/presets/erc20.cairo @@ -0,0 +1,36 @@ +#[starknet::contract] +mod MyToken { + use openzeppelin::token::erc20::ERC20Component; + use openzeppelin::token::erc20::ERC20HooksEmptyImpl; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + #[abi(embed_v0)] + impl ERC20Impl = ERC20Component::ERC20Impl; + + impl ERC20InternalImpl = ERC20Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc20: ERC20Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC20Event: ERC20Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, recipient: ContractAddress,) { + let name: ByteArray = "My Token"; + let symbol: ByteArray = "MYT"; + let initial_supply = 100_000_000_u256; + + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } +} diff --git a/src/token_giver_nft.cairo b/src/token_giver_nft.cairo index 2c6f170..d974670 100644 --- a/src/token_giver_nft.cairo +++ b/src/token_giver_nft.cairo @@ -22,6 +22,7 @@ pub mod TokenGiverNFT { component!(path: SRC5Component, storage: src5, event: SRC5Event); // ERC721 Mixin + #[abi(embed_v0)] impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl; impl ERC721InternalImpl = ERC721Component::InternalImpl; @@ -74,7 +75,7 @@ pub mod TokenGiverNFT { // EXTERNAL // ************************************************************************* - fn mint_token_giver_nft(ref self: ContractState, address: ContractAddress) { + fn mint_token_giver_nft(ref self: ContractState, address: ContractAddress) -> u256 { let mut token_id = self.last_minted_id.read() + 1; self.erc721.mint(address, token_id); let timestamp: u64 = get_block_timestamp(); @@ -82,6 +83,7 @@ pub mod TokenGiverNFT { self.user_token_id.write(address, token_id); self.last_minted_id.write(token_id); self.mint_timestamp.write(token_id, timestamp); + token_id } diff --git a/tests/test_campaign.cairo b/tests/test_campaign.cairo new file mode 100644 index 0000000..30532c7 --- /dev/null +++ b/tests/test_campaign.cairo @@ -0,0 +1,120 @@ +use snforge_std::{ + declare, start_cheat_caller_address, stop_cheat_caller_address, ContractClassTrait, + DeclareResultTrait, spy_events, EventSpyAssertionsTrait, get_class_hash +}; + +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +use starknet::{ContractAddress, ClassHash, get_block_timestamp}; + +use tokengiver::interfaces::ICampaign::{ICampaign, ICampaignDispatcher, ICampaignDispatcherTrait}; +use tokengiver::campaign::TokengiverCampaign::{Event, DonationCreated}; + +fn REGISTRY_HASH() -> felt252 { + 0x046163525551f5a50ed027548e86e1ad023c44e0eeb0733f0dab2fb1fdc31ed0.try_into().unwrap() +} +fn IMPLEMENTATION_HASH() -> felt252 { + 0x45d67b8590561c9b54e14dd309c9f38c4e2c554dd59414021f9d079811621bd.try_into().unwrap() +} + +fn DONOR() -> ContractAddress { + 'donor'.try_into().unwrap() +} + +fn SALT() -> felt252 { + 'salty'.try_into().unwrap() +} + +fn RECIPIENT() -> ContractAddress { + 'recipient'.try_into().unwrap() +} + +fn OWNER() -> ContractAddress { + 'owner'.try_into().unwrap() +} + +const ADMIN: felt252 = 'ADMIN'; + +fn __setup__() -> (ContractAddress, ContractAddress) { + let class_hash = declare("TokengiverCampaign").unwrap().contract_class(); + let strk_address = deploy_erc20(); + let nft_address = __deploy_token_giver_NFT__(); + + let mut calldata = array![]; + nft_address.serialize(ref calldata); + strk_address.serialize(ref calldata); + + let (contract_address, _) = class_hash.deploy(@calldata).unwrap(); + + (contract_address, strk_address) +} + +fn __deploy_token_giver_NFT__() -> ContractAddress { + 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 deploy_erc20() -> ContractAddress { + let class = declare("MyToken").unwrap().contract_class(); + + let mut calldata = array![]; + OWNER().serialize(ref calldata); + + let (address, _) = class.deploy(@calldata).unwrap(); + + address +} + +#[test] +#[fork("Mainnet")] +fn test_donate() { + let (token_giver_address, strk_address) = __setup__(); + 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(REGISTRY_HASH(), IMPLEMENTATION_HASH(), SALT()); + + stop_cheat_caller_address(token_giver_address); + + /// Transfer STRK to Donor + start_cheat_caller_address(strk_address, OWNER()); + let amount = 2000000; // + strk_dispatcher.transfer(DONOR(), amount); + 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, + donor_address: DONOR(), + amount: amount, + token_id: random_id, + block_timestamp: get_block_timestamp(), + } + ); + + spy.assert_emitted(@array![(token_giver.contract_address, expected_event)]); +}