diff --git a/contracts/credential-nft/src/mint.rs b/contracts/credential-nft/src/mint.rs index 8a0be03..73b4780 100644 --- a/contracts/credential-nft/src/mint.rs +++ b/contracts/credential-nft/src/mint.rs @@ -64,6 +64,11 @@ pub fn mint_credential( score: u32, metadata_uri: &Symbol, ) -> u64 { + let zero_address = chainlearn_shared::zero_address(env); + if *to == zero_address { + panic!("cannot mint to zero address"); + } + // Metadata URI gate: must be non-empty, >= 8 chars, with a valid scheme validate_metadata_uri(env, metadata_uri); diff --git a/contracts/learn-token/src/lib.rs b/contracts/learn-token/src/lib.rs index ed1865a..9e6a591 100644 --- a/contracts/learn-token/src/lib.rs +++ b/contracts/learn-token/src/lib.rs @@ -1624,6 +1624,11 @@ impl LearnToken { let admin = storage::get_admin(&env); admin.require_auth(); + let zero_address = chainlearn_shared::zero_address(&env); + if beneficiary == zero_address { + panic!("cannot vest to zero address"); + } + if total_amount <= 0 { panic!("total_amount must be positive"); } diff --git a/contracts/progress-tracker/src/lib.rs b/contracts/progress-tracker/src/lib.rs index 8fb92da..742533c 100644 --- a/contracts/progress-tracker/src/lib.rs +++ b/contracts/progress-tracker/src/lib.rs @@ -2066,6 +2066,11 @@ impl ProgressTracker { Self::require_not_paused(&env); learner.require_auth(); + let zero_address = chainlearn_shared::zero_address(&env); + if delegate == zero_address { + panic!("cannot delegate to zero address"); + } + types::write_entry( &env, &ProgressTrackerDataKey::DelegatedTo(learner.clone()), @@ -2099,10 +2104,7 @@ impl ProgressTracker { Self::require_not_paused(&env); learner.require_auth(); - types::remove_entry( - &env, - &ProgressTrackerDataKey::DelegatedTo(learner.clone()), - ); + types::remove_entry(&env, &ProgressTrackerDataKey::DelegatedTo(learner.clone())); env.events() .publish((Symbol::new(&env, "delegation_revoked"),), (&learner,)); diff --git a/tests/unit/credential_tests.rs b/tests/unit/credential_tests.rs index 46c5c3b..bdad438 100644 --- a/tests/unit/credential_tests.rs +++ b/tests/unit/credential_tests.rs @@ -564,7 +564,8 @@ mod credential_unit_tests { let learner = Address::generate(&env); let course_id = Symbol::new(&env, "rust_101"); enrolled_and_completed_with_score(&env, &tracker_id, &learner, &course_id, 85); - let cred_id = client.mint_credential(&learner, &course_id, &85, &Symbol::new(&env, "ipfs_Qm123")); + let cred_id = + client.mint_credential(&learner, &course_id, &85, &Symbol::new(&env, "ipfs_Qm123")); let before = client.get_storage_size(); let _ = client.verify_credential(&cred_id); @@ -613,6 +614,36 @@ mod credential_unit_tests { assert_ne!(cert1, cert3); // Valid credential metadata URI format - assert!(client.try_mint_credential(&learner1, &course1, &90, &cert1).is_ok()); + assert!(client + .try_mint_credential(&learner1, &course1, &90, &cert1) + .is_ok()); + } + + // ── Zero address rejection tests (#638) ────────────────────────────── + + #[test] + #[should_panic(expected = "cannot mint to zero address")] + fn test_mint_credential_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id, _) = setup_contract(&env); + let client = CredentialNftClient::new(&env, &contract_id); + env.mock_all_auths(); + + let zero_address = chainlearn_shared::zero_address(&env); + let course_id = Symbol::new(&env, "rust_101"); + let uri = Symbol::new(&env, "ipfs_valid_metadata_123"); + client.mint_credential(&zero_address, &course_id, &85, &uri); + } + + #[test] + #[should_panic(expected = "cannot transfer admin to zero address")] + fn test_transfer_admin_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id, _) = setup_contract(&env); + let client = CredentialNftClient::new(&env, &contract_id); + env.mock_all_auths(); + + let zero_address = chainlearn_shared::zero_address(&env); + client.transfer_admin(&zero_address); } } diff --git a/tests/unit/progress_tests.rs b/tests/unit/progress_tests.rs index a434a7f..74dd869 100644 --- a/tests/unit/progress_tests.rs +++ b/tests/unit/progress_tests.rs @@ -2414,11 +2414,15 @@ mod progress_unit_tests { client.emergency_pause(); // enroll_checked must fail when paused - assert!(client.try_enroll_checked(&learner, &course_id, &None).is_err()); + assert!(client + .try_enroll_checked(&learner, &course_id, &None) + .is_err()); // Unpause and verify enroll_checked succeeds client.unpause(); - assert!(client.try_enroll_checked(&learner, &course_id, &None).is_ok()); + assert!(client + .try_enroll_checked(&learner, &course_id, &None) + .is_ok()); } #[test] @@ -2440,9 +2444,52 @@ mod progress_unit_tests { client.emergency_pause(); let caller = learner.clone(); - assert!(client.try_retake_quiz_for(&caller, &learner, &course_id, &Symbol::new(&env, "quiz_1"), &80).is_err()); + assert!(client + .try_retake_quiz_for( + &caller, + &learner, + &course_id, + &Symbol::new(&env, "quiz_1"), + &80 + ) + .is_err()); client.unpause(); - assert!(client.try_retake_quiz_for(&caller, &learner, &course_id, &Symbol::new(&env, "quiz_1"), &80).is_ok()); + assert!(client + .try_retake_quiz_for( + &caller, + &learner, + &course_id, + &Symbol::new(&env, "quiz_1"), + &80 + ) + .is_ok()); + } + + // ── Zero address rejection tests (#638) ────────────────────────────── + + #[test] + #[should_panic(expected = "cannot transfer admin to zero address")] + fn test_transfer_admin_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id) = setup_contract(&env); + let client = ProgressTrackerClient::new(&env, &contract_id); + env.mock_all_auths(); + + let zero_address = chainlearn_shared::zero_address(&env); + client.transfer_admin(&zero_address); + } + + #[test] + #[should_panic(expected = "cannot delegate to zero address")] + fn test_delegate_progress_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id) = setup_contract(&env); + let client = ProgressTrackerClient::new(&env, &contract_id); + env.mock_all_auths(); + + let learner = Address::generate(&env); + let zero_address = chainlearn_shared::zero_address(&env); + client.delegate_progress(&learner, &zero_address); } } diff --git a/tests/unit/token_tests.rs b/tests/unit/token_tests.rs index 67433ba..5161185 100644 --- a/tests/unit/token_tests.rs +++ b/tests/unit/token_tests.rs @@ -891,8 +891,6 @@ mod token_unit_tests { #[test] #[should_panic(expected = "no snapshot available at specified ledger")] fn test_vote_rejects_missing_snapshot_instead_of_using_current_balance() { - #[should_panic(expected = "contract is paused")] - fn test_create_proposal_fails_while_paused() { let env = Env::default(); let (admin, contract_id, _) = setup_token(&env); let client = LearnTokenClient::new(&env, &contract_id); @@ -928,6 +926,16 @@ mod token_unit_tests { &0, &1_000, &100, + ); + } + + #[test] + #[should_panic(expected = "contract is paused")] + fn test_create_proposal_fails_while_paused() { + let env = Env::default(); + let (admin, contract_id, _) = setup_token(&env); + let client = LearnTokenClient::new(&env, &contract_id); + env.mock_all_auths(); client.pause(&admin); client.create_proposal( &SorobanString::from_str(&env, "Paused proposal"), @@ -939,7 +947,6 @@ mod token_unit_tests { } #[test] - fn test_vesting_schedule_cliff_linear_vesting_and_claiming() { #[should_panic(expected = "contract is paused")] fn test_vote_fails_while_paused() { let env = Env::default(); @@ -1841,4 +1848,30 @@ mod token_unit_tests { let beneficiary = Address::generate(&env); client.create_vesting(&beneficiary, &11, &0, &100); } + + // ── Zero address rejection tests (#638) ────────────────────────────── + + #[test] + #[should_panic(expected = "cannot transfer admin to zero address")] + fn test_transfer_admin_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id, _) = setup_token(&env); + let client = LearnTokenClient::new(&env, &contract_id); + env.mock_all_auths(); + + let zero_address = chainlearn_shared::zero_address(&env); + client.transfer_admin(&zero_address); + } + + #[test] + #[should_panic(expected = "cannot vest to zero address")] + fn test_create_vesting_to_zero_address_panics() { + let env = Env::default(); + let (_admin, contract_id, _) = setup_token(&env); + let client = LearnTokenClient::new(&env, &contract_id); + env.mock_all_auths(); + + let zero_address = chainlearn_shared::zero_address(&env); + client.create_vesting(&zero_address, &1_000, &0, &100); + } }