From 055e0ec8c2f37d8404d0e3e95777c92ebc9a6273 Mon Sep 17 00:00:00 2001 From: wheval Date: Mon, 28 Jul 2025 10:25:04 +0100 Subject: [PATCH] implement trigger based voting and mock erc20 --- contract_/src/BigIncGenesis.cairo | 334 ++++++++++- contract_/src/lib.cairo | 3 + contract_/src/mocks/mock_erc20.cairo | 42 ++ contract_/tests/test_voting_mechanism.cairo | 584 ++++++++++++++++++++ 4 files changed, 957 insertions(+), 6 deletions(-) create mode 100644 contract_/src/mocks/mock_erc20.cairo create mode 100644 contract_/tests/test_voting_mechanism.cairo diff --git a/contract_/src/BigIncGenesis.cairo b/contract_/src/BigIncGenesis.cairo index 1f6c560..2a75fdc 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); @@ -47,8 +62,38 @@ pub trait IBigIncGenesis { fn get_partner_token_rate(self: @TContractState, token_address: ContractAddress) -> 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::num::traits::Zero; + use core::poseidon::poseidon_hash_span; use core::traits::Into; use openzeppelin::access::ownable::OwnableComponent; use openzeppelin::security::pausable::PausableComponent; @@ -59,7 +104,7 @@ pub mod BigIncGenesis { StoragePointerWriteAccess, }; use starknet::{ContractAddress, get_block_timestamp, get_caller_address, get_contract_address}; - use super::IBigIncGenesis; + use super::{IBigIncGenesis, VoteResult, VoteState, WithdrawalRequest}; component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: PausableComponent, storage: pausable, event: PausableEvent); @@ -105,11 +150,19 @@ pub mod BigIncGenesis { shareholder_count: u32, // Partner token rates (tokens required for 1 full share) partner_token_rates: 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] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { #[flat] OwnableEvent: OwnableComponent::Event, #[flat] @@ -125,14 +178,17 @@ pub mod BigIncGenesis { Withdrawn: Withdrawn, PartnerShareCapSet: PartnerShareCapSet, PartnerShareMinted: PartnerShareMinted, + WithdrawalRequested: WithdrawalRequested, + VoteTriggered: VoteTriggered, + VoteVoted: VoteVoted, } #[derive(Drop, starknet::Event)] - struct ShareMinted { + pub struct ShareMinted { #[key] - buyer: ContractAddress, - shares_bought: u256, - amount: u256, + pub buyer: ContractAddress, + pub shares_bought: u256, + pub amount: u256, } #[derive(Drop, starknet::Event)] @@ -193,6 +249,32 @@ pub mod BigIncGenesis { rate: 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, @@ -221,6 +303,9 @@ pub mod BigIncGenesis { self.is_shareholder_map.write(owner, true); self.shareholder_addresses.write(0, owner); self.shareholder_count.write(1); + + // Initialize vote duration to 7 days + self.vote_duration.write(604800); } #[abi(embed_v0)] @@ -593,6 +678,243 @@ pub mod BigIncGenesis { fn renounce_owner(ref self: ContractState) { self.ownable.renounce_ownership(); } + + fn request_withdrawal( + ref self: ContractState, + token_address: ContractAddress, + amount: u256, + milestone_uri: ByteArray, + deadline_timestamp: u64, + ) -> felt252 { + self.ownable.assert_only_owner(); + self.pausable.assert_not_paused(); + self.reentrancy_guard.start(); + + let caller = get_caller_address(); + let current_timestamp: u64 = get_block_timestamp(); + + assert(deadline_timestamp > current_timestamp, 'Deadline must be in future'); + self._validate_token(token_address); + + // 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(), + ); + + let existing_request = self.withdrawal_requests.read(withdrawal_hash); + assert(existing_request.requester.is_zero(), 'Withdrawal request exists'); + + let request_timestamp: u64 = current_timestamp; + let new_request = WithdrawalRequest { + requester: caller, + token_address, + amount, + milestone_uri: milestone_uri.clone(), + deadline_timestamp, + request_timestamp, + is_executed: false, + }; + self.withdrawal_requests.write(withdrawal_hash, new_request); + + self + .emit( + Event::WithdrawalRequested( + WithdrawalRequested { + withdrawal_hash, + requester: caller, + token_address, + amount, + milestone_uri, + deadline_timestamp, + request_timestamp, + }, + ), + ); + + self.reentrancy_guard.end(); + withdrawal_hash + } + + fn trigger_vote_on_expectation(ref self: ContractState, withdrawal_hash: felt252) { + self.pausable.assert_not_paused(); + + 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.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); + + self.emit(Event::VoteTriggered(VoteTriggered { withdrawal_hash })); + } + + 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 voter = get_caller_address(); + + let has_voted = self.individual_votes.read((withdrawal_hash, voter)); + assert(!has_voted, 'Already voted'); + + // Verify voter is a shareholder and get their voting power + let voting_power = self.shareholders.read(voter); + assert(voting_power > 0, 'No voting power'); + + self.individual_votes.write((withdrawal_hash, voter), true); + + let mut updated_vote_state = vote_state; + updated_vote_state.total_voting_power += voting_power; + + if met_expectation { + updated_vote_state.total_votes_for += voting_power; + } else { + updated_vote_state.total_votes_against += voting_power; + } + + self.vote_states.write(withdrawal_hash, updated_vote_state); + + self.emit(Event::VoteVoted(VoteVoted { withdrawal_hash, voter, met_expectation })); + } + + fn get_withdrawal_request( + self: @ContractState, withdrawal_hash: felt252, + ) -> WithdrawalRequest { + self.withdrawal_requests.read(withdrawal_hash) + } + + fn get_vote_result(self: @ContractState, withdrawal_hash: felt252) -> VoteResult { + let vote_state = self.vote_states.read(withdrawal_hash); + + // 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 participation_rate = if total_possible_voting_power > 0 { + (vote_state.total_voting_power * 10000_u256) / total_possible_voting_power + } else { + 0_u256 + }; + + VoteResult { + vote_state, + met_expectation: vote_state.total_votes_for > vote_state.total_votes_against, + participation_rate, + } + } + + fn has_voted( + self: @ContractState, withdrawal_hash: felt252, voter: ContractAddress, + ) -> bool { + self.individual_votes.read((withdrawal_hash, voter)) + } + + fn execute_withdrawal_after_vote(ref self: ContractState, withdrawal_hash: felt252) { + self.ownable.assert_only_owner(); + self.reentrancy_guard.start(); + + let mut request = self.withdrawal_requests.read(withdrawal_hash); + assert(!request.requester.is_zero(), 'Withdrawal request not found'); + assert(!request.is_executed, 'Already executed'); + + let vote_state = self.vote_states.read(withdrawal_hash); + assert( + !vote_state.is_active || vote_state.end_timestamp < get_block_timestamp(), + 'Voting still active', + ); + + // 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', + ); + + // 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; + } + + let participation_rate = if total_possible_voting_power > 0 { + (vote_state.total_voting_power * 10000_u256) / total_possible_voting_power + } else { + 0_u256 + }; + + // Require at least 25% participation (2500 out of 10000) + assert(participation_rate >= 2500, 'Insufficient participation'); + + // 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); + + 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 421f5ef..97012d2 100644 --- a/contract_/src/lib.cairo +++ b/contract_/src/lib.cairo @@ -1 +1,4 @@ pub mod BigIncGenesis; +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); +}