From 2abf4b71cb3233efa00e82bcb1c703e2f948f393 Mon Sep 17 00:00:00 2001 From: Zintarh <35270183+zintarh@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:44:45 +0100 Subject: [PATCH] feat: Add KYC verification for plan creation and claiming - Add KycStatus enum (Pending, Approved, Rejected) - Add KYC status management functions (set_kyc_status, get_kyc_status) - Add KYC check to create_inheritance_plan function - Implement claim_plan function with KYC verification - Add comprehensive unit and integration tests - Block pending/rejected users from creating or claiming plans Closes #71 --- contracts/inheritance-contract/src/lib.rs | 114 ++++++- contracts/inheritance-contract/src/test.rs | 370 +++++++++++++++++++++ 2 files changed, 483 insertions(+), 1 deletion(-) diff --git a/contracts/inheritance-contract/src/lib.rs b/contracts/inheritance-contract/src/lib.rs index 53f542a..2ca43da 100644 --- a/contracts/inheritance-contract/src/lib.rs +++ b/contracts/inheritance-contract/src/lib.rs @@ -13,6 +13,14 @@ pub enum DistributionMethod { Yearly, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum KycStatus { + Pending, + Approved, + Rejected, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Beneficiary { @@ -54,6 +62,7 @@ pub enum InheritanceError { AllocationExceedsLimit = 12, InvalidAllocation = 13, InvalidClaimCodeRange = 14, + KycNotApproved = 15, } #[contracttype] @@ -61,6 +70,7 @@ pub enum InheritanceError { pub enum DataKey { NextPlanId, Plan(u64), + KycStatus(Address), } // Events for beneficiary operations @@ -229,6 +239,49 @@ impl InheritanceContract { env.storage().persistent().get(&key) } + // KYC status management functions + /// Set KYC status for a user address + /// + /// # Arguments + /// * `env` - The environment + /// * `user` - The user address + /// * `status` - The KYC status to set + pub fn set_kyc_status(env: Env, user: Address, status: KycStatus) { + user.require_auth(); + let key = DataKey::KycStatus(user.clone()); + env.storage().persistent().set(&key, &status); + log!(&env, "KYC status set for user: {:?}", user); + } + + /// Get KYC status for a user address + /// + /// # Arguments + /// * `env` - The environment + /// * `user` - The user address + /// + /// # Returns + /// The KYC status of the user, or Pending if not set + pub fn get_kyc_status(env: &Env, user: &Address) -> KycStatus { + let key = DataKey::KycStatus(user.clone()); + env.storage().persistent().get(&key).unwrap_or(KycStatus::Pending) + } + + /// Check if user's KYC status is approved + /// + /// # Arguments + /// * `env` - The environment + /// * `user` - The user address + /// + /// # Returns + /// Ok(()) if approved, Err(KycNotApproved) otherwise + fn check_kyc_approved(env: &Env, user: &Address) -> Result<(), InheritanceError> { + let status = Self::get_kyc_status(env, user); + match status { + KycStatus::Approved => Ok(()), + _ => Err(InheritanceError::KycNotApproved), + } + } + /// Add a beneficiary to an existing inheritance plan /// /// # Arguments @@ -414,6 +467,10 @@ impl InheritanceContract { // Require owner authorization owner.require_auth(); + // Pre-check: Verify that the user's KYC status is approved + Self::check_kyc_approved(&env, &owner) + .map_err(|_| InheritanceError::KycNotApproved)?; + // Validate plan inputs (asset type is hardcoded to USDC) let usdc_symbol = Symbol::new(&env, "USDC"); Self::validate_plan_inputs( @@ -464,6 +521,61 @@ impl InheritanceContract { Ok(plan_id) } + + /// Claim an inheritance plan as a beneficiary + /// + /// # Arguments + /// * `env` - The environment + /// * `claimant` - The address claiming the plan (must authorize this call) + /// * `plan_id` - The ID of the plan to claim + /// * `email` - Email address of the beneficiary (will be hashed for verification) + /// * `claim_code` - 6-digit numeric claim code (0-999999, will be hashed for verification) + /// + /// # Returns + /// Ok(()) on success + /// + /// # Errors + /// - KycNotApproved: If claimant's KYC status is not approved + /// - PlanNotFound: If plan_id doesn't exist + /// - InvalidClaimCode: If claim code doesn't match any beneficiary + pub fn claim_plan( + env: Env, + claimant: Address, + plan_id: u64, + email: String, + claim_code: u32, + ) -> Result<(), InheritanceError> { + // Require claimant authorization + claimant.require_auth(); + + // Pre-check: Verify that the user's KYC status is approved + Self::check_kyc_approved(&env, &claimant) + .map_err(|_| InheritanceError::KycNotApproved)?; + + // Get the plan + let plan = Self::get_plan(&env, plan_id).ok_or(InheritanceError::PlanNotFound)?; + + // Hash the provided email and claim code for verification + let hashed_email = Self::hash_string(&env, email); + let hashed_claim_code = Self::hash_claim_code(&env, claim_code)?; + + // Verify that the claimant matches a beneficiary in the plan + let mut found = false; + for beneficiary in plan.beneficiaries.iter() { + if beneficiary.hashed_email == hashed_email && beneficiary.hashed_claim_code == hashed_claim_code { + found = true; + break; + } + } + + if !found { + return Err(InheritanceError::InvalidClaimCode); + } + + log!(&env, "Plan {} claimed by beneficiary", plan_id); + + Ok(()) + } } -mod test; +mod test; \ No newline at end of file diff --git a/contracts/inheritance-contract/src/test.rs b/contracts/inheritance-contract/src/test.rs index d89e810..da012ae 100644 --- a/contracts/inheritance-contract/src/test.rs +++ b/contracts/inheritance-contract/src/test.rs @@ -654,3 +654,373 @@ fn test_events_emitted() { let events = env.events().all(); assert!(events.len() > 0); } + +// KYC Tests +#[test] +fn test_set_and_get_kyc_status() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let user = create_test_address(&env, 1); + + // Test default status is Pending + let status = client.get_kyc_status(&user); + assert_eq!(status, KycStatus::Pending); + + // Set to Approved + client.set_kyc_status(&user, &KycStatus::Approved); + let status = client.get_kyc_status(&user); + assert_eq!(status, KycStatus::Approved); + + // Set to Rejected + client.set_kyc_status(&user, &KycStatus::Rejected); + let status = client.get_kyc_status(&user); + assert_eq!(status, KycStatus::Rejected); + + // Set back to Pending + client.set_kyc_status(&user, &KycStatus::Pending); + let status = client.get_kyc_status(&user); + assert_eq!(status, KycStatus::Pending); +} + +#[test] +fn test_create_plan_with_approved_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + + // Set KYC status to Approved + client.set_kyc_status(&owner, &KycStatus::Approved); + + // Create plan should succeed + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Alice"), + String::from_str(&env, "alice@example.com"), + 111111u32, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let result = client.try_create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + assert!(result.is_ok()); + let plan_id = result.unwrap(); + assert!(plan_id > 0); +} + +#[test] +fn test_create_plan_with_pending_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + + // KYC status is Pending by default, so no need to set it + + // Create plan should fail with KycNotApproved + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Alice"), + String::from_str(&env, "alice@example.com"), + 111111u32, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let result = client.try_create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::KycNotApproved as u32) + ); +} + +#[test] +fn test_create_plan_with_rejected_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + + // Set KYC status to Rejected + client.set_kyc_status(&owner, &KycStatus::Rejected); + + // Create plan should fail with KycNotApproved + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Alice"), + String::from_str(&env, "alice@example.com"), + 111111u32, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let result = client.try_create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::KycNotApproved as u32) + ); +} + +#[test] +fn test_claim_plan_with_approved_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + let beneficiary = create_test_address(&env, 2); + + // Set owner KYC to Approved + client.set_kyc_status(&owner, &KycStatus::Approved); + + // Create plan + let beneficiary_email = String::from_str(&env, "beneficiary@example.com"); + let claim_code = 123456u32; + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Beneficiary Name"), + beneficiary_email.clone(), + claim_code, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let plan_id = client.create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + // Set beneficiary KYC to Approved + client.set_kyc_status(&beneficiary, &KycStatus::Approved); + + // Claim plan should succeed + let result = client.try_claim_plan( + &beneficiary, + &plan_id, + &beneficiary_email, + &claim_code, + ); + + assert!(result.is_ok()); +} + +#[test] +fn test_claim_plan_with_pending_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + let beneficiary = create_test_address(&env, 2); + + // Set owner KYC to Approved + client.set_kyc_status(&owner, &KycStatus::Approved); + + // Create plan + let beneficiary_email = String::from_str(&env, "beneficiary@example.com"); + let claim_code = 123456u32; + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Beneficiary Name"), + beneficiary_email.clone(), + claim_code, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let plan_id = client.create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + // Beneficiary KYC is Pending by default + + // Claim plan should fail with KycNotApproved + let result = client.try_claim_plan( + &beneficiary, + &plan_id, + &beneficiary_email, + &claim_code, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::KycNotApproved as u32) + ); +} + +#[test] +fn test_claim_plan_with_rejected_kyc() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + let beneficiary = create_test_address(&env, 2); + + // Set owner KYC to Approved + client.set_kyc_status(&owner, &KycStatus::Approved); + + // Create plan + let beneficiary_email = String::from_str(&env, "beneficiary@example.com"); + let claim_code = 123456u32; + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Beneficiary Name"), + beneficiary_email.clone(), + claim_code, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let plan_id = client.create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + // Set beneficiary KYC to Rejected + client.set_kyc_status(&beneficiary, &KycStatus::Rejected); + + // Claim plan should fail with KycNotApproved + let result = client.try_claim_plan( + &beneficiary, + &plan_id, + &beneficiary_email, + &claim_code, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::KycNotApproved as u32) + ); +} + +#[test] +fn test_claim_plan_invalid_credentials() { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register_contract(None, InheritanceContract); + let client = InheritanceContractClient::new(&env, &contract_id); + + let owner = create_test_address(&env, 1); + let beneficiary = create_test_address(&env, 2); + + // Set KYC to Approved for both + client.set_kyc_status(&owner, &KycStatus::Approved); + client.set_kyc_status(&beneficiary, &KycStatus::Approved); + + // Create plan + let beneficiary_email = String::from_str(&env, "beneficiary@example.com"); + let claim_code = 123456u32; + let beneficiaries_data = vec![ + &env, + ( + String::from_str(&env, "Beneficiary Name"), + beneficiary_email.clone(), + claim_code, + create_test_bytes(&env, "1111111111111111"), + 10000u32, + ), + ]; + + let plan_id = client.create_inheritance_plan( + &owner, + &String::from_str(&env, "Test Plan"), + &String::from_str(&env, "Test Description"), + &1000000u64, + &DistributionMethod::LumpSum, + &beneficiaries_data, + ); + + // Try to claim with wrong email + let result = client.try_claim_plan( + &beneficiary, + &plan_id, + &String::from_str(&env, "wrong@example.com"), + &claim_code, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::InvalidClaimCode as u32) + ); + + // Try to claim with wrong claim code + let result = client.try_claim_plan( + &beneficiary, + &plan_id, + &beneficiary_email, + &999999u32, + ); + + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().error, + soroban_sdk::Error::Contract(InheritanceError::InvalidClaimCode as u32) + ); +} \ No newline at end of file