diff --git a/contract_/src/BigIncGenesis.cairo b/contract_/src/BigIncGenesis.cairo index c445869..d47f353 100644 --- a/contract_/src/BigIncGenesis.cairo +++ b/contract_/src/BigIncGenesis.cairo @@ -1,4 +1,5 @@ use starknet::ContractAddress; +use core::byte_array::ByteArray; #[starknet::interface] pub trait IBigIncGenesis { @@ -20,8 +21,27 @@ pub trait IBigIncGenesis { fn get_shares_sold(self: @TContractState) -> u256; fn is_presale_active(self: @TContractState) -> bool; + // Governance withdrawal functions + fn submit_withdrawal_request( + ref self: TContractState, + token_address: ContractAddress, + amount: u256, + deadline_timestamp: u64, + milestone_uri: ByteArray, + ) -> u256; + fn vote_on_withdrawal_request(ref self: TContractState, request_id: u256, approve: bool); + fn execute_withdrawal(ref self: TContractState, request_id: u256); + fn cancel_withdrawal_request(ref self: TContractState, request_id: u256); + fn set_governance_parameters( + ref self: TContractState, quorum_percentage: u256, voting_period_days: u256, + ); + + // Governance view functions + fn get_withdrawal_request(self: @TContractState, request_id: u256) -> WithdrawalRequest; + fn get_vote_status(self: @TContractState, request_id: u256) -> VoteStatus; + fn get_governance_parameters(self: @TContractState) -> (u256, u256); + // Owner functions - // fn withdraw(ref self: TContractState, token_address: ContractAddress, amount: u256); fn seize_shares(ref self: TContractState, shareholder: ContractAddress); fn set_partner_share_cap(ref self: TContractState, token_address: ContractAddress, cap: u256); fn remove_partner_share_cap(ref self: TContractState, token_address: ContractAddress); @@ -45,26 +65,6 @@ pub trait IBigIncGenesis { ref self: TContractState, token_address: ContractAddress, tokens_per_share: u256, ); fn get_partner_token_rate(self: @TContractState, token_address: ContractAddress) -> u256; - - // Governance withdrawal functions - fn submit_withdrawal_request( - ref self: TContractState, - token_address: ContractAddress, - amount: u256, - deadline_timestamp: u64, - milestone_uri: ByteArray, - ) -> u256; - fn vote_on_withdrawal_request(ref self: TContractState, request_id: u256, approve: bool); - fn execute_withdrawal(ref self: TContractState, request_id: u256); - fn cancel_withdrawal_request(ref self: TContractState, request_id: u256); - fn set_governance_parameters( - ref self: TContractState, quorum_percentage: u256, voting_period_days: u256, - ); - - // Governance view functions - fn get_withdrawal_request(self: @TContractState, request_id: u256) -> WithdrawalRequest; - fn get_vote_status(self: @TContractState, request_id: u256) -> VoteStatus; - fn get_governance_parameters(self: @TContractState) -> (u256, u256); } #[derive(Drop, Serde, starknet::Store)] @@ -74,7 +74,6 @@ pub struct WithdrawalRequest { pub amount: u256, pub deadline_timestamp: u64, pub milestone_uri: ByteArray, - pub expectation_hash: felt252, pub created_timestamp: u64, pub voting_deadline: u64, pub is_executed: bool, @@ -91,8 +90,26 @@ pub struct VoteStatus { pub voting_ended: 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::pedersen; use core::traits::Into; use openzeppelin::access::ownable::OwnableComponent; @@ -106,7 +123,7 @@ pub mod BigIncGenesis { 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 + const SECONDS_PER_DAY: u64 = 86400; // 24 * 60 * 60 component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); component!(path: PausableComponent, storage: pausable, event: PausableEvent); @@ -152,14 +169,13 @@ pub mod BigIncGenesis { shareholder_count: u32, // Partner token rates (tokens required for 1 full share) partner_token_rates: Map, - // Governance + // Governance storage 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) + next_request_id: u256, + votes: Map<(u256, ContractAddress), bool>, + vote_choices: Map<(u256, ContractAddress), bool>, + quorum_percentage: u256, + voting_period_days: u256, } #[event] @@ -177,7 +193,6 @@ pub mod BigIncGenesis { Donate: Donate, SharesSeized: SharesSeized, AllSharesSold: AllSharesSold, - // Withdrawn: Withdrawn, PartnerShareCapSet: PartnerShareCapSet, PartnerShareMinted: PartnerShareMinted, WithdrawalRequestSubmitted: WithdrawalRequestSubmitted, @@ -226,16 +241,6 @@ pub mod BigIncGenesis { #[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] @@ -255,57 +260,57 @@ pub mod BigIncGenesis { } #[derive(Drop, starknet::Event)] - struct WithdrawalRequestSubmitted { + pub struct WithdrawalRequestSubmitted { #[key] - request_id: u256, + pub request_id: u256, #[key] - requester: ContractAddress, - token_address: ContractAddress, - amount: u256, - deadline_timestamp: u64, - expectation_hash: felt252, + pub requester: ContractAddress, + pub token_address: ContractAddress, + pub amount: u256, + pub deadline_timestamp: u64, + pub milestone_uri: ByteArray, } #[derive(Drop, starknet::Event)] - struct WithdrawalExecuted { + pub struct WithdrawalExecuted { #[key] - request_id: u256, - token_address: ContractAddress, - amount: u256, - requester: ContractAddress, + pub request_id: u256, + pub token_address: ContractAddress, + pub amount: u256, + pub requester: ContractAddress, } #[derive(Drop, starknet::Event)] - struct WithdrawalCancelled { + pub struct WithdrawalCancelled { #[key] - request_id: u256, - token_address: ContractAddress, - amount: u256, + pub request_id: u256, + pub token_address: ContractAddress, + pub amount: u256, } #[derive(Drop, starknet::Event)] - struct VoteCast { + pub struct VoteCast { #[key] - request_id: u256, + pub request_id: u256, #[key] - voter: ContractAddress, - approve: bool, - vote_weight: u256, + pub voter: ContractAddress, + pub approve: bool, + pub vote_weight: u256, } #[derive(Drop, starknet::Event)] - struct VotingEnded { + pub struct VotingEnded { #[key] - request_id: u256, - total_votes_for: u256, - total_votes_against: u256, - approved: bool, + pub request_id: u256, + pub total_votes_for: u256, + pub total_votes_against: u256, + pub approved: bool, } #[derive(Drop, starknet::Event)] - struct GovernanceParametersSet { - quorum_percentage: u256, - voting_period_days: u256, + pub struct GovernanceParametersSet { + pub quorum_percentage: u256, + pub voting_period_days: u256, } #[constructor] @@ -330,17 +335,17 @@ pub mod BigIncGenesis { self.available_shares.write(82000000_u256); // 82% available after 18% to owner self.is_presale_active.write(true); + // Initialize governance parameters + self.next_request_id.write(1); + self.quorum_percentage.write(51); // 51% quorum + self.voting_period_days.write(7); // 7 days voting period + // Assign 18% shares to owner let owner_shares = 18000000_u256; self.shareholders.write(owner, owner_shares); self.is_shareholder_map.write(owner, true); 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 } #[abi(embed_v0)] @@ -469,26 +474,6 @@ pub mod BigIncGenesis { self.emit(Donate { donor: caller, token_address, amount }); } - // fn withdraw(ref self: ContractState, token_address: ContractAddress, amount: u256) { - // self.ownable.assert_only_owner(); - // self.reentrancy_guard.start(); - - // let token = IERC20Dispatcher { contract_address: token_address }; - // let contract_address = get_contract_address(); - - // assert(token.balance_of(contract_address) >= amount, 'Insufficient balance'); - - // let owner = self.ownable.owner(); - // token.transfer(owner, amount); - - // // Emit Withdrawn event - // let ts: u256 = get_block_timestamp().into(); - // self.emit(Event::Withdrawn(Withdrawn { token_address, amount, owner, timestamp: ts - // })); - - // self.reentrancy_guard.end(); - // } - fn seize_shares(ref self: ContractState, shareholder: ContractAddress) { self.ownable.assert_only_owner(); self.pausable.assert_not_paused(); @@ -630,6 +615,175 @@ pub mod BigIncGenesis { self.partner_token_rates.write(token_address, tokens_per_share); } + // Governance functions + fn submit_withdrawal_request( + ref self: ContractState, + token_address: ContractAddress, + amount: u256, + deadline_timestamp: u64, + milestone_uri: ByteArray, + ) -> u256 { + self.pausable.assert_not_paused(); + self.reentrancy_guard.start(); + + let caller = get_caller_address(); + assert(self.is_shareholder(caller), 'Only shareholders can submit'); + assert(amount > 0, 'Amount must be > 0'); + assert(deadline_timestamp > get_block_timestamp(), 'Deadline must be in the future'); + + let request_id = self.next_request_id.read(); + let current_timestamp = get_block_timestamp(); + let voting_deadline = current_timestamp + (self.voting_period_days.read().try_into().unwrap() * SECONDS_PER_DAY); + + let milestone_uri_for_event = milestone_uri.clone(); + + let request = WithdrawalRequest { + requester: caller, + token_address, + amount, + deadline_timestamp, + milestone_uri, + created_timestamp: current_timestamp, + voting_deadline, + is_executed: false, + is_cancelled: false, + }; + + self.withdrawal_requests.write(request_id, request); + self.next_request_id.write(request_id + 1); + + self.emit(WithdrawalRequestSubmitted { + request_id, + requester: caller, + token_address, + amount, + deadline_timestamp, + milestone_uri: milestone_uri_for_event, + }); + + self.reentrancy_guard.end(); + request_id + } + + fn vote_on_withdrawal_request( + ref self: ContractState, request_id: u256, approve: bool, + ) { + self.pausable.assert_not_paused(); + + let caller = get_caller_address(); + assert(self.is_shareholder(caller), 'Only shareholders can vote'); + + let request = self.withdrawal_requests.read(request_id); + assert(!request.is_executed, 'Request already executed'); + assert(!request.is_cancelled, 'Request already cancelled'); + assert(get_block_timestamp() <= request.voting_deadline, 'Voting period ended'); + assert(caller != request.requester, 'Requester cannot vote'); + + // Check if already voted + assert(!self.votes.read((request_id, caller)), 'Already voted'); + + let vote_weight = self.shareholders.read(caller); + assert(vote_weight > 0, 'No shares to vote with'); + + self.votes.write((request_id, caller), true); + self.vote_choices.write((request_id, caller), approve); + + self.emit(VoteCast { request_id, voter: caller, approve, vote_weight }); + } + + fn execute_withdrawal(ref self: ContractState, request_id: u256) { + self.pausable.assert_not_paused(); + self.reentrancy_guard.start(); + + let request = self.withdrawal_requests.read(request_id); + assert(!request.is_executed, 'Request already executed'); + assert(!request.is_cancelled, 'Request already cancelled'); + assert(get_block_timestamp() > request.voting_deadline, 'Voting period not ended'); + + let vote_status = self._calculate_vote_status(request_id); + assert(vote_status.voting_ended, 'Voting not ended'); + assert(vote_status.quorum_reached, 'Quorum not reached'); + assert(vote_status.approved, 'Request not approved'); + + let contract_address = get_contract_address(); + let token = IERC20Dispatcher { contract_address: request.token_address }; + + assert( + token.balance_of(contract_address) >= request.amount, + 'Insufficient contract balance', + ); + + // Mark as executed + let mut updated_request = WithdrawalRequest { + requester: request.requester, + token_address: request.token_address, + amount: request.amount, + deadline_timestamp: request.deadline_timestamp, + milestone_uri: request.milestone_uri, + created_timestamp: request.created_timestamp, + voting_deadline: request.voting_deadline, + is_executed: true, + is_cancelled: request.is_cancelled, + }; + self.withdrawal_requests.write(request_id, updated_request); + + // Transfer tokens + token.transfer(request.requester, request.amount); + + self.emit(WithdrawalExecuted { + request_id, + token_address: request.token_address, + amount: request.amount, + requester: request.requester, + }); + + self.reentrancy_guard.end(); + } + + fn cancel_withdrawal_request(ref self: ContractState, request_id: u256) { + let caller = get_caller_address(); + let request = self.withdrawal_requests.read(request_id); + + assert( + caller == request.requester || caller == self.ownable.owner(), + 'Unauthorized cancel', + ); + assert(!request.is_executed, 'Request already executed'); + assert(!request.is_cancelled, 'Request already cancelled'); + + let mut updated_request = WithdrawalRequest { + requester: request.requester, + token_address: request.token_address, + amount: request.amount, + deadline_timestamp: request.deadline_timestamp, + milestone_uri: request.milestone_uri, + created_timestamp: request.created_timestamp, + voting_deadline: request.voting_deadline, + is_executed: request.is_executed, + is_cancelled: true, + }; + self.withdrawal_requests.write(request_id, updated_request); + + self.emit(WithdrawalCancelled { + request_id, + token_address: request.token_address, + amount: request.amount, + }); + } + + fn set_governance_parameters( + ref self: ContractState, quorum_percentage: u256, voting_period_days: u256, + ) { + self.ownable.assert_only_owner(); + assert(quorum_percentage > 0 && quorum_percentage <= 100, 'Invalid quorum percentage'); + assert(voting_period_days > 0, 'Voting period must be > 0'); + + self.quorum_percentage.write(quorum_percentage); + self.voting_period_days.write(voting_period_days); + + self.emit(GovernanceParametersSet { quorum_percentage, voting_period_days }); + } + // View functions fn get_partner_token_rate(self: @ContractState, token_address: ContractAddress) -> u256 { self.partner_token_rates.read(token_address) @@ -715,194 +869,6 @@ pub mod BigIncGenesis { self.ownable.renounce_ownership(); } - // Governance Functions - fn submit_withdrawal_request( - ref self: ContractState, - token_address: ContractAddress, - amount: u256, - deadline_timestamp: u64, - milestone_uri: ByteArray, - ) -> u256 { - self.ownable.assert_only_owner(); - self.pausable.assert_not_paused(); - self._validate_token(token_address); - - assert(amount > 0, 'Amount must be > 0'); - assert(deadline_timestamp > get_block_timestamp(), 'Deadline must be in future'); - - let token = IERC20Dispatcher { contract_address: token_address }; - let contract_address = get_contract_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', - ); - - 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 request = WithdrawalRequest { - requester, - token_address, - amount, - deadline_timestamp, - milestone_uri, - expectation_hash, - created_timestamp: current_timestamp, - voting_deadline, - is_executed: false, - is_cancelled: false, - }; - - self.withdrawal_requests.write(request_id, request); - self.withdrawal_request_count.write(request_id + 1); - - self - .emit( - WithdrawalRequestSubmitted { - request_id, - requester, - token_address, - amount, - deadline_timestamp, - expectation_hash, - }, - ); - - request_id - } - - fn vote_on_withdrawal_request(ref self: ContractState, request_id: u256, approve: bool) { - 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); - 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'); - - // Check if already voted - assert(!self.votes.read((request_id, caller)), 'Already voted'); - - // Record the vote - self.votes.write((request_id, caller), true); - self.vote_choices.write((request_id, caller), approve); - - let vote_weight = self.shareholders.read(caller); - - self.emit(VoteCast { request_id, voter: caller, approve, vote_weight }); - } - - fn execute_withdrawal(ref self: ContractState, request_id: u256) { - self.pausable.assert_not_paused(); - self.reentrancy_guard.start(); - - let mut request = self.withdrawal_requests.read(request_id); - assert(!request.is_executed, 'Already executed'); - assert(!request.is_cancelled, 'Request cancelled'); - - // Check if voting period has ended - let current_timestamp = get_block_timestamp(); - assert(current_timestamp > request.voting_deadline, 'Voting period not ended'); - - // Also check execution deadline - assert( - current_timestamp >= request.deadline_timestamp, 'Execution deadline not reached', - ); - - let vote_status = self._calculate_vote_status(request_id); - assert(vote_status.quorum_reached, 'Quorum not reached'); - assert(vote_status.approved, 'Proposal not approved'); - - // 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, - }, - ); - - let token_address = request.token_address; - let amount = request.amount; - let requester = request.requester; - - request.is_executed = 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); - - let token = IERC20Dispatcher { contract_address: token_address }; - token.transfer(requester, amount); - - self.emit(WithdrawalExecuted { request_id, token_address, amount, requester }); - - self.reentrancy_guard.end(); - } - - fn cancel_withdrawal_request(ref self: ContractState, request_id: u256) { - self.ownable.assert_only_owner(); - self.pausable.assert_not_paused(); - self.reentrancy_guard.start(); - - let mut request = self.withdrawal_requests.read(request_id); - 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'); - - self.quorum_percentage.write(quorum_percentage); - self.voting_period_days.write(voting_period_days); - - self.emit(GovernanceParametersSet { quorum_percentage, voting_period_days }); - } - - // Governance View Functions fn get_withdrawal_request(self: @ContractState, request_id: u256) -> WithdrawalRequest { self.withdrawal_requests.read(request_id) } diff --git a/contract_/src/lib.cairo b/contract_/src/lib.cairo index 6f4b4fb..f923cbc 100644 --- a/contract_/src/lib.cairo +++ b/contract_/src/lib.cairo @@ -1,2 +1,5 @@ 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); +}