diff --git a/src/base/errors.cairo b/src/base/errors.cairo index 2518d4f..b480de1 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -31,3 +31,5 @@ pub const ERROR_MILESTONE_NOT_COMPLETED: felt252 = 'Milestone not completed'; pub const ERROR_UNAUTHORIZED_REQUESTER: felt252 = 'Only project owner can request'; pub const ERROR_CONTRACT_PAUSED: felt252 = 'Contract is paused'; pub const ERROR_ALREADY_PAUSED: felt252 = 'Contract already paused'; +pub const ERROR_INVALID_MILESTONE_DESCRIPTION: felt252 = 'Invalid milestone description'; +pub const ERROR_INVALID_BUDGET: felt252 = 'Invalid budget'; diff --git a/src/budgetchain/Budget.cairo b/src/budgetchain/Budget.cairo index 6302979..11ccc27 100644 --- a/src/budgetchain/Budget.cairo +++ b/src/budgetchain/Budget.cairo @@ -138,7 +138,7 @@ pub mod Budget { #[derive(Drop, starknet::Event)] pub struct MilestoneCreated { - pub organization: u256, + pub organization: ContractAddress, pub project_id: u64, pub milestone_description: felt252, pub milestone_amount: u256, @@ -407,6 +407,10 @@ pub mod Budget { milestone_descriptions: Array, milestone_amounts: Array, ) -> u64 { + assert(project_owner != contract_address_const::<0>(), ERROR_ZERO_ADDRESS); + assert(milestone_descriptions.len() > 0, ERROR_INVALID_MILESTONE_DESCRIPTION); + assert(milestone_amounts.len() > 0, ERROR_INVALID_MILESTONE_DESCRIPTION); + assert(total_budget > 0, ERROR_INVALID_BUDGET); // Ensure the contract is not paused self.assert_not_paused(); @@ -426,7 +430,7 @@ pub mod Budget { }; assert(sum == total_budget, ERROR_BUDGET_MISMATCH); - let project_id = self.project_count.read(); + let project_id = self.project_count.read() + 1; let new_project = Project { id: project_id, org: org, owner: project_owner, total_budget: total_budget, @@ -450,9 +454,20 @@ pub mod Budget { released: false, }, ); + self + .emit( + MilestoneCreated { + organization: org, + project_id: project_id, + milestone_description: *milestone_descriptions.at(j), + milestone_amount: *milestone_amounts.at(j), + created_at: get_block_timestamp(), + }, + ); j += 1; }; + self.project_owners.write(project_id, project_owner); self.project_count.write(project_id + 1); self.org_milestones.write(org, milestone_count.try_into().unwrap()); diff --git a/tests/test_budgetchain.cairo b/tests/test_budgetchain.cairo index 669ec3b..1ee7620 100644 --- a/tests/test_budgetchain.cairo +++ b/tests/test_budgetchain.cairo @@ -4,9 +4,9 @@ use budgetchain_contracts::interfaces::IBudget::{IBudgetDispatcher, IBudgetDispa use snforge_std::{ CheatSpan, ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, cheat_caller_address, declare, spy_events, start_cheat_caller_address, - stop_cheat_caller_address, + stop_cheat_caller_address, start_cheat_block_timestamp, }; -use starknet::{ContractAddress, contract_address_const}; +use starknet::{ContractAddress, contract_address_const, get_block_timestamp}; fn setup() -> (ContractAddress, ContractAddress) { let admin_address: ContractAddress = contract_address_const::<'admin'>(); @@ -948,7 +948,8 @@ fn test_get_project_remaining_budget_multiple_releases() { } #[test] -fn test_get_project_remaining_budget_zero_funding() { +#[should_panic(expected: 'Milestone sum != total budget')] +fn test_get_project_invalid_total_budget() { // Setup project with no budget let (contract_address, admin_address) = setup(); let dispatcher = IBudgetDispatcher { contract_address }; @@ -967,11 +968,13 @@ fn test_get_project_remaining_budget_zero_funding() { stop_cheat_caller_address(admin_address); // Create project with zero budget - let total_budget: u256 = 0; + let total_budget: u256 = 80; // Set milestone descriptions and amounts - let mut milestone_descriptions = array!['Empty Milestone']; - let mut milestone_amounts = array![total_budget]; + let mut milestone_descriptions = array![ + 'Empty Milestone', 'non empty milestone', 'something else', + ]; + let mut milestone_amounts = array![25, 59, 30]; // Set org_address as caller to create project cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); @@ -986,6 +989,47 @@ fn test_get_project_remaining_budget_zero_funding() { assert(remaining_budget == 0, 'Zero budget incorrect'); } +#[test] +fn test_get_project_remaining_budget_zero_funding() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<'ProjectOwner'>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let _org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with zero budget + let total_budget: u256 = 114; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array![ + 'Empty Milestone', 'non empty milestone', 'something else', + ]; + let mut milestone_amounts = array![25, 59, 30]; + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + let project_id = dispatcher + .allocate_project_budget( + org_address, project_owner, total_budget, milestone_descriptions, milestone_amounts, + ); + stop_cheat_caller_address(org_address); + + // Test that remaining budget is zero + let remaining_budget = dispatcher.get_project_remaining_budget(project_id); + assert(remaining_budget == 114, 'Zero budget incorrect'); +} + #[test] fn test_get_project_remaining_budget_full_utilization() { // Setup project with milestones @@ -1221,7 +1265,7 @@ fn test_functions_should_panic_when_contract_is_done() { stop_cheat_caller_address(admin_address); // Create project with zero budget - let total_budget: u256 = 0; + let total_budget: u256 = 20; // Set milestone descriptions and amounts let mut milestone_descriptions = array!['Empty Milestone']; @@ -1287,6 +1331,7 @@ fn test_get_project_budget_full_utilization() { } #[test] +#[should_panic(expected: 'Invalid budget')] fn test_get_project_budget_zero_funding() { // Setup project with no budget let (contract_address, admin_address) = setup(); @@ -1721,3 +1766,217 @@ fn test_remove_organization_event_emission() { ], ); } + +#[test] +#[should_panic(expected: 'Zero address forbidden')] +fn test_allocate_project_budget_zero_address() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<0>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let _org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with zero budget + let total_budget: u256 = 40; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array!['Empty Milestone']; + let mut milestone_amounts = array![total_budget]; + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + dispatcher + .allocate_project_budget( + org_address, project_owner, total_budget, milestone_descriptions, milestone_amounts, + ); + stop_cheat_caller_address(org_address); +} + +#[test] +#[should_panic(expected: 'Invalid milestone description')] +fn test_allocate_project_budget_zero_milestone_description() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<'owner'>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let _org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with zero budget + let total_budget: u256 = 0; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array![]; + let mut milestone_amounts = array![total_budget]; + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + dispatcher + .allocate_project_budget( + org_address, project_owner, total_budget, milestone_descriptions, milestone_amounts, + ); + stop_cheat_caller_address(org_address); +} + +#[test] +#[should_panic(expected: 'Invalid milestone description')] +fn test_allocate_project_budget_zero_milestone_amount() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<'owner'>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let _org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with zero budget + let total_budget: u256 = 60; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array!['milestone description']; + let mut milestone_amounts = array![]; + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + dispatcher + .allocate_project_budget( + org_address, project_owner, total_budget, milestone_descriptions, milestone_amounts, + ); + stop_cheat_caller_address(org_address); +} + +#[test] +#[should_panic(expected: 'Invalid budget')] +fn test_allocate_project_budget_Invalid_budget() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<'owner'>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let _org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with zero budget + let total_budget: u256 = 0; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array!['milestone description']; + let mut milestone_amounts = array![total_budget, 54, 56]; + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + dispatcher + .allocate_project_budget( + org_address, project_owner, total_budget, milestone_descriptions, milestone_amounts, + ); + stop_cheat_caller_address(org_address); +} + +#[test] +fn test_allocate_project_event() { + // Setup project with no budget + let (contract_address, admin_address) = setup(); + let dispatcher = IBudgetDispatcher { contract_address }; + + // Create organization + let org_name = 'Test Org'; + let org_address = contract_address_const::<'Organization'>(); + let org_mission = 'Testing Budget Chain'; + + // Create project owner + let project_owner = contract_address_const::<'owner'>(); + + // Set admin as caller to create organization + cheat_caller_address(contract_address, admin_address, CheatSpan::Indefinite); + let org_id = dispatcher.create_organization(org_name, org_address, org_mission); + stop_cheat_caller_address(admin_address); + + // Create project with budget + let total_budget: u256 = 120; + + // Set milestone descriptions and amounts + let mut milestone_descriptions = array![ + 'milestone description', 'non milestone description', 'yeah', + ]; + let mut milestone_amounts = array![10, 54, 56]; + + // Set block timestamp to a known value (default is 0) + // You're not currently setting this in your test, so get_block_timestamp() might be returning 0 + start_cheat_block_timestamp(contract_address, 500); + + // Start spy BEFORE calling functions that emit events + let mut spy = spy_events(); + + // Set org_address as caller to create project + cheat_caller_address(contract_address, org_address, CheatSpan::Indefinite); + + let project_id = dispatcher + .allocate_project_budget( + org_address, + project_owner, + total_budget, + milestone_descriptions.clone(), + milestone_amounts.clone(), + ); + + // Check for MilestoneCreated event with correct timestamp (500) + spy + .assert_emitted( + @array![ + ( + contract_address, + Budget::Budget::Event::MilestoneCreated( + Budget::Budget::MilestoneCreated { + organization: org_address, + project_id: project_id, + milestone_description: *milestone_descriptions.at(0), + milestone_amount: *milestone_amounts.at(0), + created_at: 500 // Must match the set timestamp + }, + ), + ), + ], + ); + + stop_cheat_caller_address(org_address); +} +