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
20 changes: 20 additions & 0 deletions quicklendx-contracts/src/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
//! See `src/test_audit.rs` for comprehensive integrity tests.

use crate::errors::QuickLendXError;
use crate::storage::extend_instance_ttl;
use crate::types::{Invoice, InvoiceStatus};
use soroban_sdk::{
contracttype, symbol_short, xdr::ToXdr, Address, Bytes, BytesN, Env, String, Symbol, Vec,
Expand Down Expand Up @@ -541,6 +542,25 @@ impl AuditStorage {
.unwrap_or_else(|| Vec::new(env))
}

pub fn extend_invoice_audit_ttl(env: &Env, invoice_id: &BytesN<32>) -> u32 {
let trail = Self::get_invoice_audit_trail(env, invoice_id);
if trail.len() == 0 {
return 0;
}

let mut refreshed = 1u32;
for audit_id in trail.iter() {
if Self::get_audit_entry(env, &audit_id).is_some() {
refreshed = refreshed.saturating_add(1);
}
}

// Audit data uses instance storage, whose TTL is extended for the whole
// instance rather than one key at a time.
extend_instance_ttl(env);
refreshed
}

/// Get audit entries by operation type
pub fn get_audit_entries_by_operation(
env: &Env,
Expand Down
30 changes: 30 additions & 0 deletions quicklendx-contracts/src/bid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,36 @@ impl BidStorage {
bids
}

pub fn extend_invoice_bid_ttl(env: &Env, invoice_id: &BytesN<32>) -> u32 {
let count_key = Self::invoice_bid_count_key(invoice_id);
let count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0);
let mut refreshed = 0u32;

if count > 0 {
extend_persistent_ttl(env, &count_key);
refreshed = refreshed.saturating_add(1);
}

let mut idx: u32 = 0;
while idx < count {
let entry_key = Self::invoice_bid_entry_key(invoice_id, idx);
let bid_id: Option<BytesN<32>> = env.storage().persistent().get(&entry_key);
if let Some(bid_id) = bid_id {
extend_persistent_ttl(env, &entry_key);
refreshed = refreshed.saturating_add(1);

let bid: Option<Bid> = env.storage().persistent().get(&bid_id);
if bid.is_some() {
extend_persistent_ttl(env, &bid_id);
refreshed = refreshed.saturating_add(1);
}
}
idx += 1;
}

refreshed
}

pub fn get_active_bid_count(env: &Env, invoice_id: &BytesN<32>) -> u32 {
let _ = Self::refresh_expired_bids(env, invoice_id);
let bid_ids = Self::get_bids_for_invoice(env, invoice_id);
Expand Down
23 changes: 23 additions & 0 deletions quicklendx-contracts/src/investment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,29 @@ impl InvestmentStorage {
.filter(|inv| inv.invoice_id == *invoice_id)
}

pub fn extend_invoice_investment_ttl(env: &Env, invoice_id: &BytesN<32>) -> u32 {
let index_key = Self::invoice_index_key(invoice_id);
let investment_id: Option<BytesN<32>> = env.storage().persistent().get(&index_key);
let mut refreshed = 0u32;

if let Some(investment_id) = investment_id {
extend_persistent_ttl(env, &index_key);
refreshed = refreshed.saturating_add(1);

let investment: Option<Investment> = env.storage().persistent().get(&investment_id);
if investment
.as_ref()
.map(|investment| investment.invoice_id == invoice_id.clone())
.unwrap_or(false)
{
extend_persistent_ttl(env, &investment_id);
refreshed = refreshed.saturating_add(1);
}
}

refreshed
}

/// Update an investment, enforcing the transition guard and maintaining the
/// active-investment index so no orphan `Active` records can accumulate.
///
Expand Down
9 changes: 9 additions & 0 deletions quicklendx-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,15 @@ impl QuickLendXContract {
maintenance::MaintenanceControl::extend_protocol_ttl(&env, &admin)
}

/// Admin-only: extends TTL for one invoice and its associated storage keys.
pub fn extend_invoice_ttl(
env: Env,
admin: Address,
invoice_id: BytesN<32>,
) -> Result<maintenance::ExtendReport, QuickLendXError> {
maintenance::MaintenanceControl::extend_invoice_ttl(&env, &admin, &invoice_id)
}

/// Admin-gated protocol heartbeat. Authenticates `admin` as the stored protocol
/// admin, then runs every composed invariant check read-only.
pub fn invariant_self_check(
Expand Down
110 changes: 66 additions & 44 deletions quicklendx-contracts/src/maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
//! - Admin operations that toggle maintenance mode itself are always allowed.

use crate::admin::AdminStorage;
use crate::audit::AuditStorage;
use crate::bid::BidStorage;
use crate::currency::CurrencyWhitelist;
use crate::errors::QuickLendXError;
use crate::investment::InvestmentStorage;
use crate::payments::EscrowStorage;
use crate::settlement;
use crate::storage::{extend_persistent_ttl, DataKey, InvoiceStorage};
use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Symbol};
use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, String, Symbol};

/// Storage key for the maintenance mode boolean flag.
pub const MAINTENANCE_MODE_KEY: Symbol = symbol_short!("maint");
Expand All @@ -37,6 +39,9 @@ pub const MAX_REASON_LEN: u32 = 256;
/// * `escrows_refreshed` — Number of escrow records extended.
/// * `currencies_refreshed` — Number of whitelisted currency entries extended.
///
/// The targeted invoice path also fills payment and audit counts when those
/// record sets exist.
///
/// # Idempotency
/// Calling `extend_protocol_ttl` multiple times within the same ledger is safe:
/// `extend_ttl` is itself idempotent, and the report always reflects the current
Expand All @@ -53,13 +58,45 @@ pub struct ExtendReport {
pub investments_refreshed: u32,
/// Number of escrow records whose TTL was extended.
pub escrows_refreshed: u32,
/// Number of payment storage keys whose TTL was extended.
pub payments_refreshed: u32,
/// Number of audit storage keys covered by an instance TTL extension.
pub audit_refreshed: u32,
/// Number of whitelisted currency entries whose TTL was extended.
pub currencies_refreshed: u32,
}

pub struct MaintenanceControl;

impl MaintenanceControl {
fn empty_report() -> ExtendReport {
ExtendReport {
invoices_refreshed: 0,
bids_refreshed: 0,
investments_refreshed: 0,
escrows_refreshed: 0,
payments_refreshed: 0,
audit_refreshed: 0,
currencies_refreshed: 0,
}
}

fn emit_ttl_event(env: &Env, kind: &str, count: u32) {
if count > 0 {
crate::events::emit_ttl_extended(env, &String::from_str(env, kind), count);
}
}

fn emit_extend_report_events(env: &Env, report: &ExtendReport) {
Self::emit_ttl_event(env, "invoice", report.invoices_refreshed);
Self::emit_ttl_event(env, "bid", report.bids_refreshed);
Self::emit_ttl_event(env, "investment", report.investments_refreshed);
Self::emit_ttl_event(env, "escrow", report.escrows_refreshed);
Self::emit_ttl_event(env, "payment", report.payments_refreshed);
Self::emit_ttl_event(env, "audit", report.audit_refreshed);
Self::emit_ttl_event(env, "currency", report.currencies_refreshed);
}

/// Return `true` if the protocol is currently in maintenance mode.
pub fn is_maintenance_mode(env: &Env) -> bool {
env.storage()
Expand Down Expand Up @@ -147,13 +184,7 @@ impl MaintenanceControl {
) -> Result<ExtendReport, QuickLendXError> {
AdminStorage::require_admin(env, admin)?;

let mut report = ExtendReport {
invoices_refreshed: 0,
bids_refreshed: 0,
investments_refreshed: 0,
escrows_refreshed: 0,
currencies_refreshed: 0,
};
let mut report = Self::empty_report();

// 1. Extend Invoices
for invoice_id in InvoiceStorage::get_all_invoice_ids(env).iter() {
Expand Down Expand Up @@ -187,42 +218,33 @@ impl MaintenanceControl {
report.currencies_refreshed += 1;
}

// Emit events for each kind that was refreshed
if report.invoices_refreshed > 0 {
crate::events::emit_ttl_extended(
env,
&String::from_str(env, "invoice"),
report.invoices_refreshed,
);
}
if report.bids_refreshed > 0 {
crate::events::emit_ttl_extended(
env,
&String::from_str(env, "bid"),
report.bids_refreshed,
);
}
if report.investments_refreshed > 0 {
crate::events::emit_ttl_extended(
env,
&String::from_str(env, "investment"),
report.investments_refreshed,
);
}
if report.escrows_refreshed > 0 {
crate::events::emit_ttl_extended(
env,
&String::from_str(env, "escrow"),
report.escrows_refreshed,
);
}
if report.currencies_refreshed > 0 {
crate::events::emit_ttl_extended(
env,
&String::from_str(env, "currency"),
report.currencies_refreshed,
);
}
Self::emit_extend_report_events(env, &report);

Ok(report)
}

/// Admin-only: extends TTL for one invoice and its associated storage keys.
pub fn extend_invoice_ttl(
env: &Env,
admin: &Address,
invoice_id: &BytesN<32>,
) -> Result<ExtendReport, QuickLendXError> {
AdminStorage::require_admin(env, admin)?;
InvoiceStorage::get_invoice(env, invoice_id).ok_or(QuickLendXError::InvoiceNotFound)?;

let mut report = Self::empty_report();

extend_persistent_ttl(env, &DataKey::Invoice(invoice_id.clone()));
report.invoices_refreshed = 1;

report.bids_refreshed = BidStorage::extend_invoice_bid_ttl(env, invoice_id);
report.investments_refreshed =
InvestmentStorage::extend_invoice_investment_ttl(env, invoice_id);
report.escrows_refreshed = EscrowStorage::extend_invoice_escrow_ttl(env, invoice_id);
report.payments_refreshed = settlement::extend_invoice_payment_ttl(env, invoice_id);
report.audit_refreshed = AuditStorage::extend_invoice_audit_ttl(env, invoice_id);

Self::emit_extend_report_events(env, &report);

Ok(report)
}
Expand Down
31 changes: 26 additions & 5 deletions quicklendx-contracts/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,29 @@ impl EscrowStorage {
}
}

pub fn extend_invoice_escrow_ttl(env: &Env, invoice_id: &BytesN<32>) -> u32 {
let invoice_key = (symbol_short!("escrow"), invoice_id);
let escrow_id: Option<BytesN<32>> = env.storage().persistent().get(&invoice_key);
let mut refreshed = 0u32;

if let Some(escrow_id) = escrow_id {
extend_persistent_ttl(env, &invoice_key);
refreshed = refreshed.saturating_add(1);

let escrow: Option<Escrow> = env.storage().persistent().get(&escrow_id);
if escrow
.as_ref()
.map(|escrow| escrow.invoice_id == invoice_id.clone())
.unwrap_or(false)
{
extend_persistent_ttl(env, &escrow_id);
refreshed = refreshed.saturating_add(1);
}
}

refreshed
}

pub fn update_escrow(env: &Env, escrow: &Escrow) {
env.storage().persistent().set(&escrow.escrow_id, escrow);
extend_persistent_ttl(env, &escrow.escrow_id);
Expand Down Expand Up @@ -491,8 +514,7 @@ pub fn create_escrow(
/// * [`QuickLendXError::TokenTransferFailed`] - the token contract panicked; escrow status is
/// **not** updated so the release can be safely retried.
pub fn release_escrow(env: &Env, invoice_id: &BytesN<32>) -> Result<(), QuickLendXError> {
let mut escrow = EscrowStorage::get_escrow_by_invoice(env, invoice_id)
.unwrap();
let mut escrow = EscrowStorage::get_escrow_by_invoice(env, invoice_id).unwrap();

if escrow.status != EscrowStatus::Held {
// Prevents repeated release (idempotency)
Expand Down Expand Up @@ -547,8 +569,7 @@ pub fn release_escrow(env: &Env, invoice_id: &BytesN<32>) -> Result<(), QuickLen
/// * [`QuickLendXError::TokenTransferFailed`] - the token contract panicked; escrow status is
/// **not** updated so the refund can be safely retried.
pub fn refund_escrow(env: &Env, invoice_id: &BytesN<32>) -> Result<(), QuickLendXError> {
let mut escrow = EscrowStorage::get_escrow_by_invoice(env, invoice_id)
.unwrap();
let mut escrow = EscrowStorage::get_escrow_by_invoice(env, invoice_id).unwrap();

if escrow.status != EscrowStatus::Held {
return Err(QuickLendXError::InvalidStatus);
Expand Down Expand Up @@ -946,4 +967,4 @@ mod payments_tests {
});
assert_eq!(r2, Err(QuickLendXError::InvoiceAlreadyFunded));
}
}
}
Loading