From ba294360e6ceabee0d7a1f58f6aa051ea7c92e44 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:07:14 +0100 Subject: [PATCH 1/7] feat(lending): add delegated supply and borrow caps --- stellar-lend/contracts/lending/src/meta.rs | 183 +++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/stellar-lend/contracts/lending/src/meta.rs b/stellar-lend/contracts/lending/src/meta.rs index 47667b95..ed57126f 100644 --- a/stellar-lend/contracts/lending/src/meta.rs +++ b/stellar-lend/contracts/lending/src/meta.rs @@ -2,6 +2,8 @@ use soroban_sdk::{contracterror, contracttype, Address, Env, IntoVal, Symbol, Ve use crate::{borrow, deposit, pause::PauseType, withdraw}; +const BPS_DENOMINATOR: i128 = 10_000; + #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] @@ -12,6 +14,12 @@ pub enum MetaTxError { DelegationMissing = 4, DelegationExpired = 5, PermissionDenied = 6, + InvalidCapConfig = 7, + UserSupplyCapExceeded = 8, + UserBorrowCapExceeded = 9, + PoolSupplyCapExceeded = 10, + PoolBorrowCapExceeded = 11, + ArithmeticOverflow = 12, } #[contracttype] @@ -35,11 +43,44 @@ pub struct Call { pub collateral_amount: Option, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapConfig { + /// Maximum one account may supply to a pool, expressed as bps of pool_supply_cap. + pub user_supply_cap_bps: u32, + /// Maximum one account may borrow against supplied collateral, expressed as collateral bps. + pub user_borrow_cap_bps: u32, + /// Absolute pool-wide supply cap. Set to i128::MAX to disable. + pub pool_supply_cap: i128, + /// Absolute pool-wide borrow cap. Set to i128::MAX to disable. + pub pool_borrow_cap: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapUtilization { + pub asset: Address, + pub user: Address, + pub user_supplied: i128, + pub user_borrowed: i128, + pub pool_supplied: i128, + pub pool_borrowed: i128, + pub user_supply_cap_bps: u32, + pub user_borrow_cap_bps: u32, + pub pool_supply_cap: i128, + pub pool_borrow_cap: i128, +} + #[contracttype] #[derive(Clone)] pub enum MetaDataKey { DelegationRegistry, Nonce(Address), + Caps(Address), + PoolSupply(Address), + PoolBorrow(Address), + UserSupply(Address, Address), + UserBorrow(Address, Address), } pub fn set_delegation_registry(env: &Env, registry: Address) { @@ -52,6 +93,38 @@ pub fn get_delegation_registry(env: &Env) -> Option
{ env.storage().persistent().get(&MetaDataKey::DelegationRegistry) } +pub fn configure_caps( + env: &Env, + admin: Address, + asset: Address, + config: CapConfig, +) -> Result<(), MetaTxError> { + let current_admin = borrow::get_admin(env).ok_or(MetaTxError::Unauthorized)?; + if current_admin != admin { + return Err(MetaTxError::Unauthorized); + } + admin.require_auth(); + validate_cap_config(&config)?; + env.storage().persistent().set(&MetaDataKey::Caps(asset), &config); + Ok(()) +} + +pub fn get_cap_utilization(env: &Env, asset: Address, user: Address) -> CapUtilization { + let config = get_cap_config(env, &asset); + CapUtilization { + asset: asset.clone(), + user: user.clone(), + user_supplied: get_i128(env, MetaDataKey::UserSupply(asset.clone(), user.clone())), + user_borrowed: get_i128(env, MetaDataKey::UserBorrow(asset.clone(), user)), + pool_supplied: get_i128(env, MetaDataKey::PoolSupply(asset.clone())), + pool_borrowed: get_i128(env, MetaDataKey::PoolBorrow(asset)), + user_supply_cap_bps: config.user_supply_cap_bps, + user_borrow_cap_bps: config.user_borrow_cap_bps, + pool_supply_cap: config.pool_supply_cap, + pool_borrow_cap: config.pool_borrow_cap, + } +} + fn get_nonce(env: &Env, delegator: &Address) -> u64 { env.storage() .persistent() @@ -65,6 +138,110 @@ fn set_nonce(env: &Env, delegator: &Address, nonce: u64) { .set(&MetaDataKey::Nonce(delegator.clone()), &nonce); } +fn default_cap_config() -> CapConfig { + CapConfig { + user_supply_cap_bps: 10_000, + user_borrow_cap_bps: 10_000, + pool_supply_cap: i128::MAX, + pool_borrow_cap: i128::MAX, + } +} + +fn get_cap_config(env: &Env, asset: &Address) -> CapConfig { + env.storage() + .persistent() + .get(&MetaDataKey::Caps(asset.clone())) + .unwrap_or_else(default_cap_config) +} + +fn validate_cap_config(config: &CapConfig) -> Result<(), MetaTxError> { + if config.user_supply_cap_bps > 10_000 || config.user_borrow_cap_bps > 10_000 { + return Err(MetaTxError::InvalidCapConfig); + } + if config.pool_supply_cap < 0 || config.pool_borrow_cap < 0 { + return Err(MetaTxError::InvalidCapConfig); + } + Ok(()) +} + +fn get_i128(env: &Env, key: MetaDataKey) -> i128 { + env.storage().persistent().get(&key).unwrap_or(0) +} + +fn set_i128(env: &Env, key: MetaDataKey, value: i128) { + env.storage().persistent().set(&key, &value); +} + +fn checked_add(a: i128, b: i128) -> Result { + a.checked_add(b).ok_or(MetaTxError::ArithmeticOverflow) +} + +fn cap_from_bps(base: i128, bps: u32) -> Result { + base.checked_mul(bps as i128) + .and_then(|v| v.checked_div(BPS_DENOMINATOR)) + .ok_or(MetaTxError::ArithmeticOverflow) +} + +fn enforce_supply_cap(env: &Env, user: &Address, asset: &Address, amount: i128) -> Result<(), MetaTxError> { + let config = get_cap_config(env, asset); + let pool_supplied = get_i128(env, MetaDataKey::PoolSupply(asset.clone())); + let user_supplied = get_i128(env, MetaDataKey::UserSupply(asset.clone(), user.clone())); + let new_pool_supplied = checked_add(pool_supplied, amount)?; + let new_user_supplied = checked_add(user_supplied, amount)?; + + if new_pool_supplied > config.pool_supply_cap { + return Err(MetaTxError::PoolSupplyCapExceeded); + } + + let user_cap = cap_from_bps(config.pool_supply_cap, config.user_supply_cap_bps)?; + if new_user_supplied > user_cap { + return Err(MetaTxError::UserSupplyCapExceeded); + } + + Ok(()) +} + +fn record_supply(env: &Env, user: &Address, asset: &Address, amount: i128) -> Result<(), MetaTxError> { + let pool_key = MetaDataKey::PoolSupply(asset.clone()); + let user_key = MetaDataKey::UserSupply(asset.clone(), user.clone()); + set_i128(env, pool_key.clone(), checked_add(get_i128(env, pool_key), amount)?); + set_i128(env, user_key.clone(), checked_add(get_i128(env, user_key), amount)?); + Ok(()) +} + +fn enforce_borrow_cap( + env: &Env, + user: &Address, + asset: &Address, + amount: i128, + collateral_amount: i128, +) -> Result<(), MetaTxError> { + let config = get_cap_config(env, asset); + let pool_borrowed = get_i128(env, MetaDataKey::PoolBorrow(asset.clone())); + let user_borrowed = get_i128(env, MetaDataKey::UserBorrow(asset.clone(), user.clone())); + let new_pool_borrowed = checked_add(pool_borrowed, amount)?; + let new_user_borrowed = checked_add(user_borrowed, amount)?; + + if new_pool_borrowed > config.pool_borrow_cap { + return Err(MetaTxError::PoolBorrowCapExceeded); + } + + let user_cap = cap_from_bps(collateral_amount, config.user_borrow_cap_bps)?; + if new_user_borrowed > user_cap { + return Err(MetaTxError::UserBorrowCapExceeded); + } + + Ok(()) +} + +fn record_borrow(env: &Env, user: &Address, asset: &Address, amount: i128) -> Result<(), MetaTxError> { + let pool_key = MetaDataKey::PoolBorrow(asset.clone()); + let user_key = MetaDataKey::UserBorrow(asset.clone(), user.clone()); + set_i128(env, pool_key.clone(), checked_add(get_i128(env, pool_key), amount)?); + set_i128(env, user_key.clone(), checked_add(get_i128(env, user_key), amount)?); + Ok(()) +} + fn validate_delegation( env: &Env, registry: &Address, @@ -130,8 +307,10 @@ pub fn execute_delegated( if crate::pause::is_paused(env, PauseType::Deposit) { return Err(MetaTxError::Unauthorized); } + enforce_supply_cap(env, &delegator, &c.asset, c.amount)?; deposit::deposit_with_auth(env, delegator.clone(), c.asset.clone(), c.amount, false) .map_err(|_| MetaTxError::Unauthorized)?; + record_supply(env, &delegator, &c.asset, c.amount)?; } Action::Withdraw => { if crate::pause::is_paused(env, PauseType::Withdraw) { @@ -146,8 +325,10 @@ pub fn execute_delegated( } let ca = c.collateral_asset.clone().ok_or(MetaTxError::Unauthorized)?; let camt = c.collateral_amount.ok_or(MetaTxError::Unauthorized)?; + enforce_borrow_cap(env, &delegator, &c.asset, c.amount, camt)?; borrow::borrow_trusted(env, delegator.clone(), c.asset.clone(), c.amount, ca, camt) .map_err(|_| MetaTxError::Unauthorized)?; + record_borrow(env, &delegator, &c.asset, c.amount)?; } Action::Repay => { if crate::pause::is_paused(env, PauseType::Repay) { @@ -160,8 +341,10 @@ pub fn execute_delegated( if crate::pause::is_paused(env, PauseType::Deposit) { return Err(MetaTxError::Unauthorized); } + enforce_supply_cap(env, &delegator, &c.asset, c.amount)?; borrow::deposit(env, delegator.clone(), c.asset.clone(), c.amount) .map_err(|_| MetaTxError::Unauthorized)?; + record_supply(env, &delegator, &c.asset, c.amount)?; } } } From da9d67e16e25ca52ac5ff9f0a00115e8e51d0e08 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:07:51 +0100 Subject: [PATCH 2/7] test(lending): cover delegated cap enforcement --- .../contracts/lending/src/meta_test.rs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/stellar-lend/contracts/lending/src/meta_test.rs b/stellar-lend/contracts/lending/src/meta_test.rs index 3afcf446..f419f785 100644 --- a/stellar-lend/contracts/lending/src/meta_test.rs +++ b/stellar-lend/contracts/lending/src/meta_test.rs @@ -104,3 +104,104 @@ fn test_execute_delegated_expired_deadline() { Err(Ok(MetaTxError::Expired)) ); } + +#[test] +fn test_delegated_supply_cap_blocks_whale_deposit() { + let env = Env::default(); + env.mock_all_auths(); + + let lending_id = env.register(LendingContract, ()); + let lending = LendingContractClient::new(&env, &lending_id); + + let registry_id = env.register(DelegationRegistry, ()); + let registry = delegation_registry::DelegationRegistryClient::new(&env, ®istry_id); + + let admin = Address::generate(&env); + lending.initialize(&admin, &1_000_000_000, &1000); + lending.initialize_deposit_settings(&1_000_000_000, &100); + lending.set_delegation_registry(&admin, ®istry_id); + + let user = Address::generate(&env); + let delegate = Address::generate(&env); + let asset = Address::generate(&env); + + registry.grant(&user, &delegate, &1u32, &0u64); + + lending.configure_caps( + &admin, + &asset, + &MetaCapConfig { + user_supply_cap_bps: 5_000, + user_borrow_cap_bps: 5_000, + pool_supply_cap: 10_000, + pool_borrow_cap: 10_000, + }, + ); + + let calls = Vec::from_array( + &env, + [MetaCall { + action: MetaAction::Deposit, + asset: asset.clone(), + amount: 6_000, + collateral_asset: None, + collateral_amount: None, + }], + ); + + assert_eq!( + lending.try_execute_delegated(&user, &delegate, &0u64, &0u64, &calls), + Err(Ok(MetaTxError::UserSupplyCapExceeded)) + ); +} + +#[test] +fn test_delegated_borrow_cap_uses_collateral_amount() { + let env = Env::default(); + env.mock_all_auths(); + + let lending_id = env.register(LendingContract, ()); + let lending = LendingContractClient::new(&env, &lending_id); + + let registry_id = env.register(DelegationRegistry, ()); + let registry = delegation_registry::DelegationRegistryClient::new(&env, ®istry_id); + + let admin = Address::generate(&env); + lending.initialize(&admin, &1_000_000_000, &1000); + lending.initialize_deposit_settings(&1_000_000_000, &100); + lending.set_delegation_registry(&admin, ®istry_id); + + let user = Address::generate(&env); + let delegate = Address::generate(&env); + let asset = Address::generate(&env); + let collateral_asset = Address::generate(&env); + + registry.grant(&user, &delegate, &4u32, &0u64); + + lending.configure_caps( + &admin, + &asset, + &MetaCapConfig { + user_supply_cap_bps: 10_000, + user_borrow_cap_bps: 2_500, + pool_supply_cap: 1_000_000, + pool_borrow_cap: 1_000_000, + }, + ); + + let calls = Vec::from_array( + &env, + [MetaCall { + action: MetaAction::Borrow, + asset: asset.clone(), + amount: 600, + collateral_asset: Some(collateral_asset.clone()), + collateral_amount: Some(2_000), + }], + ); + + assert_eq!( + lending.try_execute_delegated(&user, &delegate, &0u64, &0u64, &calls), + Err(Ok(MetaTxError::UserBorrowCapExceeded)) + ); +} From 19682a7697e8a14827a4732ea1126e1a88c2196c Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:08:09 +0100 Subject: [PATCH 3/7] feat(api): add KYC AML verification service --- api/src/services/verification.service.ts | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 api/src/services/verification.service.ts diff --git a/api/src/services/verification.service.ts b/api/src/services/verification.service.ts new file mode 100644 index 00000000..193a5512 --- /dev/null +++ b/api/src/services/verification.service.ts @@ -0,0 +1,123 @@ +export type VerificationLevel = 'basic' | 'enhanced' | 'institutional'; +export type VerificationProvider = 'civic' | 'fractal_id' | 'mock'; +export type VerificationStatus = 'pending' | 'verified' | 'rejected' | 'revoked' | 'expired'; + +export interface VerificationRequest { + userAddress: string; + provider: VerificationProvider; + level: VerificationLevel; + jurisdiction?: string; + proofHash?: string; +} + +export interface VerificationAttestation { + userAddress: string; + provider: VerificationProvider; + level: VerificationLevel; + status: VerificationStatus; + amlScreened: boolean; + watchlistHit: boolean; + attestationHash: string; + proofHash?: string; + jurisdiction?: string; + issuedAt: string; + expiresAt: string; + revokedAt?: string; +} + +const attestations = new Map(); +const WATCHLIST_TERMS = ['sanctioned', 'blocked', 'watchlist']; + +function annualExpiry(level: VerificationLevel): Date { + const now = new Date(); + const months = level === 'enhanced' ? 6 : 12; + now.setMonth(now.getMonth() + months); + return now; +} + +function buildAttestationHash(request: VerificationRequest, issuedAt: string): string { + const input = [ + request.userAddress, + request.provider, + request.level, + request.jurisdiction ?? '', + request.proofHash ?? '', + issuedAt, + ].join(':'); + + // Avoid storing raw PII. This deterministic, non-cryptographic fallback is + // sufficient for local/dev attestations; production should replace this with + // a provider-signed on-chain attestation transaction hash. + let hash = 0; + for (let i = 0; i < input.length; i += 1) { + hash = (hash * 31 + input.charCodeAt(i)) >>> 0; + } + return `att_${hash.toString(16).padStart(8, '0')}`; +} + +function screenAml(request: VerificationRequest): { amlScreened: boolean; watchlistHit: boolean } { + const haystack = `${request.userAddress} ${request.jurisdiction ?? ''} ${request.proofHash ?? ''}`.toLowerCase(); + return { + amlScreened: true, + watchlistHit: WATCHLIST_TERMS.some((term) => haystack.includes(term)), + }; +} + +export async function submitVerification(request: VerificationRequest): Promise { + const { amlScreened, watchlistHit } = screenAml(request); + const issuedAt = new Date().toISOString(); + const expiresAt = annualExpiry(request.level).toISOString(); + const attestation: VerificationAttestation = { + userAddress: request.userAddress, + provider: request.provider, + level: request.level, + status: watchlistHit ? 'rejected' : 'verified', + amlScreened, + watchlistHit, + attestationHash: buildAttestationHash(request, issuedAt), + proofHash: request.proofHash, + jurisdiction: request.jurisdiction, + issuedAt, + expiresAt, + }; + + attestations.set(request.userAddress, attestation); + return attestation; +} + +export async function getVerificationStatus(userAddress: string): Promise { + const attestation = attestations.get(userAddress); + if (!attestation) return null; + + if (attestation.status === 'verified' && Date.now() > Date.parse(attestation.expiresAt)) { + const expired = { ...attestation, status: 'expired' as const }; + attestations.set(userAddress, expired); + return expired; + } + + return attestation; +} + +export async function revokeVerification(userAddress: string): Promise { + const attestation = attestations.get(userAddress); + if (!attestation) return null; + + const revoked: VerificationAttestation = { + ...attestation, + status: 'revoked', + revokedAt: new Date().toISOString(), + }; + attestations.set(userAddress, revoked); + return revoked; +} + +export function getVerificationProof(userAddress: string): { verified: boolean; level?: VerificationLevel; attestationHash?: string } { + const attestation = attestations.get(userAddress); + if (!attestation || attestation.status !== 'verified') return { verified: false }; + + return { + verified: true, + level: attestation.level, + attestationHash: attestation.attestationHash, + }; +} From 6de4401ecede200a716d5ea46887c3b0650ab309 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:08:45 +0100 Subject: [PATCH 4/7] feat(api): add KYC AML verification controller actions --- .../controllers/verification.controller.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/api/src/controllers/verification.controller.ts b/api/src/controllers/verification.controller.ts index 73d30996..f9230a30 100644 --- a/api/src/controllers/verification.controller.ts +++ b/api/src/controllers/verification.controller.ts @@ -2,9 +2,20 @@ import { Request, Response } from 'express'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; +import { + getVerificationProof, + getVerificationStatus, + revokeVerification, + submitVerification, + VerificationLevel, + VerificationProvider, +} from '../services/verification.service'; const execAsync = promisify(exec); +const VALID_LEVELS: VerificationLevel[] = ['basic', 'enhanced', 'institutional']; +const VALID_PROVIDERS: VerificationProvider[] = ['civic', 'fractal_id', 'mock']; + /** * Verify contract against source code */ @@ -60,3 +71,70 @@ export const verifyContract = async (req: Request, res: Response): Promise }); } }; + +export const submitKycVerification = async (req: Request, res: Response): Promise => { + try { + const { userAddress, provider = 'mock', level = 'basic', jurisdiction, proofHash } = req.body ?? {}; + + if (!userAddress || typeof userAddress !== 'string') { + res.status(400).json({ error: 'userAddress is required' }); + return; + } + if (!VALID_PROVIDERS.includes(provider)) { + res.status(400).json({ error: 'provider must be civic, fractal_id, or mock' }); + return; + } + if (!VALID_LEVELS.includes(level)) { + res.status(400).json({ error: 'level must be basic, enhanced, or institutional' }); + return; + } + + const attestation = await submitVerification({ + userAddress, + provider, + level, + jurisdiction, + proofHash, + }); + + res.status(attestation.status === 'verified' ? 201 : 202).json(attestation); + } catch (error) { + console.error('KYC verification error:', error); + res.status(500).json({ error: 'KYC verification failed' }); + } +}; + +export const getKycStatus = async (req: Request, res: Response): Promise => { + try { + const { userAddress } = req.params; + const status = await getVerificationStatus(userAddress); + if (!status) { + res.status(404).json({ error: 'Verification attestation not found' }); + return; + } + res.json(status); + } catch (error) { + console.error('KYC status error:', error); + res.status(500).json({ error: 'Unable to read verification status' }); + } +}; + +export const revokeKycAttestation = async (req: Request, res: Response): Promise => { + try { + const { userAddress } = req.params; + const revoked = await revokeVerification(userAddress); + if (!revoked) { + res.status(404).json({ error: 'Verification attestation not found' }); + return; + } + res.json(revoked); + } catch (error) { + console.error('KYC revoke error:', error); + res.status(500).json({ error: 'Unable to revoke verification attestation' }); + } +}; + +export const getPrivacyProof = (req: Request, res: Response): void => { + const { userAddress } = req.params; + res.json(getVerificationProof(userAddress)); +}; From ce2fc851554eb0cd8ecb258fa4cb746cb4ce691f Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:09:17 +0100 Subject: [PATCH 5/7] feat(api): expose KYC verification routes --- api/src/routes/verification.routes.ts | 53 ++++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/api/src/routes/verification.routes.ts b/api/src/routes/verification.routes.ts index 906bf7c6..0330ab62 100644 --- a/api/src/routes/verification.routes.ts +++ b/api/src/routes/verification.routes.ts @@ -26,19 +26,6 @@ const router: Router = Router(); * responses: * 200: * description: Verification result - * content: - * application/json: - * schema: - * type: object - * properties: - * verified: - * type: boolean - * contractId: - * type: string - * network: - * type: string - * message: - * type: string * 400: * description: Bad request * 500: @@ -46,4 +33,44 @@ const router: Router = Router(); */ router.get('/', verificationController.verifyContract); +/** + * @openapi + * /verification/kyc: + * post: + * summary: Submit KYC/AML verification and create an attestation + * tags: + * - Verification + */ +router.post('/kyc', verificationController.submitKycVerification); + +/** + * @openapi + * /verification/kyc/{userAddress}: + * get: + * summary: Get KYC/AML verification status for a user + * tags: + * - Verification + */ +router.get('/kyc/:userAddress', verificationController.getKycStatus); + +/** + * @openapi + * /verification/kyc/{userAddress}/proof: + * get: + * summary: Return a privacy-preserving proof summary without PII + * tags: + * - Verification + */ +router.get('/kyc/:userAddress/proof', verificationController.getPrivacyProof); + +/** + * @openapi + * /verification/kyc/{userAddress}/revoke: + * post: + * summary: Revoke an existing verification attestation + * tags: + * - Verification + */ +router.post('/kyc/:userAddress/revoke', verificationController.revokeKycAttestation); + export default router; From 541187f0e58482b55ec3540be9f97d44eca4a856 Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:10:42 +0100 Subject: [PATCH 6/7] feat(api): use sha256 attestation hashes --- api/src/services/verification.service.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/api/src/services/verification.service.ts b/api/src/services/verification.service.ts index 193a5512..f6d5372a 100644 --- a/api/src/services/verification.service.ts +++ b/api/src/services/verification.service.ts @@ -1,3 +1,5 @@ +import crypto from 'crypto'; + export type VerificationLevel = 'basic' | 'enhanced' | 'institutional'; export type VerificationProvider = 'civic' | 'fractal_id' | 'mock'; export type VerificationStatus = 'pending' | 'verified' | 'rejected' | 'revoked' | 'expired'; @@ -28,8 +30,10 @@ export interface VerificationAttestation { const attestations = new Map(); const WATCHLIST_TERMS = ['sanctioned', 'blocked', 'watchlist']; -function annualExpiry(level: VerificationLevel): Date { +function expiryForLevel(level: VerificationLevel): Date { const now = new Date(); + // Basic and institutional attestations require annual re-verification; + // enhanced attestations are reviewed biannually. const months = level === 'enhanced' ? 6 : 12; now.setMonth(now.getMonth() + months); return now; @@ -45,14 +49,9 @@ function buildAttestationHash(request: VerificationRequest, issuedAt: string): s issuedAt, ].join(':'); - // Avoid storing raw PII. This deterministic, non-cryptographic fallback is - // sufficient for local/dev attestations; production should replace this with - // a provider-signed on-chain attestation transaction hash. - let hash = 0; - for (let i = 0; i < input.length; i += 1) { - hash = (hash * 31 + input.charCodeAt(i)) >>> 0; - } - return `att_${hash.toString(16).padStart(8, '0')}`; + // Privacy preserving: no PII is stored in the proof. Production providers can + // replace this with a provider-signed on-chain attestation transaction hash. + return `att_${crypto.createHash('sha256').update(input).digest('hex')}`; } function screenAml(request: VerificationRequest): { amlScreened: boolean; watchlistHit: boolean } { @@ -66,7 +65,7 @@ function screenAml(request: VerificationRequest): { amlScreened: boolean; watchl export async function submitVerification(request: VerificationRequest): Promise { const { amlScreened, watchlistHit } = screenAml(request); const issuedAt = new Date().toISOString(); - const expiresAt = annualExpiry(request.level).toISOString(); + const expiresAt = expiryForLevel(request.level).toISOString(); const attestation: VerificationAttestation = { userAddress: request.userAddress, provider: request.provider, From e46afed9143ac55bad5e914e603ce80331f4957c Mon Sep 17 00:00:00 2001 From: Emmanuel Obiajulu Okoye <64269783+Obiajulu-gif@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:11:20 +0100 Subject: [PATCH 7/7] docs(gas): add benchmark dashboard guide --- stellar-lend/benchmarks/gas-dashboard.md | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 stellar-lend/benchmarks/gas-dashboard.md diff --git a/stellar-lend/benchmarks/gas-dashboard.md b/stellar-lend/benchmarks/gas-dashboard.md new file mode 100644 index 00000000..a336914a --- /dev/null +++ b/stellar-lend/benchmarks/gas-dashboard.md @@ -0,0 +1,37 @@ +# Gas Benchmark Dashboard + +This dashboard documents how to read and maintain StellarLend gas benchmark results. + +## CI gate + +The workflow at `.github/workflows/gas-benchmarks.yml` runs the benchmark binary, uploads `benchmark-results.json`, and fails the build when an operation exceeds its configured gas budget. A practical regression policy is: + +- fail when any function exceeds its explicit budget; +- review any operation whose CPU instruction cost increases by more than 10% from the committed baseline; +- update `benchmarks/baseline.json` only after an intentional optimization or feature change is reviewed. + +## Result fields + +Each benchmark result should include: + +- `contract` +- `operation` +- `instructions` +- `memory_bytes` +- `budget` +- `within_budget` +- `storage_reads` +- `storage_writes` +- `cross_contract_calls` + +## Optimization checklist + +- Prefer packed storage records over many small keys when values are read together. +- Cache config records loaded more than once inside the same entrypoint. +- Avoid repeated cross-contract calls in loops. +- Emit compact events and avoid duplicating data already present in storage. +- Add a focused benchmark before and after any storage-layout change. + +## Storage slot analysis + +When a benchmark regresses, inspect whether the operation added persistent keys, duplicate reads, or larger serialized values. Cross-contract accounting should be called out separately because token/oracle calls can dominate protocol-level gas.