diff --git a/contract_/src/BigIncGenesis.cairo b/contract_/src/BigIncGenesis.cairo index ac0ff7c..05a6fe0 100644 --- a/contract_/src/BigIncGenesis.cairo +++ b/contract_/src/BigIncGenesis.cairo @@ -20,6 +20,21 @@ pub trait IBigIncGenesis { fn get_shares_sold(self: @TContractState) -> u256; fn is_presale_active(self: @TContractState) -> bool; + fn request_withdrawal( + ref self: TContractState, + token_address: ContractAddress, + amount: u256, + milestone_uri: ByteArray, + deadline_timestamp: u64, + ) -> felt252; + fn trigger_vote_on_expectation(ref self: TContractState, withdrawal_hash: felt252); + fn vote_on_milestone(ref self: TContractState, withdrawal_hash: felt252, met_expectation: bool); + + fn get_withdrawal_request(self: @TContractState, withdrawal_hash: felt252) -> WithdrawalRequest; + fn get_vote_result(self: @TContractState, withdrawal_hash: felt252) -> VoteResult; + fn has_voted(self: @TContractState, withdrawal_hash: felt252, voter: ContractAddress) -> bool; + fn execute_withdrawal_after_vote(ref self: TContractState, withdrawal_hash: felt252); + // Owner functions // fn withdraw(ref self: TContractState, token_address: ContractAddress, amount: u256); fn seize_shares(ref self: TContractState, shareholder: ContractAddress); @@ -124,9 +139,38 @@ pub struct ReputationInfo { pub failed_deliveries: u256 } +#[derive(Drop, Serde, starknet::Store, Clone)] +pub struct WithdrawalRequest { + pub requester: ContractAddress, + pub token_address: ContractAddress, + pub amount: u256, + pub milestone_uri: ByteArray, + pub deadline_timestamp: u64, + pub request_timestamp: u64, + pub is_executed: bool, +} + +#[derive(Drop, Serde, starknet::Store, Copy)] +pub struct VoteState { + pub is_active: bool, + pub start_timestamp: u64, + pub end_timestamp: u64, + pub total_votes_for: u256, + pub total_votes_against: u256, + pub total_voting_power: u256, +} + +#[derive(Drop, Serde, starknet::Store, Copy)] +pub struct VoteResult { + pub vote_state: VoteState, + pub met_expectation: bool, + pub participation_rate: u256, +} + #[starknet::contract] pub mod BigIncGenesis { - use core::pedersen; + use core::num::traits::Zero; + use core::poseidon::poseidon_hash_span; use core::traits::Into; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::security::pausable::PausableComponent; @@ -137,9 +181,7 @@ pub mod BigIncGenesis { StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; - use super::{IBigIncGenesis, VoteStatus, WithdrawalRequest}; - - const SECONDS_PER_DAY: u256 = 86400; // 24 * 60 * 60 + use super::{IBigIncGenesis, VoteResult, VoteState, WithdrawalRequest}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: PausableComponent, storage: pausable, event: PausableEvent); @@ -185,27 +227,14 @@ pub mod BigIncGenesis { shareholder_count: u32, // Partner token rates (tokens required for 1 full share) partner_token_rates: Map, - // Governance - withdrawal_requests: Map, - withdrawal_request_count: u256, - withdrawal_progress_amount: Map, - votes: Map<(u256, ContractAddress), bool>, // (request_id, voter) -> has_voted - vote_choices: Map<(u256, ContractAddress), bool>, // (request_id, voter) -> vote_choice - quorum_percentage: u256, // percentage of total shares needed for quorum (e.g., 50 = 50%) - voting_period_days: u256 // number of days for voting period (e.g., 2 = 2 days) - - ////////////integrety voting//////////// - integrity_vote_trigered: Map, //request_id -> has integrity vote began(true or false) - integrity_vote_deadline: Map, // request_id -> when triggered integrity vote ends - integrity_votes: Map<(u256, ContractAddress), bool>, // (request_id & voter) -> has voted on integrity - integrity_vote_choice: Map<(u256, ContractAddress), bool> // (request_id, voter) -> met expectation (true or false) - integrity_voter: Map<(u256, ContractAddress), IntegrityVoteStatus> // (request_id, voter) -> integrity vote status - integrity_vote_days: u256, // configuration days for integrity vote - - ////////// reputation/accountability tracking //////////// - artist_reputation_score: Map, - failed_deliveries: Map, - successful_deliveries: Map, + // Withdrawal requests + withdrawal_requests: Map, + // Vote states + vote_states: Map, + // Individual votes (withdrawal_hash -> voter -> has_voted) + individual_votes: Map<(felt252, ContractAddress), bool>, + // Vote duration in seconds (default 7 days) + vote_duration: u64, } #[event] @@ -226,12 +255,9 @@ pub mod BigIncGenesis { // Withdrawn: Withdrawn, PartnerShareCapSet: PartnerShareCapSet, PartnerShareMinted: PartnerShareMinted, - WithdrawalRequestSubmitted: WithdrawalRequestSubmitted, - WithdrawalExecuted: WithdrawalExecuted, - WithdrawalCancelled: WithdrawalCancelled, - VoteCast: VoteCast, - VotingEnded: VotingEnded, - GovernanceParametersSet: GovernanceParametersSet, + WithdrawalRequested: WithdrawalRequested, + VoteTriggered: VoteTriggered, + VoteVoted: VoteVoted, } #[derive(Drop, starknet::Event)] @@ -242,76 +268,6 @@ pub mod BigIncGenesis { pub amount: u256, } - #[derive(Drop, starknet::Event)] - pub struct PresaleEnded {} - - #[derive(Drop, starknet::Event)] - pub struct TransferShare { - #[key] - pub from: ContractAddress, - #[key] - pub to: ContractAddress, - pub share_amount: u256, - } - - #[derive(Drop, starknet::Event)] - pub struct Donate { - #[key] - pub donor: ContractAddress, - pub token_address: ContractAddress, - pub amount: u256, - } - - #[derive(Drop, starknet::Event)] - pub struct SharesSeized { - #[key] - pub shareholder: ContractAddress, - pub share_amount: u256, - } - - #[derive(Drop, starknet::Event)] - pub struct AllSharesSold {} - - - #[derive(Drop, starknet::Event)] - pub struct Withdrawn { - #[key] - pub token_address: ContractAddress, - pub amount: u256, - pub owner: ContractAddress, - pub timestamp: u256, - } - - #[derive(Drop, starknet::Event)] - pub struct PartnerShareCapSet { - #[key] - pub token_address: ContractAddress, - pub cap: u256, - } - - #[derive(Drop, starknet::Event)] - pub struct PartnerShareMinted { - #[key] - pub token_address: ContractAddress, - #[key] - pub buyer: ContractAddress, - pub amount_paid: u256, - pub shares_received: u256, - pub rate: u256, - } - - #[derive(Drop, starknet::Event)] - struct WithdrawalRequestSubmitted { - #[key] - request_id: u256, - #[key] - requester: ContractAddress, - token_address: ContractAddress, - amount: u256, - deadline_timestamp: u64, - expectation_hash: felt252, - } - #[derive(Drop, starknet::Event)] struct WithdrawalExecuted { #[key] @@ -354,6 +310,32 @@ pub mod BigIncGenesis { voting_period_days: u256, } + #[derive(Drop, starknet::Event)] + struct WithdrawalRequested { + #[key] + withdrawal_hash: felt252, + requester: ContractAddress, + token_address: ContractAddress, + amount: u256, + milestone_uri: ByteArray, + deadline_timestamp: u64, + request_timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct VoteTriggered { + #[key] + pub withdrawal_hash: felt252, + } + + #[derive(Drop, starknet::Event)] + pub struct VoteVoted { + #[key] + pub withdrawal_hash: felt252, + pub voter: ContractAddress, + pub met_expectation: bool, + } + #[constructor] fn constructor( ref self: ContractState, @@ -383,10 +365,8 @@ pub mod BigIncGenesis { self.shareholder_addresses.write(0, owner); self.shareholder_count.write(1); - // Initialize governance parameters - self.withdrawal_request_count.write(0); - self.quorum_percentage.write(50); // 50% quorum by default - self.voting_period_days.write(2); // 2 days voting period by default + // Initialize vote duration to 7 days + self.vote_duration.write(604800); } #[abi(embed_v0)] @@ -761,216 +741,242 @@ pub mod BigIncGenesis { self.ownable.renounce_ownership(); } - // Governance Functions - fn submit_withdrawal_request( + fn request_withdrawal( ref self: ContractState, token_address: ContractAddress, amount: u256, - deadline_timestamp: u64, milestone_uri: ByteArray, - ) -> u256 { + deadline_timestamp: u64, + ) -> felt252 { self.ownable.assert_only_owner(); self.pausable.assert_not_paused(); - self._validate_token(token_address); + self.reentrancy_guard.start(); - assert(amount > 0, 'Amount must be > 0'); - assert(deadline_timestamp > get_block_timestamp(), 'Deadline must be in future'); + let caller = get_caller_address(); + let current_timestamp: u64 = get_block_timestamp(); - let token = IERC20Dispatcher { contract_address: token_address }; - let contract_address = get_contract_address(); + assert(deadline_timestamp > current_timestamp, 'Deadline must be in future'); + self._validate_token(token_address); - // ensure that we can payout all the withdrawal requests in queue - let withdrawal_progress_amount = self.withdrawal_progress_amount.read(token_address); - assert( - token.balance_of(contract_address) >= withdrawal_progress_amount + amount, - 'Insufficient contract balance', + // Create a simple hash for the withdrawal request + let withdrawal_hash = poseidon_hash_span( + array![ + caller.into(), + token_address.into(), + amount.low.into(), + amount.high.into(), + current_timestamp.into(), + ] + .span(), ); - self - .withdrawal_progress_amount - .write(token_address, withdrawal_progress_amount + amount); - - let request_id = self.withdrawal_request_count.read(); - let current_timestamp = get_block_timestamp(); - let requester = get_caller_address(); - - let voting_period_seconds: u64 = (self.voting_period_days.read() * SECONDS_PER_DAY) - .try_into() - .unwrap(); - let voting_deadline = current_timestamp + voting_period_seconds; - - // Create expectation hash from the milestone URI content and deadline - let uri_hash = self._hash_byte_array(@milestone_uri); - let expectation_hash = pedersen::pedersen(uri_hash, deadline_timestamp.into()); + let existing_request = self.withdrawal_requests.read(withdrawal_hash); + assert(existing_request.requester.is_zero(), 'Withdrawal request exists'); - let request = WithdrawalRequest { - requester, + let request_timestamp: u64 = current_timestamp; + let new_request = WithdrawalRequest { + requester: caller, token_address, amount, + milestone_uri: milestone_uri.clone(), deadline_timestamp, - milestone_uri, - expectation_hash, - created_timestamp: current_timestamp, - voting_deadline, + request_timestamp, is_executed: false, - is_cancelled: false, }; - - self.withdrawal_requests.write(request_id, request); - self.withdrawal_request_count.write(request_id + 1); + self.withdrawal_requests.write(withdrawal_hash, new_request); self .emit( - WithdrawalRequestSubmitted { - request_id, - requester, - token_address, - amount, - deadline_timestamp, - expectation_hash, - }, + Event::WithdrawalRequested( + WithdrawalRequested { + withdrawal_hash, + requester: caller, + token_address, + amount, + milestone_uri, + deadline_timestamp, + request_timestamp, + }, + ), ); - request_id + self.reentrancy_guard.end(); + withdrawal_hash } - fn vote_on_withdrawal_request(ref self: ContractState, request_id: u256, approve: bool) { + fn trigger_vote_on_expectation(ref self: ContractState, withdrawal_hash: felt252) { self.pausable.assert_not_paused(); - let caller = get_caller_address(); - assert(self.is_shareholder_map.read(caller), 'Not a shareholder'); - - let request = self.withdrawal_requests.read(request_id); + let request = self.withdrawal_requests.read(withdrawal_hash); + assert(!request.requester.is_zero(), 'Withdrawal request not found'); assert(!request.is_executed, 'Already executed'); - assert(!request.is_cancelled, 'Request cancelled'); - assert(get_block_timestamp() <= request.voting_deadline, 'Voting period ended'); - assert(caller != request.requester, 'Requester cannot vote'); + assert(request.deadline_timestamp <= get_block_timestamp(), 'Deadline not reached'); + + let existing_vote = self.vote_states.read(withdrawal_hash); + assert(!existing_vote.is_active, 'Vote already active'); + + let current_timestamp: u64 = get_block_timestamp(); + let vote_duration = self.vote_duration.read(); + let new_vote_state = VoteState { + is_active: true, + start_timestamp: current_timestamp, + end_timestamp: current_timestamp + vote_duration, + total_votes_for: 0, + total_votes_against: 0, + total_voting_power: 0, + }; + self.vote_states.write(withdrawal_hash, new_vote_state); - // Check if already voted - assert(!self.votes.read((request_id, caller)), 'Already voted'); + self.emit(Event::VoteTriggered(VoteTriggered { withdrawal_hash })); + } - // Record the vote - self.votes.write((request_id, caller), true); - self.vote_choices.write((request_id, caller), approve); + fn vote_on_milestone( + ref self: ContractState, withdrawal_hash: felt252, met_expectation: bool, + ) { + let vote_state = self.vote_states.read(withdrawal_hash); + assert(vote_state.is_active, 'Voting not active'); + assert(vote_state.end_timestamp >= get_block_timestamp(), 'Voting ended'); - let vote_weight = self.shareholders.read(caller); + let voter = get_caller_address(); - self.emit(VoteCast { request_id, voter: caller, approve, vote_weight }); - } + let has_voted = self.individual_votes.read((withdrawal_hash, voter)); + assert(!has_voted, 'Already voted'); - fn execute_withdrawal(ref self: ContractState, request_id: u256) { - self.pausable.assert_not_paused(); - self.reentrancy_guard.start(); + // Verify voter is a shareholder and get their voting power + let voting_power = self.shareholders.read(voter); + assert(voting_power > 0, 'No voting power'); - let mut request = self.withdrawal_requests.read(request_id); - assert(!request.is_executed, 'Already executed'); - assert(!request.is_cancelled, 'Request cancelled'); + self.individual_votes.write((withdrawal_hash, voter), true); - // Check if voting period has ended - let current_timestamp = get_block_timestamp(); - assert(current_timestamp > request.voting_deadline, 'Voting period not ended'); + let mut updated_vote_state = vote_state; + updated_vote_state.total_voting_power += voting_power; - // Also check execution deadline - assert( - current_timestamp >= request.deadline_timestamp, 'Execution deadline not reached', - ); + if met_expectation { + updated_vote_state.total_votes_for += voting_power; + } else { + updated_vote_state.total_votes_against += voting_power; + } - let vote_status = self._calculate_vote_status(request_id); - assert(vote_status.quorum_reached, 'Quorum not reached'); - assert(vote_status.approved, 'Proposal not approved'); + self.vote_states.write(withdrawal_hash, updated_vote_state); - // Emit voting ended event - self - .emit( - VotingEnded { - request_id, - total_votes_for: vote_status.total_votes_for, - total_votes_against: vote_status.total_votes_against, - approved: vote_status.approved, - }, - ); + self.emit(Event::VoteVoted(VoteVoted { withdrawal_hash, voter, met_expectation })); + } - let token_address = request.token_address; - let amount = request.amount; - let requester = request.requester; + fn get_withdrawal_request( + self: @ContractState, withdrawal_hash: felt252, + ) -> WithdrawalRequest { + self.withdrawal_requests.read(withdrawal_hash) + } - request.is_executed = true; - self.withdrawal_requests.write(request_id, request); + fn get_vote_result(self: @ContractState, withdrawal_hash: felt252) -> VoteResult { + let vote_state = self.vote_states.read(withdrawal_hash); - // Reduce withdrawal_progress_amount - let current_progress = self.withdrawal_progress_amount.read(token_address); - self.withdrawal_progress_amount.write(token_address, current_progress - amount); + // Calculate total possible voting power (all shareholders) + let total_shares_count = self.shareholder_count.read(); + let mut total_possible_voting_power: u256 = 0; + let mut i = 0; + while i < total_shares_count { + let shareholder = self.shareholder_addresses.read(i); + if self.is_shareholder_map.read(shareholder) { + total_possible_voting_power += self.shareholders.read(shareholder); + } + i += 1; + } - let token = IERC20Dispatcher { contract_address: token_address }; - token.transfer(requester, amount); + let participation_rate = if total_possible_voting_power > 0 { + (vote_state.total_voting_power * 10000_u256) / total_possible_voting_power + } else { + 0_u256 + }; - self.emit(WithdrawalExecuted { request_id, token_address, amount, requester }); + VoteResult { + vote_state, + met_expectation: vote_state.total_votes_for > vote_state.total_votes_against, + participation_rate, + } + } - self.reentrancy_guard.end(); + fn has_voted( + self: @ContractState, withdrawal_hash: felt252, voter: ContractAddress, + ) -> bool { + self.individual_votes.read((withdrawal_hash, voter)) } - fn cancel_withdrawal_request(ref self: ContractState, request_id: u256) { + fn execute_withdrawal_after_vote(ref self: ContractState, withdrawal_hash: felt252) { self.ownable.assert_only_owner(); - self.pausable.assert_not_paused(); self.reentrancy_guard.start(); - let mut request = self.withdrawal_requests.read(request_id); + let mut request = self.withdrawal_requests.read(withdrawal_hash); + assert(!request.requester.is_zero(), 'Withdrawal request not found'); assert(!request.is_executed, 'Already executed'); - assert(!request.is_cancelled, 'Already cancelled'); - - let token_address = request.token_address; - let amount = request.amount; - - // Update is_cancelled = true - request.is_cancelled = true; - self.withdrawal_requests.write(request_id, request); - // Reduce withdrawal_progress_amount - let current_progress = self.withdrawal_progress_amount.read(token_address); - self.withdrawal_progress_amount.write(token_address, current_progress - amount); - - self.emit(WithdrawalCancelled { request_id, token_address, amount }); - - self.reentrancy_guard.end(); - } - - fn set_governance_parameters( - ref self: ContractState, quorum_percentage: u256, voting_period_days: u256, - ) { - self.ownable.assert_only_owner(); - assert(quorum_percentage <= 100, 'Quorum cannot exceed 100%'); - assert(voting_period_days > 0, 'Voting period must be > 0'); - assert(quorum_percentage > 0, 'Quorum must be > 0'); + let vote_state = self.vote_states.read(withdrawal_hash); + assert( + !vote_state.is_active || vote_state.end_timestamp < get_block_timestamp(), + 'Voting still active', + ); - self.quorum_percentage.write(quorum_percentage); - self.voting_period_days.write(voting_period_days); + // Check if the vote passed (more votes for than against) + assert( + vote_state.total_votes_for > vote_state.total_votes_against, 'Vote did not pass', + ); - self.emit(GovernanceParametersSet { quorum_percentage, voting_period_days }); - } + // Minimum participation requirement (e.g., at least 25% of voting power participated) + let total_shares_count = self.shareholder_count.read(); + let mut total_possible_voting_power: u256 = 0; + let mut i = 0; + while i < total_shares_count { + let shareholder = self.shareholder_addresses.read(i); + if self.is_shareholder_map.read(shareholder) { + total_possible_voting_power += self.shareholders.read(shareholder); + } + i += 1; + } - // Governance View Functions - fn get_withdrawal_request(self: @ContractState, request_id: u256) -> WithdrawalRequest { - self.withdrawal_requests.read(request_id) - } + let participation_rate = if total_possible_voting_power > 0 { + (vote_state.total_voting_power * 10000_u256) / total_possible_voting_power + } else { + 0_u256 + }; - fn get_vote_status(self: @ContractState, request_id: u256) -> VoteStatus { - self._calculate_vote_status(request_id) - } + // Require at least 25% participation (2500 out of 10000) + assert(participation_rate >= 2500, 'Insufficient participation'); - fn get_governance_parameters(self: @ContractState) -> (u256, u256) { - (self.quorum_percentage.read(), self.voting_period_days.read()) - } + // Execute the withdrawal + let token = IERC20Dispatcher { contract_address: request.token_address }; + let contract_address = get_contract_address(); + assert(token.balance_of(contract_address) >= request.amount, 'Insufficient balance'); - + let owner = self.ownable.owner(); + token.transfer(owner, request.amount); + + // Mark as executed - create new struct with updated field + let updated_request = WithdrawalRequest { + requester: request.requester, + token_address: request.token_address, + amount: request.amount, + milestone_uri: request.milestone_uri.clone(), + deadline_timestamp: request.deadline_timestamp, + request_timestamp: request.request_timestamp, + is_executed: true, + }; + self.withdrawal_requests.write(withdrawal_hash, updated_request); - ///////////////////////////////////////////////////////// - ///////////////////Integrety Voting Implementation////////////////////// - ///////////////////////////////////////////////////////// - - fn integrety_vote(ref sef: ContractState, vote_id: u64, voter: ContractAddress, vote_cast: bool) -> bool { + let ts: u256 = get_block_timestamp().into(); + self + .emit( + Event::Withdrawn( + Withdrawn { + token_address: request.token_address, + amount: request.amount, + owner, + timestamp: ts, + }, + ), + ); + self.reentrancy_guard.end(); } - } #[generate_trait] diff --git a/contract_/src/lib.cairo b/contract_/src/lib.cairo index 6f4b4fb..97012d2 100644 --- a/contract_/src/lib.cairo +++ b/contract_/src/lib.cairo @@ -1,2 +1,4 @@ pub mod BigIncGenesis; -pub mod MockERC20; +pub mod mocks { + pub mod mock_erc20; +} diff --git a/contract_/src/mocks/mock_erc20.cairo b/contract_/src/mocks/mock_erc20.cairo new file mode 100644 index 0000000..4a336ad --- /dev/null +++ b/contract_/src/mocks/mock_erc20.cairo @@ -0,0 +1,42 @@ +#[starknet::contract] +pub mod MockERC20 { + use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl}; + use starknet::ContractAddress; + + component!(path: ERC20Component, storage: erc20, event: ERC20Event); + + // ERC20 Mixin + #[abi(embed_v0)] + impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl; + 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, + name: ByteArray, + symbol: ByteArray, + initial_supply: u256, + recipient: ContractAddress, + ) { + self.erc20.initializer(name, symbol); + self.erc20.mint(recipient, initial_supply); + } + + #[external(v0)] + pub fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.erc20.mint(recipient, amount); + } +} diff --git a/contract_/tests/test_voting_mechanism.cairo b/contract_/tests/test_voting_mechanism.cairo new file mode 100644 index 0000000..49e5034 --- /dev/null +++ b/contract_/tests/test_voting_mechanism.cairo @@ -0,0 +1,584 @@ +use contract_::BigIncGenesis::{ + BigIncGenesis, IBigIncGenesisDispatcher, IBigIncGenesisDispatcherTrait, +}; +use core::result::ResultTrait; +use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use openzeppelin::utils::serde::SerializedAppend; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, declare, spy_events, + start_cheat_block_timestamp, start_cheat_caller_address, stop_cheat_block_timestamp, + stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +const OWNER: felt252 = 'owner'; +const USER1: felt252 = 'user1'; +const USER2: felt252 = 'user2'; +const USER3: felt252 = 'user3'; +const USDT_INITIAL_SUPPLY: u256 = 1000000000000_u256; // 1M USDT with 6 decimals +const USDC_INITIAL_SUPPLY: u256 = 1000000000000_u256; // 1M USDC with 6 decimals + +fn deploy_mock_erc20( + name: ByteArray, symbol: ByteArray, initial_supply: u256, recipient: ContractAddress, +) -> ContractAddress { + let contract = declare("MockERC20").unwrap().contract_class(); + let mut constructor_args = array![]; + constructor_args.append_serde(name); + constructor_args.append_serde(symbol); + constructor_args.append_serde(initial_supply); + constructor_args.append_serde(recipient); + + let (contract_address, _) = contract.deploy(@constructor_args).unwrap(); + contract_address +} + +fn deploy_big_inc_genesis( + usdt_address: ContractAddress, usdc_address: ContractAddress, +) -> ContractAddress { + let contract = declare("BigIncGenesis").unwrap().contract_class(); + let (contract_address, _) = contract + .deploy(@array![usdt_address.into(), usdc_address.into(), OWNER.try_into().unwrap()]) + .unwrap(); + contract_address +} + +fn setup() -> (ContractAddress, ContractAddress, ContractAddress) { + let owner: ContractAddress = OWNER.try_into().unwrap(); + let usdt_address = deploy_mock_erc20("USDT", "USDT", USDT_INITIAL_SUPPLY, owner); + let usdc_address = deploy_mock_erc20("USDC", "USDC", USDC_INITIAL_SUPPLY, owner); + let big_inc_address = deploy_big_inc_genesis(usdt_address, usdc_address); + + // Distribute tokens to users for testing + let user1: ContractAddress = USER1.try_into().unwrap(); + let user2: ContractAddress = USER2.try_into().unwrap(); + let user3: ContractAddress = USER3.try_into().unwrap(); + + let usdt = IERC20Dispatcher { contract_address: usdt_address }; + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + start_cheat_caller_address(usdt_address, owner); + usdt.transfer(user1, 1100000000_u256); // 1100 USDT + usdt.transfer(user2, 1100000000_u256); // 1100 USDT + usdt.transfer(user3, 600000000_u256); // 600 USDT + stop_cheat_caller_address(usdt_address); + + start_cheat_caller_address(usdc_address, owner); + usdc.transfer(user1, 1100000000_u256); // 1100 USDC + usdc.transfer(user2, 1100000000_u256); // 1100 USDC + usdc.transfer(user3, 600000000_u256); // 600 USDC + stop_cheat_caller_address(usdc_address); + + (big_inc_address, usdt_address, usdc_address) +} + + +fn create_shareholders( + big_inc: IBigIncGenesisDispatcher, usdt_address: ContractAddress, usdc_address: ContractAddress, +) { + let user1: ContractAddress = USER1.try_into().unwrap(); + let user2: ContractAddress = USER2.try_into().unwrap(); + let user3: ContractAddress = USER3.try_into().unwrap(); + + let usdt = IERC20Dispatcher { contract_address: usdt_address }; + let usdc = IERC20Dispatcher { contract_address: usdc_address }; + + // User1 buys shares with USDT + start_cheat_caller_address(usdt_address, user1); + usdt.approve(big_inc.contract_address, 1000000000_u256); // 1000 USDT + stop_cheat_caller_address(usdt_address); + + start_cheat_caller_address(big_inc.contract_address, user1); + big_inc.mint_share(usdt_address); + stop_cheat_caller_address(big_inc.contract_address); + + // User2 buys shares with USDC + start_cheat_caller_address(usdc_address, user2); + usdc.approve(big_inc.contract_address, 1000000000_u256); // 1000 USDC + stop_cheat_caller_address(usdc_address); + + start_cheat_caller_address(big_inc.contract_address, user2); + big_inc.mint_share(usdc_address); + stop_cheat_caller_address(big_inc.contract_address); + + // User3 buys shares with USDT + start_cheat_caller_address(usdt_address, user3); + usdt.approve(big_inc.contract_address, 500000000_u256); // 500 USDT + stop_cheat_caller_address(usdt_address); + + start_cheat_caller_address(big_inc.contract_address, user3); + big_inc.mint_share(usdt_address); + stop_cheat_caller_address(big_inc.contract_address); +} + + +#[test] +fn test_request_withdrawal_success() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + + start_cheat_caller_address(big_inc_address, owner); + + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; // 1 day from now + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal( + usdt_address, 100000000_u256, // 100 USDT + "ipfs://milestone1", deadline_timestamp, + ); + + let request = big_inc.get_withdrawal_request(withdrawal_hash); + assert(request.requester == owner, 'Wrong requester'); + assert(request.token_address == usdt_address, 'Wrong token address'); + assert(request.amount == 100000000_u256, 'Wrong amount'); + assert(request.deadline_timestamp == deadline_timestamp, 'Wrong deadline'); + assert(!request.is_executed, 'Should not be executed'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('Caller is not the owner',))] +fn test_request_withdrawal_not_owner() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let user1: ContractAddress = USER1.try_into().unwrap(); + + start_cheat_caller_address(big_inc_address, user1); + + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('Deadline must be in future',))] +fn test_request_withdrawal_past_deadline() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + + start_cheat_caller_address(big_inc_address, owner); + + let current_timestamp = 1000000_u64; + let deadline_timestamp = 913600_u64; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +fn test_trigger_vote_on_expectation_success() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + + // Create withdrawal request + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + let vote_result = big_inc.get_vote_result(withdrawal_hash); + assert(vote_result.vote_state.is_active, 'Vote should be active'); + assert(vote_result.vote_state.total_votes_for == 0, 'No votes yet'); + assert(vote_result.vote_state.total_votes_against == 0, 'No votes yet'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('Withdrawal request not found',))] +fn test_trigger_vote_nonexistent_request() { + let (big_inc_address, _usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + + let fake_hash = 999999; + big_inc.trigger_vote_on_expectation(fake_hash); +} + + +#[test] +#[should_panic(expected: ('Deadline not reached',))] +fn test_trigger_vote_before_deadline() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +fn test_vote_on_milestone_success() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + + let has_voted = big_inc.has_voted(withdrawal_hash, user1); + assert(has_voted, 'User should have voted'); + + let vote_result = big_inc.get_vote_result(withdrawal_hash); + assert(vote_result.vote_state.total_votes_for > 0, 'Should have votes'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('Already voted',))] +fn test_vote_on_milestone_double_vote() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + // User1 votes twice + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + big_inc.vote_on_milestone(withdrawal_hash, false); // Should fail + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +fn test_execute_withdrawal_after_vote_success() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + let user2: ContractAddress = USER2.try_into().unwrap(); + + let mut spy = spy_events(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + start_cheat_caller_address(big_inc_address, user2); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + let user3: ContractAddress = USER3.try_into().unwrap(); + start_cheat_caller_address(big_inc_address, user3); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + start_cheat_caller_address(big_inc_address, owner); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + // Move time past voting period (voting started at deadline_timestamp + 1) + start_cheat_block_timestamp( + big_inc_address, (deadline_timestamp + 1) + 604800 + 1, + ); // 7 days after voting started + 1 second + + // Execute withdrawal + start_cheat_caller_address(big_inc_address, owner); + big_inc.execute_withdrawal_after_vote(withdrawal_hash); + + // Verify withdrawal was executed + let request = big_inc.get_withdrawal_request(withdrawal_hash); + assert(request.is_executed, 'Should be executed'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('Vote did not pass',))] +fn test_execute_withdrawal_vote_failed() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + let user2: ContractAddress = USER2.try_into().unwrap(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, false); + stop_cheat_caller_address(big_inc_address); + + start_cheat_caller_address(big_inc_address, user2); + big_inc.vote_on_milestone(withdrawal_hash, false); + stop_cheat_caller_address(big_inc_address); + + // Move time past voting period (voting started at deadline_timestamp + 1) + start_cheat_block_timestamp(big_inc_address, (deadline_timestamp + 1) + 604800 + 1); + + // Try to execute withdrawal (should fail) + start_cheat_caller_address(big_inc_address, owner); + big_inc.execute_withdrawal_after_vote(withdrawal_hash); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +#[should_panic(expected: ('No voting power',))] +fn test_vote_on_milestone_no_shares() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +fn test_has_voted_function() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + let has_voted_before = big_inc.has_voted(withdrawal_hash, user1); + assert(!has_voted_before, 'Should not have voted yet'); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + + let has_voted_after = big_inc.has_voted(withdrawal_hash, user1); + assert(has_voted_after, 'Should have voted'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + + +#[test] +fn test_vote_participation_rate() { + let (big_inc_address, usdt_address, usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + let user2: ContractAddress = USER2.try_into().unwrap(); + + create_shareholders(big_inc, usdt_address, usdc_address); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + let vote_result = big_inc.get_vote_result(withdrawal_hash); + + assert(vote_result.vote_state.total_votes_for > 0, 'Should have some votes'); + assert(vote_result.vote_state.total_voting_power > 0, 'Should have voting power'); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +} + +#[test] +fn test_events_emission() { + let (big_inc_address, usdt_address, _usdc_address) = setup(); + let big_inc = IBigIncGenesisDispatcher { contract_address: big_inc_address }; + let owner: ContractAddress = OWNER.try_into().unwrap(); + let user1: ContractAddress = USER1.try_into().unwrap(); + + let mut spy = spy_events(); + + let usdt = IERC20Dispatcher { contract_address: usdt_address }; + start_cheat_caller_address(usdt_address, user1); + usdt.approve(big_inc.contract_address, 1000000000_u256); // 1000 USDT + stop_cheat_caller_address(usdt_address); + + start_cheat_caller_address(big_inc.contract_address, user1); + big_inc.mint_share(usdt_address); + stop_cheat_caller_address(big_inc.contract_address); + + let expected_shares = (1000000000_u256 * 100000000_u256) / 457143000000_u256; // ~218750 shares + spy + .assert_emitted( + @array![ + ( + big_inc_address, + BigIncGenesis::Event::ShareMinted( + BigIncGenesis::ShareMinted { + buyer: user1, shares_bought: expected_shares, amount: 1000000000_u256, + }, + ), + ), + ], + ); + + start_cheat_caller_address(big_inc_address, owner); + let current_timestamp = 1000000_u64; + let deadline_timestamp = current_timestamp + 86400; + start_cheat_block_timestamp(big_inc_address, current_timestamp); + + let withdrawal_hash = big_inc + .request_withdrawal(usdt_address, 100000000_u256, "ipfs://milestone1", deadline_timestamp); + + start_cheat_block_timestamp(big_inc_address, deadline_timestamp + 1); + big_inc.trigger_vote_on_expectation(withdrawal_hash); + + spy + .assert_emitted( + @array![ + ( + big_inc_address, + BigIncGenesis::Event::VoteTriggered( + BigIncGenesis::VoteTriggered { withdrawal_hash }, + ), + ), + ], + ); + + start_cheat_caller_address(big_inc_address, user1); + big_inc.vote_on_milestone(withdrawal_hash, true); + stop_cheat_caller_address(big_inc_address); + + spy + .assert_emitted( + @array![ + ( + big_inc_address, + BigIncGenesis::Event::VoteVoted( + BigIncGenesis::VoteVoted { + withdrawal_hash, voter: user1, met_expectation: true, + }, + ), + ), + ], + ); + + stop_cheat_caller_address(big_inc_address); + stop_cheat_block_timestamp(big_inc_address); +}