Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions src/kyc_policy.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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<KycRecord>,
now: u64,
nonce: u64,
) -> Result<KycRecord, KycEligibilityError> {
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),
Expand All @@ -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 => {
Expand All @@ -72,7 +85,8 @@ pub fn authorize_action(
nonce: u64,
) -> Result<KycRecord, KycEligibilityError> {
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 {
Expand All @@ -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)
}
Expand All @@ -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),
Expand All @@ -124,11 +138,11 @@ pub fn authorize_before_side_effect(
nonce: u64,
) -> Result<KycRecord, KycEligibilityError> {
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)
Expand All @@ -153,11 +167,11 @@ mod tests {
use super::*;

fn verified(expires_at: u64) -> Option<KycRecord> {
Some(KycRecord {
Some(KycRecord::V1(KycRecordV1 {
status: VerificationStatus::Verified,
expires_at,
version: 1,
})
}))
}

#[test]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
78 changes: 13 additions & 65 deletions src/test_kyc_policy_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ mod entrypoint_tests {
use crate::verification::VerificationStatus;

fn valid() -> Option<KycRecord> {
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]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
7 changes: 2 additions & 5 deletions src/test_kyc_policy_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}))
);
}
}
Expand Down
Loading