From ed453dd5eb7699b13ce2ae40b07c08701b4fb211 Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Fri, 30 May 2025 14:55:56 +0100 Subject: [PATCH 1/4] feat: implemented the milestone manager contract --- src/base/types.cairo | 3 +- src/budgetchain/Budget.cairo | 7 +- src/budgetchain/MilestoneManager.cairo | 232 ++++++++++++++++ src/interfaces/IMilestoneManager.cairo | 26 ++ src/lib.cairo | 4 + tests/test_milestone_manager.cairo | 364 +++++++++++++++++++++++++ 6 files changed, 633 insertions(+), 3 deletions(-) create mode 100644 src/budgetchain/MilestoneManager.cairo create mode 100644 src/interfaces/IMilestoneManager.cairo create mode 100644 tests/test_milestone_manager.cairo diff --git a/src/base/types.cairo b/src/base/types.cairo index 4fb856c..04ff9d8 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -49,8 +49,9 @@ pub struct Organization { #[derive(Copy, Drop, Serde, starknet::Store)] pub struct Milestone { - pub organization: ContractAddress, pub project_id: u64, + pub milestone_id: u64, + pub organization: ContractAddress, pub milestone_description: felt252, pub milestone_amount: u256, pub created_at: u64, diff --git a/src/budgetchain/Budget.cairo b/src/budgetchain/Budget.cairo index 11ccc27..f75bdb6 100644 --- a/src/budgetchain/Budget.cairo +++ b/src/budgetchain/Budget.cairo @@ -447,6 +447,7 @@ pub mod Budget { Milestone { organization: org, project_id: project_id, + milestone_id: j.into() + 1, milestone_description: *milestone_descriptions.at(j), milestone_amount: *milestone_amounts.at(j), created_at: get_block_timestamp(), @@ -526,17 +527,19 @@ pub mod Budget { let created_at = get_block_timestamp(); + // Read the number of the current milestones the organization has + let current_milestone = self.org_milestones.read(org); + let new_milestone: Milestone = Milestone { organization: org, project_id: project_id, + milestone_id: current_milestone + 1, milestone_description: milestone_description, milestone_amount: milestone_amount, created_at: created_at, completed: false, released: false, }; - // // read the number of the current milestones the organization has - let current_milestone = self.org_milestones.read(org); self.milestones.write((project_id, current_milestone + 1), new_milestone); self.org_milestones.write(org, current_milestone + 1); diff --git a/src/budgetchain/MilestoneManager.cairo b/src/budgetchain/MilestoneManager.cairo new file mode 100644 index 0000000..ab63e27 --- /dev/null +++ b/src/budgetchain/MilestoneManager.cairo @@ -0,0 +1,232 @@ +#[starknet::contract] +pub mod MilestoneManager { + use budgetchain_contracts::base::errors::*; + use budgetchain_contracts::base::types::{ADMIN_ROLE, Milestone, ORGANIZATION_ROLE, Project}; + use budgetchain_contracts::interfaces::IMilestoneManager::IMilestoneManager; + use core::array::{Array, ArrayTrait}; + use openzeppelin::access::accesscontrol::{AccessControlComponent, DEFAULT_ADMIN_ROLE}; + use openzeppelin::introspection::src5::SRC5Component; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ + ContractAddress, contract_address_const, get_block_timestamp, get_caller_address, + }; + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // AccessControl Mixin + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + // SRC5 Mixin + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + struct Storage { + admin: ContractAddress, + projects: Map, + milestones: Map<(u64, u64), Milestone>, // (project_id, milestone_id) -> Milestone + project_milestone_count: Map, // project_id -> count of milestones + is_paused: bool, + #[substorage(v0)] + accesscontrol: AccessControlComponent::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + MilestoneCreated: MilestoneCreated, + MilestoneCompleted: MilestoneCompleted, + #[flat] + AccessControlEvent: AccessControlComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event, + } + + #[derive(Drop, starknet::Event)] + pub struct MilestoneCreated { + pub organization: ContractAddress, + pub project_id: u64, + pub milestone_id: u64, + pub milestone_description: felt252, + pub milestone_amount: u256, + pub created_at: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct MilestoneCompleted { + pub project_id: u64, + pub milestone_id: u64, + } + + #[constructor] + fn constructor(ref self: ContractState, default_admin: ContractAddress) { + assert(default_admin != contract_address_const::<0>(), ERROR_ZERO_ADDRESS); + + // Initialize access control + self.accesscontrol.initializer(); + self.accesscontrol._grant_role(DEFAULT_ADMIN_ROLE, default_admin); + self.accesscontrol._grant_role(ADMIN_ROLE, default_admin); + + // Initialize contract storage + self.admin.write(default_admin); + self.is_paused.write(false); + } + + #[abi(embed_v0)] + impl MilestoneManagerImpl of IMilestoneManager { + fn create_milestone( + ref self: ContractState, + organization: ContractAddress, + project_id: u64, + milestone_description: felt252, + milestone_amount: u256, + ) -> u64 { + // Ensure the contract is not paused + self.assert_not_paused(); + + // Verify caller's authorization + let caller = get_caller_address(); + let admin = self.admin.read(); + + // Only admin or organization can create milestones + assert( + caller == admin + || self.accesscontrol.has_role(ORGANIZATION_ROLE, caller) + || caller == organization, + ERROR_UNAUTHORIZED, + ); + + // Verify project exists (this would be validated in a real implementation) + // For this implementation, we'll assume the project exists if it's a valid project_id + assert(project_id > 0, ERROR_INVALID_PROJECT_ID); + + // Generate new milestone ID + let milestone_id = self.project_milestone_count.read(project_id) + 1; + + // Create new milestone + let created_at = get_block_timestamp(); + let new_milestone = Milestone { + project_id, + milestone_id, + organization, + milestone_description, + milestone_amount, + created_at, + completed: false, + released: false, + }; + + // Store the milestone + self.milestones.write((project_id, milestone_id), new_milestone); + + // Update milestone count for the project + self.project_milestone_count.write(project_id, milestone_id); + + // Emit MilestoneCreated event + self + .emit( + Event::MilestoneCreated( + MilestoneCreated { + organization, + project_id, + milestone_id, + milestone_description, + milestone_amount, + created_at, + }, + ), + ); + + milestone_id + } + + fn set_milestone_complete(ref self: ContractState, project_id: u64, milestone_id: u64) { + // Ensure the contract is not paused + self.assert_not_paused(); + + // Get the milestone + let mut milestone = self.milestones.read((project_id, milestone_id)); + + // Verify the milestone exists for this project + assert(milestone.project_id == project_id, ERROR_INVALID_MILESTONE); + assert(milestone.milestone_id == milestone_id, ERROR_INVALID_MILESTONE); + + // Validate milestone status + assert(milestone.completed != true, ERROR_MILESTONE_ALREADY_COMPLETED); + + // Update the completed status + milestone.completed = true; + + // Write back to storage + self.milestones.write((project_id, milestone_id), milestone); + + // Emit the MilestoneCompleted event + self.emit(Event::MilestoneCompleted(MilestoneCompleted { project_id, milestone_id })); + } + + fn get_milestone(self: @ContractState, project_id: u64, milestone_id: u64) -> Milestone { + self.milestones.read((project_id, milestone_id)) + } + + fn get_project_milestones(self: @ContractState, project_id: u64) -> Array { + let mut milestones = ArrayTrait::new(); + let milestone_count = self.project_milestone_count.read(project_id); + + let mut i: u64 = 1; + while i <= milestone_count { + let milestone = self.milestones.read((project_id, i)); + milestones.append(milestone); + i += 1; + }; + + milestones + } + + fn get_admin(self: @ContractState) -> ContractAddress { + self.admin.read() + } + + fn is_paused(self: @ContractState) -> bool { + self.is_paused.read() + } + + fn pause_contract(ref self: ContractState) { + // Ensure only the admin can pause the contract + let caller = get_caller_address(); + assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); + + // Check if already paused + assert(!self.is_paused.read(), ERROR_ALREADY_PAUSED); + + // Set the paused state to true + self.is_paused.write(true); + } + + fn unpause_contract(ref self: ContractState) { + // Ensure only the admin can unpause the contract + let caller = get_caller_address(); + assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); + + // Set the paused state to false + self.is_paused.write(false); + } + } + + #[generate_trait] + pub impl Internal of InternalTrait { + // Internal view function + // - Takes `@self` as it only needs to read state + // - Can only be called by other functions within the contract + fn assert_not_paused(self: @ContractState) { + assert(!self.is_paused.read(), ERROR_CONTRACT_PAUSED); + } + } +} diff --git a/src/interfaces/IMilestoneManager.cairo b/src/interfaces/IMilestoneManager.cairo new file mode 100644 index 0000000..45b119d --- /dev/null +++ b/src/interfaces/IMilestoneManager.cairo @@ -0,0 +1,26 @@ +use budgetchain_contracts::base::types::Milestone; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IMilestoneManager { + // Milestone Management + fn create_milestone( + ref self: TContractState, + organization: ContractAddress, + project_id: u64, + milestone_description: felt252, + milestone_amount: u256, + ) -> u64; + + fn set_milestone_complete(ref self: TContractState, project_id: u64, milestone_id: u64); + + fn get_milestone(self: @TContractState, project_id: u64, milestone_id: u64) -> Milestone; + + fn get_project_milestones(self: @TContractState, project_id: u64) -> Array; + + // Admin functions + fn get_admin(self: @TContractState) -> ContractAddress; + fn is_paused(self: @TContractState) -> bool; + fn pause_contract(ref self: TContractState); + fn unpause_contract(ref self: TContractState); +} diff --git a/src/lib.cairo b/src/lib.cairo index b035bc4..6011d86 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -6,12 +6,16 @@ pub mod base { pub mod interfaces { pub mod IBudget; + pub mod IMilestoneManager; } pub mod budgetchain { pub mod Budget; + pub mod MilestoneManager; } // Re-export the main modules for easier access pub use budgetchain::Budget; +pub use budgetchain::MilestoneManager; pub use interfaces::IBudget; +pub use interfaces::IMilestoneManager; diff --git a/tests/test_milestone_manager.cairo b/tests/test_milestone_manager.cairo new file mode 100644 index 0000000..8722924 --- /dev/null +++ b/tests/test_milestone_manager.cairo @@ -0,0 +1,364 @@ +use budgetchain_contracts::base::errors::*; +use budgetchain_contracts::budgetchain::MilestoneManager::*; +use budgetchain_contracts::interfaces::IMilestoneManager::{IMilestoneManagerDispatcher, IMilestoneManagerDispatcherTrait}; +use core::array::ArrayTrait; +use core::result::ResultTrait; +use core::traits::Into; +use snforge_std::{ + CheatSpan, ContractClassTrait, DeclareResultTrait, EventSpyAssertionsTrait, + cheat_caller_address, declare, spy_events, +}; +use starknet::{ContractAddress, contract_address_const}; + +// Utility functions to create test contract addresses from a felt +fn ADMIN() -> ContractAddress { + contract_address_const::<'ADMIN'>() +} + +fn ORGANIZATION() -> ContractAddress { + contract_address_const::<'ORGANIZATION'>() +} + +fn OTHER_ORG() -> ContractAddress { + contract_address_const::<'OTHER_ORG'>() +} + +fn NON_ORG() -> ContractAddress { + contract_address_const::<'NON_ORG'>() +} + +fn PROJECT_OWNER() -> ContractAddress { + contract_address_const::<'PROJECT_OWNER'>() +} + +// Utility function to setup test data that can be destructured +fn setup_test_data() -> (u64, u256, felt252) { + ( + 1_u64, // project_id + 500_u256, // milestone_amount + 'Test milestone' // milestone_description + ) +} + +// Helper function to deploy the MilestoneManager contract +fn deploy_milestone_manager(admin: ContractAddress) -> (ContractAddress, IMilestoneManagerDispatcher) { + let contract_class = declare("MilestoneManager").unwrap().contract_class(); + + // Set up constructor calldata with admin address + let mut calldata: Array = ArrayTrait::new(); + calldata.append(admin.into()); + + // Deploy the contract + let (contract_address, _) = contract_class.deploy(@calldata).unwrap(); + + // Return the contract address and dispatcher + (contract_address, IMilestoneManagerDispatcher { contract_address }) +} + +#[test] +fn test_create_milestone() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Create milestone as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + + // Set up event spy + let mut spy = spy_events(); + + // Create milestone + let milestone_id = dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); + + // Verify milestone ID is correct + assert(milestone_id == 1, 'Incorrect milestone ID'); + + // Verify milestone was created correctly + let milestone = dispatcher.get_milestone(project_id, milestone_id); + assert(milestone.project_id == project_id, 'Wrong project ID'); + assert(milestone.milestone_id == milestone_id, 'Wrong milestone ID'); + assert(milestone.organization == org, 'Wrong organization'); + assert(milestone.milestone_description == milestone_description, 'Wrong description'); + assert(milestone.milestone_amount == milestone_amount, 'Wrong amount'); + assert(milestone.completed == false, 'Should not be completed'); + assert(milestone.released == false, 'Should not be released'); + + // Verify event was emitted + spy.assert_emitted( + @array![ + ( + contract_address, + MilestoneManager::Event::MilestoneCreated( + MilestoneManager::MilestoneCreated { + organization: org, + project_id, + milestone_id, + milestone_description, + milestone_amount, + created_at: milestone.created_at, + }, + ), + ), + ], + ); +} + +#[test] +fn test_create_multiple_milestones() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Create milestones as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(2)); + + // Create first milestone + let milestone_id1 = dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); + + // Create second milestone + let milestone_id2 = dispatcher.create_milestone( + org, project_id, 'Second milestone', milestone_amount * 2 + ); + + // Verify milestone IDs are correct + assert(milestone_id1 == 1, 'Incorrect first milestone ID'); + assert(milestone_id2 == 2, 'Incorrect second milestone ID'); + + // Verify milestones were created correctly + let milestone1 = dispatcher.get_milestone(project_id, milestone_id1); + let milestone2 = dispatcher.get_milestone(project_id, milestone_id2); + + assert(milestone1.milestone_description == milestone_description, 'Wrong description 1'); + assert(milestone1.milestone_amount == milestone_amount, 'Wrong amount 1'); + + assert(milestone2.milestone_description == 'Second milestone', 'Wrong description 2'); + assert(milestone2.milestone_amount == milestone_amount * 2, 'Wrong amount 2'); + + // Get all project milestones + let milestones = dispatcher.get_project_milestones(project_id); + assert(milestones.len() == 2, 'Wrong number of milestones'); +} + +#[test] +fn test_set_milestone_complete() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Create milestone as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + let milestone_id = dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); + + // Set up event spy + let mut spy = spy_events(); + + // Complete milestone as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.set_milestone_complete(project_id, milestone_id); + + // Verify milestone is marked as completed + let milestone = dispatcher.get_milestone(project_id, milestone_id); + assert(milestone.completed == true, 'Milestone not marked complete'); + assert(milestone.released == false, 'Released should still be false'); + + // Verify event was emitted + spy.assert_emitted( + @array![ + ( + contract_address, + MilestoneManager::Event::MilestoneCompleted( + MilestoneManager::MilestoneCompleted { + project_id, + milestone_id, + }, + ), + ), + ], + ); +} + +#[test] +#[should_panic(expected: 'Milestone already completed')] +fn test_cannot_complete_milestone_twice() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Create and complete milestone as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(2)); + let milestone_id = dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); + dispatcher.set_milestone_complete(project_id, milestone_id); + + // Try to complete the milestone again (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.set_milestone_complete(project_id, milestone_id); +} + +#[test] +#[should_panic(expected: 'Invalid milestone')] +fn test_cannot_complete_nonexistent_milestone() { + // Setup addresses + let admin = ADMIN(); + let _org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, _, _) = setup_test_data(); + let nonexistent_milestone_id = 999_u64; + + // Try to complete a nonexistent milestone (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.set_milestone_complete(project_id, nonexistent_milestone_id); +} + +#[test] +#[should_panic(expected: 'Caller not authorized')] +fn test_unauthorized_cannot_create_milestone() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + let non_org = NON_ORG(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Try to create milestone as non-org (should fail) + cheat_caller_address(contract_address, non_org, CheatSpan::TargetCalls(1)); + dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); +} + +#[test] +fn test_pause_and_unpause_contract() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Verify contract is not paused initially + assert(dispatcher.is_paused() == false, 'Contract should not be paused'); + + // Pause contract as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.pause_contract(); + + // Verify contract is paused + assert(dispatcher.is_paused() == true, 'Contract should be paused'); + + // Unpause contract as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.unpause_contract(); + + // Verify contract is unpaused + assert(dispatcher.is_paused() == false, 'Contract should be unpaused'); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Create milestone after unpausing (should succeed) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + let milestone_id = dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); + + // Verify milestone was created + assert(milestone_id == 1, 'Milestone should be created'); +} + +#[test] +#[should_panic(expected: 'Contract is paused')] +fn test_cannot_create_milestone_when_paused() { + // Setup addresses + let admin = ADMIN(); + let org = ORGANIZATION(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Setup test data + let (project_id, milestone_amount, milestone_description) = setup_test_data(); + + // Pause contract as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.pause_contract(); + + // Try to create milestone while paused (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.create_milestone( + org, project_id, milestone_description, milestone_amount + ); +} + +#[test] +#[should_panic(expected: 'ONLY ADMIN')] +fn test_only_admin_can_pause_contract() { + // Setup addresses + let admin = ADMIN(); + let _non_admin = NON_ORG(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Try to pause contract as non-admin (should fail) + cheat_caller_address(contract_address, _non_admin, CheatSpan::TargetCalls(1)); + dispatcher.pause_contract(); +} + +#[test] +#[should_panic(expected: 'ONLY ADMIN')] +fn test_only_admin_can_unpause_contract() { + // Setup addresses + let admin = ADMIN(); + let non_admin = NON_ORG(); + + // Deploy contract + let (contract_address, dispatcher) = deploy_milestone_manager(admin); + + // Pause contract as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); + dispatcher.pause_contract(); + + // Try to unpause contract as non-admin (should fail) + cheat_caller_address(contract_address, non_admin, CheatSpan::TargetCalls(1)); + dispatcher.unpause_contract(); +} From 741587aea4e685406f5a1a7cd6fe821d23a5e5cf Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Fri, 30 May 2025 15:03:46 +0100 Subject: [PATCH 2/4] feat: implementation cleanup --- src/budgetchain/MilestoneManager.cairo | 32 ++-- tests/test_milestone_manager.cairo | 214 ++++++++++--------------- 2 files changed, 96 insertions(+), 150 deletions(-) diff --git a/src/budgetchain/MilestoneManager.cairo b/src/budgetchain/MilestoneManager.cairo index ab63e27..7c8d02e 100644 --- a/src/budgetchain/MilestoneManager.cairo +++ b/src/budgetchain/MilestoneManager.cairo @@ -96,7 +96,6 @@ pub mod MilestoneManager { let caller = get_caller_address(); let admin = self.admin.read(); - // Only admin or organization can create milestones assert( caller == admin || self.accesscontrol.has_role(ORGANIZATION_ROLE, caller) @@ -104,8 +103,7 @@ pub mod MilestoneManager { ERROR_UNAUTHORIZED, ); - // Verify project exists (this would be validated in a real implementation) - // For this implementation, we'll assume the project exists if it's a valid project_id + // Verify project exists with a valid ID assert(project_id > 0, ERROR_INVALID_PROJECT_ID); // Generate new milestone ID @@ -124,13 +122,13 @@ pub mod MilestoneManager { released: false, }; - // Store the milestone + self.milestones.write((project_id, milestone_id), new_milestone); - // Update milestone count for the project + self.project_milestone_count.write(project_id, milestone_id); - // Emit MilestoneCreated event + self .emit( Event::MilestoneCreated( @@ -152,23 +150,23 @@ pub mod MilestoneManager { // Ensure the contract is not paused self.assert_not_paused(); - // Get the milestone + let mut milestone = self.milestones.read((project_id, milestone_id)); - // Verify the milestone exists for this project + assert(milestone.project_id == project_id, ERROR_INVALID_MILESTONE); assert(milestone.milestone_id == milestone_id, ERROR_INVALID_MILESTONE); - // Validate milestone status + assert(milestone.completed != true, ERROR_MILESTONE_ALREADY_COMPLETED); - // Update the completed status + milestone.completed = true; - // Write back to storage + self.milestones.write((project_id, milestone_id), milestone); - // Emit the MilestoneCompleted event + self.emit(Event::MilestoneCompleted(MilestoneCompleted { project_id, milestone_id })); } @@ -199,23 +197,23 @@ pub mod MilestoneManager { } fn pause_contract(ref self: ContractState) { - // Ensure only the admin can pause the contract + let caller = get_caller_address(); assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); - // Check if already paused + assert(!self.is_paused.read(), ERROR_ALREADY_PAUSED); - // Set the paused state to true + self.is_paused.write(true); } fn unpause_contract(ref self: ContractState) { - // Ensure only the admin can unpause the contract + let caller = get_caller_address(); assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); - // Set the paused state to false + self.is_paused.write(false); } } diff --git a/tests/test_milestone_manager.cairo b/tests/test_milestone_manager.cairo index 8722924..51ae95d 100644 --- a/tests/test_milestone_manager.cairo +++ b/tests/test_milestone_manager.cairo @@ -1,6 +1,8 @@ use budgetchain_contracts::base::errors::*; use budgetchain_contracts::budgetchain::MilestoneManager::*; -use budgetchain_contracts::interfaces::IMilestoneManager::{IMilestoneManagerDispatcher, IMilestoneManagerDispatcherTrait}; +use budgetchain_contracts::interfaces::IMilestoneManager::{ + IMilestoneManagerDispatcher, IMilestoneManagerDispatcherTrait, +}; use core::array::ArrayTrait; use core::result::ResultTrait; use core::traits::Into; @@ -10,7 +12,7 @@ use snforge_std::{ }; use starknet::{ContractAddress, contract_address_const}; -// Utility functions to create test contract addresses from a felt + fn ADMIN() -> ContractAddress { contract_address_const::<'ADMIN'>() } @@ -31,37 +33,33 @@ fn PROJECT_OWNER() -> ContractAddress { contract_address_const::<'PROJECT_OWNER'>() } -// Utility function to setup test data that can be destructured + fn setup_test_data() -> (u64, u256, felt252) { - ( - 1_u64, // project_id - 500_u256, // milestone_amount - 'Test milestone' // milestone_description + (1_u64, // project_id + 500_u256, // milestone_amount + 'Test milestone' // milestone_description ) } -// Helper function to deploy the MilestoneManager contract -fn deploy_milestone_manager(admin: ContractAddress) -> (ContractAddress, IMilestoneManagerDispatcher) { + +fn deploy_milestone_manager( + admin: ContractAddress, +) -> (ContractAddress, IMilestoneManagerDispatcher) { let contract_class = declare("MilestoneManager").unwrap().contract_class(); - // Set up constructor calldata with admin address let mut calldata: Array = ArrayTrait::new(); calldata.append(admin.into()); - // Deploy the contract let (contract_address, _) = contract_class.deploy(@calldata).unwrap(); - // Return the contract address and dispatcher (contract_address, IMilestoneManagerDispatcher { contract_address }) } #[test] fn test_create_milestone() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data @@ -69,19 +67,16 @@ fn test_create_milestone() { // Create milestone as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); - + // Set up event spy let mut spy = spy_events(); - + // Create milestone - let milestone_id = dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); - - // Verify milestone ID is correct + let milestone_id = dispatcher + .create_milestone(org, project_id, milestone_description, milestone_amount); + assert(milestone_id == 1, 'Incorrect milestone ID'); - - // Verify milestone was created correctly + let milestone = dispatcher.get_milestone(project_id, milestone_id); assert(milestone.project_id == project_id, 'Wrong project ID'); assert(milestone.milestone_id == milestone_id, 'Wrong milestone ID'); @@ -90,34 +85,33 @@ fn test_create_milestone() { assert(milestone.milestone_amount == milestone_amount, 'Wrong amount'); assert(milestone.completed == false, 'Should not be completed'); assert(milestone.released == false, 'Should not be released'); - + // Verify event was emitted - spy.assert_emitted( - @array![ - ( - contract_address, - MilestoneManager::Event::MilestoneCreated( - MilestoneManager::MilestoneCreated { - organization: org, - project_id, - milestone_id, - milestone_description, - milestone_amount, - created_at: milestone.created_at, - }, + spy + .assert_emitted( + @array![ + ( + contract_address, + MilestoneManager::Event::MilestoneCreated( + MilestoneManager::MilestoneCreated { + organization: org, + project_id, + milestone_id, + milestone_description, + milestone_amount, + created_at: milestone.created_at, + }, + ), ), - ), - ], - ); + ], + ); } #[test] fn test_create_multiple_milestones() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data @@ -125,43 +119,34 @@ fn test_create_multiple_milestones() { // Create milestones as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(2)); - - // Create first milestone - let milestone_id1 = dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); - - // Create second milestone - let milestone_id2 = dispatcher.create_milestone( - org, project_id, 'Second milestone', milestone_amount * 2 - ); - - // Verify milestone IDs are correct + + let milestone_id1 = dispatcher + .create_milestone(org, project_id, milestone_description, milestone_amount); + + let milestone_id2 = dispatcher + .create_milestone(org, project_id, 'Second milestone', milestone_amount * 2); + assert(milestone_id1 == 1, 'Incorrect first milestone ID'); assert(milestone_id2 == 2, 'Incorrect second milestone ID'); - - // Verify milestones were created correctly + let milestone1 = dispatcher.get_milestone(project_id, milestone_id1); let milestone2 = dispatcher.get_milestone(project_id, milestone_id2); - + assert(milestone1.milestone_description == milestone_description, 'Wrong description 1'); assert(milestone1.milestone_amount == milestone_amount, 'Wrong amount 1'); - + assert(milestone2.milestone_description == 'Second milestone', 'Wrong description 2'); assert(milestone2.milestone_amount == milestone_amount * 2, 'Wrong amount 2'); - - // Get all project milestones + let milestones = dispatcher.get_project_milestones(project_id); assert(milestones.len() == 2, 'Wrong number of milestones'); } #[test] fn test_set_milestone_complete() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data @@ -169,59 +154,49 @@ fn test_set_milestone_complete() { // Create milestone as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); - let milestone_id = dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); - + let milestone_id = dispatcher + .create_milestone(org, project_id, milestone_description, milestone_amount); + // Set up event spy let mut spy = spy_events(); - - // Complete milestone as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.set_milestone_complete(project_id, milestone_id); - - // Verify milestone is marked as completed + let milestone = dispatcher.get_milestone(project_id, milestone_id); assert(milestone.completed == true, 'Milestone not marked complete'); assert(milestone.released == false, 'Released should still be false'); - + // Verify event was emitted - spy.assert_emitted( - @array![ - ( - contract_address, - MilestoneManager::Event::MilestoneCompleted( - MilestoneManager::MilestoneCompleted { - project_id, - milestone_id, - }, + spy + .assert_emitted( + @array![ + ( + contract_address, + MilestoneManager::Event::MilestoneCompleted( + MilestoneManager::MilestoneCompleted { project_id, milestone_id }, + ), ), - ), - ], - ); + ], + ); } #[test] #[should_panic(expected: 'Milestone already completed')] fn test_cannot_complete_milestone_twice() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data let (project_id, milestone_amount, milestone_description) = setup_test_data(); - // Create and complete milestone as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(2)); - let milestone_id = dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); + let milestone_id = dispatcher + .create_milestone(org, project_id, milestone_description, milestone_amount); dispatcher.set_milestone_complete(project_id, milestone_id); - - // Try to complete the milestone again (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.set_milestone_complete(project_id, milestone_id); } @@ -229,18 +204,15 @@ fn test_cannot_complete_milestone_twice() { #[test] #[should_panic(expected: 'Invalid milestone')] fn test_cannot_complete_nonexistent_milestone() { - // Setup addresses let admin = ADMIN(); let _org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data let (project_id, _, _) = setup_test_data(); let nonexistent_milestone_id = 999_u64; - - // Try to complete a nonexistent milestone (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.set_milestone_complete(project_id, nonexistent_milestone_id); } @@ -248,59 +220,46 @@ fn test_cannot_complete_nonexistent_milestone() { #[test] #[should_panic(expected: 'Caller not authorized')] fn test_unauthorized_cannot_create_milestone() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); let non_org = NON_ORG(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Setup test data let (project_id, milestone_amount, milestone_description) = setup_test_data(); - // Try to create milestone as non-org (should fail) cheat_caller_address(contract_address, non_org, CheatSpan::TargetCalls(1)); - dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); + dispatcher.create_milestone(org, project_id, milestone_description, milestone_amount); } #[test] fn test_pause_and_unpause_contract() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); - // Verify contract is not paused initially assert(dispatcher.is_paused() == false, 'Contract should not be paused'); // Pause contract as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.pause_contract(); - - // Verify contract is paused + assert(dispatcher.is_paused() == true, 'Contract should be paused'); - - // Unpause contract as admin + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.unpause_contract(); - - // Verify contract is unpaused + assert(dispatcher.is_paused() == false, 'Contract should be unpaused'); - + // Setup test data let (project_id, milestone_amount, milestone_description) = setup_test_data(); - - // Create milestone after unpausing (should succeed) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); - let milestone_id = dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); - + let milestone_id = dispatcher + .create_milestone(org, project_id, milestone_description, milestone_amount); + // Verify milestone was created assert(milestone_id == 1, 'Milestone should be created'); } @@ -308,38 +267,30 @@ fn test_pause_and_unpause_contract() { #[test] #[should_panic(expected: 'Contract is paused')] fn test_cannot_create_milestone_when_paused() { - // Setup addresses let admin = ADMIN(); let org = ORGANIZATION(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); - + // Setup test data let (project_id, milestone_amount, milestone_description) = setup_test_data(); - + // Pause contract as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.pause_contract(); - - // Try to create milestone while paused (should fail) + cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); - dispatcher.create_milestone( - org, project_id, milestone_description, milestone_amount - ); + dispatcher.create_milestone(org, project_id, milestone_description, milestone_amount); } #[test] #[should_panic(expected: 'ONLY ADMIN')] fn test_only_admin_can_pause_contract() { - // Setup addresses let admin = ADMIN(); let _non_admin = NON_ORG(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); - // Try to pause contract as non-admin (should fail) cheat_caller_address(contract_address, _non_admin, CheatSpan::TargetCalls(1)); dispatcher.pause_contract(); } @@ -347,18 +298,15 @@ fn test_only_admin_can_pause_contract() { #[test] #[should_panic(expected: 'ONLY ADMIN')] fn test_only_admin_can_unpause_contract() { - // Setup addresses let admin = ADMIN(); let non_admin = NON_ORG(); - // Deploy contract let (contract_address, dispatcher) = deploy_milestone_manager(admin); // Pause contract as admin cheat_caller_address(contract_address, admin, CheatSpan::TargetCalls(1)); dispatcher.pause_contract(); - - // Try to unpause contract as non-admin (should fail) + cheat_caller_address(contract_address, non_admin, CheatSpan::TargetCalls(1)); dispatcher.unpause_contract(); } From 41b1e1ba2d7a26c1f237a7499d50b7efc20f1d59 Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Sat, 31 May 2025 20:31:51 +0100 Subject: [PATCH 3/4] fixed fmt error --- src/budgetchain/MilestoneManager.cairo | 31 +------------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/budgetchain/MilestoneManager.cairo b/src/budgetchain/MilestoneManager.cairo index 7c8d02e..08d5afb 100644 --- a/src/budgetchain/MilestoneManager.cairo +++ b/src/budgetchain/MilestoneManager.cairo @@ -121,14 +121,8 @@ pub mod MilestoneManager { completed: false, released: false, }; - - self.milestones.write((project_id, milestone_id), new_milestone); - - self.project_milestone_count.write(project_id, milestone_id); - - self .emit( Event::MilestoneCreated( @@ -142,31 +136,18 @@ pub mod MilestoneManager { }, ), ); - milestone_id } fn set_milestone_complete(ref self: ContractState, project_id: u64, milestone_id: u64) { // Ensure the contract is not paused self.assert_not_paused(); - - let mut milestone = self.milestones.read((project_id, milestone_id)); - - assert(milestone.project_id == project_id, ERROR_INVALID_MILESTONE); assert(milestone.milestone_id == milestone_id, ERROR_INVALID_MILESTONE); - - assert(milestone.completed != true, ERROR_MILESTONE_ALREADY_COMPLETED); - - milestone.completed = true; - - self.milestones.write((project_id, milestone_id), milestone); - - self.emit(Event::MilestoneCompleted(MilestoneCompleted { project_id, milestone_id })); } @@ -177,14 +158,12 @@ pub mod MilestoneManager { fn get_project_milestones(self: @ContractState, project_id: u64) -> Array { let mut milestones = ArrayTrait::new(); let milestone_count = self.project_milestone_count.read(project_id); - let mut i: u64 = 1; while i <= milestone_count { let milestone = self.milestones.read((project_id, i)); milestones.append(milestone); i += 1; - }; - + } milestones } @@ -197,23 +176,15 @@ pub mod MilestoneManager { } fn pause_contract(ref self: ContractState) { - let caller = get_caller_address(); assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); - - assert(!self.is_paused.read(), ERROR_ALREADY_PAUSED); - - self.is_paused.write(true); } fn unpause_contract(ref self: ContractState) { - let caller = get_caller_address(); assert(caller == self.admin.read(), ERROR_ONLY_ADMIN); - - self.is_paused.write(false); } } From cb6ea7fc6823b0fd01e627547a2b6fad41c225f5 Mon Sep 17 00:00:00 2001 From: caxtonacollins Date: Sat, 31 May 2025 20:43:31 +0100 Subject: [PATCH 4/4] fixed fmt error --- src/budgetchain/MilestoneManager.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/budgetchain/MilestoneManager.cairo b/src/budgetchain/MilestoneManager.cairo index 08d5afb..0ff0552 100644 --- a/src/budgetchain/MilestoneManager.cairo +++ b/src/budgetchain/MilestoneManager.cairo @@ -163,7 +163,7 @@ pub mod MilestoneManager { let milestone = self.milestones.read((project_id, i)); milestones.append(milestone); i += 1; - } + }; milestones }