diff --git a/dongle-smartcontract/src/errors.rs b/dongle-smartcontract/src/errors.rs index ae12e6f..1a43c5c 100644 --- a/dongle-smartcontract/src/errors.rs +++ b/dongle-smartcontract/src/errors.rs @@ -1,5 +1,4 @@ use soroban_sdk::contracterror; - /// Error types for the Dongle smart contract #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -53,7 +52,10 @@ pub enum ContractError { InvalidProjectDescriptionFormat = 23, /// User has exceeded maximum number of projects allowed MaxProjectsExceeded = 24, + /// Fee already paid for this project in the current verification cycle + FeeAlreadyPaid = 25, + /// Token provided does not match the configured payment token + InvalidToken = 26, } - // Legacy alias to avoid breaking any code that uses `Error` directly -pub type Error = ContractError; +pub type Error = ContractError; \ No newline at end of file diff --git a/dongle-smartcontract/src/fee_manager.rs b/dongle-smartcontract/src/fee_manager.rs index 9f0b494..94c857d 100644 --- a/dongle-smartcontract/src/fee_manager.rs +++ b/dongle-smartcontract/src/fee_manager.rs @@ -52,8 +52,12 @@ impl FeeManager { .get(&StorageKey::Treasury) .ok_or(ContractError::TreasuryNotSet)?; + if Self::is_fee_paid(env, project_id) { + return Err(ContractError::FeeAlreadyPaid); + } + if config.token != token { - return Err(ContractError::InvalidProjectData); + return Err(ContractError::InvalidToken); } let amount = config.verification_fee; diff --git a/dongle-smartcontract/src/tests/fixtures.rs b/dongle-smartcontract/src/tests/fixtures.rs index cdfe5a7..aaf3763 100644 --- a/dongle-smartcontract/src/tests/fixtures.rs +++ b/dongle-smartcontract/src/tests/fixtures.rs @@ -11,7 +11,7 @@ use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec}; /// /// This is the most basic setup function used by most tests. pub fn setup_contract(env: &Env) -> (DongleContractClient<'_>, Address) { - let contract_id = env.register(DongleContract, ()); + let contract_id = env.register_contract(None, DongleContract); let client = DongleContractClient::new(env, &contract_id); let admin = Address::generate(env); @@ -87,4 +87,4 @@ pub fn assert_project_state( assert_eq!(project.name, String::from_str(env, expected_name)); assert_eq!(project.owner, *expected_owner); assert_eq!(project.verification_status, expected_status); -} +} \ No newline at end of file diff --git a/dongle-smartcontract/src/tests/verification.rs b/dongle-smartcontract/src/tests/verification.rs index 9b33721..29effdd 100644 --- a/dongle-smartcontract/src/tests/verification.rs +++ b/dongle-smartcontract/src/tests/verification.rs @@ -9,8 +9,10 @@ use soroban_sdk::{testutils::Address as _, Address, Env, String}; fn setup(env: &Env) -> (DongleContractClient<'_>, Address, Address) { let contract_id = env.register(DongleContract, ()); let client = DongleContractClient::new(env, &contract_id); + let admin = Address::generate(env); client.initialize(&admin); + (client, admin, Address::generate(env)) } @@ -30,25 +32,24 @@ fn setup_project_with_fee( logo_cid: None, metadata_cid: None, }; + let project_id = client.register_project(¶ms); - // Set up fee configuration let token_admin = Address::generate(env); let token_address = env .register_stellar_asset_contract_v2(token_admin) .address(); + client.set_fee(admin, &Some(token_address.clone()), &100, admin); - // Mint tokens and pay fee let token_client = soroban_sdk::token::StellarAssetClient::new(env, &token_address); token_client.mint(owner, &1000); + client.pay_fee(owner, &project_id, &Some(token_address)); project_id } -// --- Basic Verification Lifecycle Tests --- - #[test] fn test_verification_lifecycle() { let env = Env::default(); @@ -64,29 +65,24 @@ fn test_verification_lifecycle() { logo_cid: None, metadata_cid: None, }; + let project_id = client.register_project(¶ms); - // 1. Initially unverified let project = client.get_project(&project_id).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Unverified); - // 2. Set fee (using admin) - client.set_fee(&admin, &None, &100, &admin); - - // 3. Pay fee (using owner) let token_admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(token_admin) .address(); + client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); - // Mock token balance for owner let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); token_client.mint(&owner, &1000); - client.pay_fee(&owner, &project_id, &Some(token_address.clone())); + client.pay_fee(&owner, &project_id, &Some(token_address)); - // 4. Request verification client.request_verification( &project_id, &owner, @@ -96,7 +92,6 @@ fn test_verification_lifecycle() { let project = client.get_project(&project_id).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Pending); - // 5. Approve verification (using admin) client.approve_verification(&project_id, &admin); let project = client.get_project(&project_id).unwrap(); @@ -109,26 +104,7 @@ fn test_reject_verification() { env.mock_all_auths(); let (client, admin, owner) = setup(&env); - let params = ProjectRegistrationParams { - owner: owner.clone(), - name: String::from_str(&env, "Project Y"), - description: String::from_str(&env, "Description... Description... Description..."), - category: String::from_str(&env, "NFT"), - website: None, - logo_cid: None, - metadata_cid: None, - }; - let project_id = client.register_project(¶ms); - - // Set fee and pay - let token_admin = Address::generate(&env); - let token_address = env - .register_stellar_asset_contract_v2(token_admin) - .address(); - let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); - token_client.mint(&owner, &100); - client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); - client.pay_fee(&owner, &project_id, &Some(token_address)); + let project_id = setup_project_with_fee(&client, &env, &admin, &owner, "Project Y"); client.request_verification( &project_id, @@ -136,22 +112,18 @@ fn test_reject_verification() { &String::from_str(&env, "ipfs://evidence"), ); - // Reject client.reject_verification(&project_id, &admin); let project = client.get_project(&project_id).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Rejected); } -// --- State Machine Transition Tests --- - #[test] fn test_valid_state_transitions() { let env = Env::default(); env.mock_all_auths(); let (client, admin, owner) = setup(&env); - // Test 1: Unverified -> Pending (verification request) let project_id = setup_project_with_fee(&client, &env, &admin, &owner, "Project 1"); let project = client.get_project(&project_id).unwrap(); @@ -163,15 +135,18 @@ fn test_valid_state_transitions() { &String::from_str(&env, "ipfs://evidence1"), ); - let project = client.get_project(&project_id).unwrap(); - assert_eq!(project.verification_status, VerificationStatus::Pending); + assert_eq!( + client.get_project(&project_id).unwrap().verification_status, + VerificationStatus::Pending + ); - // Test 2: Pending -> Verified (admin approval) client.approve_verification(&project_id, &admin); - let project = client.get_project(&project_id).unwrap(); - assert_eq!(project.verification_status, VerificationStatus::Verified); - // Test 3: Rejected -> Pending (re-request verification) + assert_eq!( + client.get_project(&project_id).unwrap().verification_status, + VerificationStatus::Verified + ); + let project_id2 = setup_project_with_fee(&client, &env, &admin, &owner, "Project 2"); client.request_verification( @@ -179,18 +154,25 @@ fn test_valid_state_transitions() { &owner, &String::from_str(&env, "ipfs://evidence2"), ); + client.reject_verification(&project_id2, &admin); let project = client.get_project(&project_id2).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Rejected); - // Re-request verification after rejection + assert_eq!( + client.get_project(&project_id2).unwrap().verification_status, + VerificationStatus::Rejected + ); + let token_admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(token_admin) .address(); + let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); token_client.mint(&owner, &1000); + client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); client.pay_fee(&owner, &project_id2, &Some(token_address)); @@ -203,10 +185,101 @@ fn test_valid_state_transitions() { let project = client.get_project(&project_id2).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Pending); - // Test 4: Pending -> Rejected (admin rejection) - client.reject_verification(&project_id2, &admin); - let project = client.get_project(&project_id2).unwrap(); - assert_eq!(project.verification_status, VerificationStatus::Rejected); + assert_eq!( + client.get_project(&project_id2).unwrap().verification_status, + VerificationStatus::Pending + ); +} + +#[test] +fn test_duplicate_payment_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, owner) = setup(&env); + + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "Project Dup"), + description: String::from_str(&env, "Description... Description... Description..."), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let project_id = client.register_project(¶ms); + + let token_admin = Address::generate(&env); + let token_address = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + + let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); + token_client.mint(&owner, &1000); + + client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); + + client.pay_fee(&owner, &project_id, &Some(token_address.clone())); + + let result = client.try_pay_fee(&owner, &project_id, &Some(token_address)); + assert_eq!(result, Err(Ok(ContractError::FeeAlreadyPaid))); +} + +#[test] +fn test_wrong_token_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, owner) = setup(&env); + + let params = ProjectRegistrationParams { + owner: owner.clone(), + name: String::from_str(&env, "Project WrongTok"), + description: String::from_str(&env, "Description... Description... Description..."), + category: String::from_str(&env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }; + + let project_id = client.register_project(¶ms); + + let token_admin = Address::generate(&env); + let correct_token = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + + let wrong_token_admin = Address::generate(&env); + let wrong_token = env + .register_stellar_asset_contract_v2(wrong_token_admin) + .address(); + + client.set_fee(&admin, &Some(correct_token), &100, &admin); + + let result = client.try_pay_fee(&owner, &project_id, &Some(wrong_token)); + assert_eq!(result, Err(Ok(ContractError::InvalidToken))); +} + +#[test] +fn test_replay_attack_rejected() { + let env = Env::default(); + env.mock_all_auths(); + let (client, admin, owner) = setup(&env); + + let project_id = setup_project_with_fee(&client, &env, &admin, &owner, "Project Replay"); + + client.request_verification( + &project_id, + &owner, + &String::from_str(&env, "ipfs://evidence"), + ); + + let result = client.try_request_verification( + &project_id, + &owner, + &String::from_str(&env, "ipfs://evidence2"), + ); + + assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } #[test] @@ -240,12 +313,12 @@ fn test_invalid_transitions_from_pending() { &String::from_str(&env, "ipfs://evidence"), ); - // Cannot request verification again while already pending let result = client.try_request_verification( &project_id, &owner, &String::from_str(&env, "ipfs://evidence2"), ); + assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } @@ -262,9 +335,9 @@ fn test_invalid_transitions_from_verified() { &owner, &String::from_str(&env, "ipfs://evidence"), ); + client.approve_verification(&project_id, &admin); - // Cannot request verification for already verified project let result = client.try_request_verification( &project_id, &owner, @@ -272,11 +345,9 @@ fn test_invalid_transitions_from_verified() { ); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); - // Cannot approve already verified project let result = client.try_approve_verification(&project_id, &admin); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); - // Cannot reject already verified project let result = client.try_reject_verification(&project_id, &admin); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } @@ -294,13 +365,12 @@ fn test_invalid_transitions_from_rejected() { &owner, &String::from_str(&env, "ipfs://evidence"), ); + client.reject_verification(&project_id, &admin); - // Cannot approve directly from rejected state let result = client.try_approve_verification(&project_id, &admin); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); - // Cannot reject again from rejected state let result = client.try_reject_verification(&project_id, &admin); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } @@ -313,30 +383,32 @@ fn test_multiple_verification_cycles() { let project_id = setup_project_with_fee(&client, &env, &admin, &owner, "Project Cycle"); - // First cycle: Request -> Reject -> Request -> Approve client.request_verification( &project_id, &owner, &String::from_str(&env, "ipfs://evidence1"), ); + assert_eq!( client.get_project(&project_id).unwrap().verification_status, VerificationStatus::Pending ); client.reject_verification(&project_id, &admin); + assert_eq!( client.get_project(&project_id).unwrap().verification_status, VerificationStatus::Rejected ); - // Pay fee again for re-submission let token_admin = Address::generate(&env); let token_address = env .register_stellar_asset_contract_v2(token_admin) .address(); + let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); token_client.mint(&owner, &1000); + client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); client.pay_fee(&owner, &project_id, &Some(token_address)); @@ -345,24 +417,27 @@ fn test_multiple_verification_cycles() { &owner, &String::from_str(&env, "ipfs://evidence2"), ); + assert_eq!( client.get_project(&project_id).unwrap().verification_status, VerificationStatus::Pending ); client.approve_verification(&project_id, &admin); + assert_eq!( client.get_project(&project_id).unwrap().verification_status, VerificationStatus::Verified ); - // After verification, no more transitions should be possible let token_admin2 = Address::generate(&env); let token_address2 = env .register_stellar_asset_contract_v2(token_admin2) .address(); + let token_client2 = soroban_sdk::token::StellarAssetClient::new(&env, &token_address2); token_client2.mint(&owner, &1000); + client.set_fee(&admin, &Some(token_address2.clone()), &100, &admin); client.pay_fee(&owner, &project_id, &Some(token_address2)); @@ -371,6 +446,7 @@ fn test_multiple_verification_cycles() { &owner, &String::from_str(&env, "ipfs://evidence3"), ); + assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } @@ -388,17 +464,14 @@ fn test_idempotent_transitions() { VerificationStatus::Unverified ); - // Request verification client.request_verification( &project_id, &owner, &String::from_str(&env, "ipfs://evidence"), ); - // Approve verification client.approve_verification(&project_id, &admin); - // Try to approve again - should fail because Verified is a terminal state let result = client.try_approve_verification(&project_id, &admin); assert_eq!(result, Err(Ok(ContractError::InvalidStatusTransition))); } @@ -409,7 +482,6 @@ fn test_state_machine_with_different_admins() { env.mock_all_auths(); let (client, admin, owner) = setup(&env); - // Add another admin let admin2 = Address::generate(&env); client.add_admin(&admin, &admin2); @@ -421,10 +493,10 @@ fn test_state_machine_with_different_admins() { &String::from_str(&env, "ipfs://evidence"), ); - // Different admin should be able to approve client.approve_verification(&project_id, &admin2); + assert_eq!( client.get_project(&project_id).unwrap().verification_status, VerificationStatus::Verified ); -} +} \ No newline at end of file