From 1666eb00c81d434b9a2fdf20ff82a768cba1438a Mon Sep 17 00:00:00 2001 From: enny791 Date: Mon, 31 Aug 2026 16:15:50 +0100 Subject: [PATCH] fix(kyc): upgrade KycRecord to enum with V1 for storage and migration compatibility (fixes #2478) --- src/kyc_policy.rs | 48 +++++++++++------- src/test_kyc_policy_entrypoints.rs | 78 +++++------------------------- src/test_kyc_policy_matrix.rs | 7 +-- 3 files changed, 46 insertions(+), 87 deletions(-) diff --git a/src/kyc_policy.rs b/src/kyc_policy.rs index ee28a5a43..7903eac27 100644 --- a/src/kyc_policy.rs +++ b/src/kyc_policy.rs @@ -1,13 +1,22 @@ //! One deterministic KYC eligibility predicate for every KYC-dependent action. use crate::verification::VerificationStatus; +use soroban_sdk::contracttype; +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum KycRecord { + V1(KycRecordV1), +} + +#[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct KycRecord { +pub struct KycRecordV1 { pub status: VerificationStatus, pub expires_at: u64, pub version: u32, } +#[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum KycEligibilityError { Missing, @@ -18,12 +27,14 @@ pub enum KycEligibilityError { ReplayedNonce, } +#[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum KycActor { Business, Investor, } +#[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum KycDependentAction { CreateInvoice, @@ -32,18 +43,20 @@ pub enum KycDependentAction { SettleInvoice, } -/// The shared predicate. Expiry is exclusive: `now == expires_at` is expired. +/// The shared predicate. Expiry is exclusive: +ow == expires_at is expired. pub fn require_eligible( record: Option, now: u64, nonce: u64, ) -> Result { let record = record.ok_or(KycEligibilityError::Missing)?; - if record.expires_at == 0 { + let KycRecord::V1(inner) = &record; + if inner.expires_at == 0 { return Err(KycEligibilityError::InvalidExpiry); } - let status_res = match record.status { - VerificationStatus::Verified if now < record.expires_at => Ok(record), + let status_res = match inner.status { + VerificationStatus::Verified if now < inner.expires_at => Ok(record.clone()), VerificationStatus::Verified => Err(KycEligibilityError::Expired), VerificationStatus::Pending => Err(KycEligibilityError::Pending), VerificationStatus::Rejected => Err(KycEligibilityError::Revoked), @@ -52,7 +65,7 @@ pub fn require_eligible( return status_res; } if nonce > 0 { - match crate::kyc_nonces::check_and_record_nonce(record.version, nonce, None, None) { + match crate::kyc_nonces::check_and_record_nonce(inner.version, nonce, None, None) { crate::kyc_nonces::NonceCheckResult::New | crate::kyc_nonces::NonceCheckResult::SafeRetry => {} crate::kyc_nonces::NonceCheckResult::Conflict => { @@ -72,7 +85,8 @@ pub fn authorize_action( nonce: u64, ) -> Result { let record = record.ok_or(KycEligibilityError::Missing)?; - if record.expires_at == 0 { + let KycRecord::V1(inner) = &record; + if inner.expires_at == 0 { return Err(KycEligibilityError::InvalidExpiry); } let actor_allowed = match action { @@ -82,10 +96,10 @@ pub fn authorize_action( } KycDependentAction::SettleInvoice => true, }; - let status_res = match record.status { - VerificationStatus::Verified if now < record.expires_at => { + let status_res = match inner.status { + VerificationStatus::Verified if now < inner.expires_at => { if actor_allowed { - Ok(record) + Ok(record.clone()) } else { Err(KycEligibilityError::Revoked) } @@ -99,7 +113,7 @@ pub fn authorize_action( } if nonce > 0 { match crate::kyc_nonces::check_and_record_nonce( - record.version, + inner.version, nonce, Some(actor), Some(action), @@ -124,11 +138,11 @@ pub fn authorize_before_side_effect( nonce: u64, ) -> Result { if terminal { - return Ok(record.unwrap_or(KycRecord { + return Ok(record.unwrap_or(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Verified, expires_at: u64::MAX, version: 0, - })); + }))); } let record = authorize_action(record, actor, action, now, nonce)?; Ok(record) @@ -153,11 +167,11 @@ mod tests { use super::*; fn verified(expires_at: u64) -> Option { - Some(KycRecord { + Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Verified, expires_at, version: 1, - }) + })) } #[test] @@ -190,7 +204,7 @@ mod tests { crate::kyc_nonces::reset_nonces(); let r = verified(100); assert!(authorize_action( - r, + r.clone(), KycActor::Business, KycDependentAction::CreateInvoice, 50, @@ -212,7 +226,7 @@ mod tests { crate::kyc_nonces::reset_nonces(); let r = verified(100); assert!(authorize_action( - r, + r.clone(), KycActor::Business, KycDependentAction::CreateInvoice, 50, diff --git a/src/test_kyc_policy_entrypoints.rs b/src/test_kyc_policy_entrypoints.rs index a063dced2..cadecce8b 100644 --- a/src/test_kyc_policy_entrypoints.rs +++ b/src/test_kyc_policy_entrypoints.rs @@ -4,11 +4,7 @@ mod entrypoint_tests { use crate::verification::VerificationStatus; fn valid() -> Option { - Some(KycRecord { - status: VerificationStatus::Verified, - expires_at: 1_000, - version: 9, - }) + Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Verified, expires_at: 1_000, version: 9 })) } #[test] @@ -111,11 +107,7 @@ mod entrypoint_tests { #[test] fn pending_create_invoice_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -130,11 +122,7 @@ mod entrypoint_tests { #[test] fn pending_submit_bid_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -149,11 +137,7 @@ mod entrypoint_tests { #[test] fn pending_fund_invoice_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -168,11 +152,7 @@ mod entrypoint_tests { #[test] fn pending_settlement_is_blocked_before_side_effect() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 1 })); assert_eq!( authorize_before_side_effect( record, @@ -188,11 +168,7 @@ mod entrypoint_tests { #[test] fn revoked_create_invoice_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Rejected, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Rejected, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -207,11 +183,7 @@ mod entrypoint_tests { #[test] fn revoked_submit_bid_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Rejected, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Rejected, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -226,11 +198,7 @@ mod entrypoint_tests { #[test] fn revoked_fund_invoice_is_blocked() { - let record = Some(KycRecord { - status: VerificationStatus::Rejected, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Rejected, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -245,11 +213,7 @@ mod entrypoint_tests { #[test] fn revoked_settlement_is_blocked_before_side_effect() { - let record = Some(KycRecord { - status: VerificationStatus::Rejected, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Rejected, expires_at: 1_000, version: 1 })); assert_eq!( authorize_before_side_effect( record, @@ -367,11 +331,7 @@ mod entrypoint_tests { #[test] fn action_error_precedes_actor_error() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 1 })); assert_eq!( authorize_action( record, @@ -400,11 +360,7 @@ mod entrypoint_tests { #[test] fn invalid_expiry_precedes_status_error() { - let record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 0, - version: 1, - }); + let record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 0, version: 1 })); assert_eq!( require_eligible(record, 0, 0), Err(KycEligibilityError::InvalidExpiry) @@ -513,11 +469,7 @@ mod entrypoint_tests { #[test] fn failed_eligibility_leaves_no_nonce_state() { crate::kyc_nonces::reset_nonces(); - let pending_record = Some(KycRecord { - status: VerificationStatus::Pending, - expires_at: 1_000, - version: 5, - }); + let pending_record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 1_000, version: 5 })); let res1 = authorize_action( pending_record, KycActor::Business, @@ -527,11 +479,7 @@ mod entrypoint_tests { ); assert_eq!(res1, Err(KycEligibilityError::Pending)); - let verified_record = Some(KycRecord { - status: VerificationStatus::Verified, - expires_at: 1_000, - version: 5, - }); + let verified_record = Some(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Verified, expires_at: 1_000, version: 5 })); let res2 = authorize_action( verified_record, KycActor::Business, diff --git a/src/test_kyc_policy_matrix.rs b/src/test_kyc_policy_matrix.rs index 7851b4ff4..25bbe2420 100644 --- a/src/test_kyc_policy_matrix.rs +++ b/src/test_kyc_policy_matrix.rs @@ -160,11 +160,8 @@ mod tests { 1, 0 ), - Ok(KycRecord { - status: VerificationStatus::Pending, - expires_at: 100, - version: 1 - }) + Ok(KycRecord::V1(KycRecordV1 { status: VerificationStatus::Pending, expires_at: 100, version: 1 + })) ); } }