diff --git a/contracts/inheritance-contract/src/lib.rs b/contracts/inheritance-contract/src/lib.rs index 291d31b..e70e8ee 100644 --- a/contracts/inheritance-contract/src/lib.rs +++ b/contracts/inheritance-contract/src/lib.rs @@ -16,6 +16,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 { @@ -70,6 +78,7 @@ pub enum InheritanceError { AllocationExceedsLimit = 12, InvalidAllocation = 13, InvalidClaimCodeRange = 14, + KycNotApproved = 15, ClaimNotAllowedYet = 15, AlreadyClaimed = 16, BeneficiaryNotFound = 17, @@ -98,6 +107,7 @@ pub enum InheritanceError { pub enum DataKey { NextPlanId, Plan(u64), + KycStatus(Address), Claim(BytesN<32>), // keyed by hashed_email UserPlans(Address), // keyed by owner Address, value is Vec UserClaimedPlans(Address), // keyed by owner Address, value is Vec @@ -430,6 +440,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), + } + } + fn add_plan_to_user(env: &Env, owner: Address, plan_id: u64) { let key = DataKey::UserPlans(owner.clone()); let mut plans: Vec = env @@ -766,6 +819,11 @@ 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) // Admin must be set to receive the fee let admin = Self::get_admin(&env).ok_or(InheritanceError::AdminNotSet)?; @@ -886,6 +944,14 @@ 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) pub fn set_lendable( env: Env, owner: Address, @@ -1248,6 +1314,44 @@ impl InheritanceContract { /// 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); /// - Unauthorized: If caller is not the plan owner /// - PlanNotFound: If plan_id doesn't exist /// - PlanAlreadyDeactivated: If plan is already deactivated @@ -1807,4 +1911,4 @@ impl InheritanceContract { } } -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 8d490d9..8ebdc9a 100644 --- a/contracts/inheritance-contract/src/test.rs +++ b/contracts/inheritance-contract/src/test.rs @@ -2562,3 +2562,373 @@ fn test_full_loan_recall_workflow() { assert_eq!(info.recalled_amount, 150_000); assert_eq!(info.settled_amount, 50_000); } + +// 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