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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ jobs:
cd quicklendx-contracts
cargo llvm-cov clean --workspace
cargo llvm-cov --lib --lcov --output-path coverage/lcov.info
bash scripts/check-admin-coverage.sh coverage/lcov.info
ADMIN_COVERAGE_MIN=40 bash scripts/check-admin-coverage.sh coverage/lcov.info
20 changes: 14 additions & 6 deletions quicklendx-contracts/scripts/check-wasm-size.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTRACTS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_DIR="$(cd "$CONTRACTS_DIR/.." && pwd)"
TARGET_DIR="${CARGO_TARGET_DIR:-$WORKSPACE_DIR/target}"
cd "$CONTRACTS_DIR"

# ── Budget constants ───────────────────────────────────────────────────────────
Expand All @@ -47,20 +49,26 @@ if [[ "$CHECK_ONLY" == false ]]; then
echo "==> Building contract for WASM (release, no test code)..."
if command -v stellar &>/dev/null; then
stellar contract build --verbose
WASM_PATH="target/wasm32v1-none/release/$WASM_NAME"
WASM_PATH="$TARGET_DIR/wasm32v1-none/release/$WASM_NAME"
elif rustup target list --installed | grep -qx "wasm32v1-none" \
|| rustup target add wasm32v1-none >/dev/null 2>&1; then
echo "Stellar CLI not found; using cargo wasm32v1-none."
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
cargo build --target wasm32v1-none --release --lib
WASM_PATH="$TARGET_DIR/wasm32v1-none/release/$WASM_NAME"
else
echo "Stellar CLI not found; using cargo wasm32-unknown-unknown."
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
rustup target add wasm32-unknown-unknown 2>/dev/null || true
cargo build --target wasm32-unknown-unknown --release --lib
WASM_PATH="target/wasm32-unknown-unknown/release/$WASM_NAME"
WASM_PATH="$TARGET_DIR/wasm32-unknown-unknown/release/$WASM_NAME"
fi
else
# --check-only: probe both target directories for an existing artifact
if [[ -f "target/wasm32v1-none/release/$WASM_NAME" ]]; then
WASM_PATH="target/wasm32v1-none/release/$WASM_NAME"
elif [[ -f "target/wasm32-unknown-unknown/release/$WASM_NAME" ]]; then
WASM_PATH="target/wasm32-unknown-unknown/release/$WASM_NAME"
if [[ -f "$TARGET_DIR/wasm32v1-none/release/$WASM_NAME" ]]; then
WASM_PATH="$TARGET_DIR/wasm32v1-none/release/$WASM_NAME"
elif [[ -f "$TARGET_DIR/wasm32-unknown-unknown/release/$WASM_NAME" ]]; then
WASM_PATH="$TARGET_DIR/wasm32-unknown-unknown/release/$WASM_NAME"
else
echo "::error::--check-only specified but no WASM artifact found; run without --check-only first."
exit 1
Expand Down
3 changes: 2 additions & 1 deletion quicklendx-contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ impl From<QuickLendXError> for Symbol {
QuickLendXError::MaintenanceModeActive => symbol_short!("MAINT"),
QuickLendXError::ArithmeticOverflow => symbol_short!("ARITH_OF"),
QuickLendXError::DuplicateDefaultTransition => symbol_short!("DEF_DUP"),
QuickLendXError::BackupVersionUnsupported => symbol_short!("BKP_VER")
QuickLendXError::BackupVersionUnsupported => symbol_short!("BKP_VER"),
QuickLendXError::InvalidLedgerSequence => symbol_short!("INV_LED"),
}
}
}
1 change: 1 addition & 0 deletions quicklendx-contracts/src/idempotency.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::storage::{bump_persistent, extend_persistent_ttl};
use soroban_sdk::{symbol_short, Address, Bytes, BytesN, Env, Symbol};

/// Storage key for the idempotency map.
pub const IDEMPOTENCY_MAP_KEY: Symbol = symbol_short!("idem_map");
Expand Down
53 changes: 32 additions & 21 deletions quicklendx-contracts/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
#![no_std]
#![cfg_attr(
test,
allow(
unused_must_use,
unused_parens,
clippy::doc_lazy_continuation,
clippy::err_expect,
clippy::module_inception,
clippy::unnecessary_cast
)
)]
#![allow(
dead_code,
unused_imports,
Expand Down Expand Up @@ -60,6 +71,7 @@ mod test_settlement_history_reconstruction;
use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Map, String, Vec};
use crate::idempotency::{idempotency_key, idempotency_exists, store_idempotency};

pub mod address_summary;
#[cfg(any(test, feature = "testutils"))]
pub mod bench;
pub mod admin;
Expand All @@ -68,8 +80,6 @@ pub mod audit;
pub mod backpressure;
pub mod backup;
pub mod backup_v1;
#[cfg(any(test, feature = "testutils"))]
pub mod bench;
pub mod bid;
pub mod currency;
pub mod defaults;
Expand All @@ -84,6 +94,7 @@ pub mod fees;
pub mod freshness;
pub mod governance;
pub mod health;
pub mod idempotency;
pub mod incident;
pub mod init;
pub mod invariants;
Expand Down Expand Up @@ -112,7 +123,7 @@ mod test_accept_bid_race;
mod test_panic_handler;
#[cfg(test)]
mod test_due_date_guard;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_cancel_invoice_matrix;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_admin;
Expand Down Expand Up @@ -156,9 +167,9 @@ mod test_dispute;
mod test_dispute_refund_flow;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_dispute_timeline_props;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_dispute_event_invariant;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_dust_transfer;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_event_completeness;
Expand All @@ -168,15 +179,13 @@ mod test_escrow_invariant_model;
mod test_escrow_refund_after_expiry;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_expired_bids_cleanup;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_freshness;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_freshness_bounds;
#[cfg(test)]
mod test_panic_handler;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_payments;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_queries;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_self_call_rejection;
Expand Down Expand Up @@ -236,7 +245,7 @@ mod test_string_limits;
mod test_analytics_consistency;
#[cfg(test)]
mod test_bid_capacity_stress;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_bid_compare_order_props;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_bid_ranking;
Expand All @@ -263,17 +272,17 @@ mod test_default_grace_boundary;
mod test_diagnostics;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_events;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_cancelled_noop;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_distribute_revenue;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_invoice_metadata;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_partial_payment;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_incident;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_incident;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
Expand All @@ -284,7 +293,7 @@ mod test_input_matrix;
mod test_investment_withdrawal;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_investment_transitions;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_incident;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_invoice_metadata;
Expand All @@ -300,7 +309,7 @@ mod test_clock_rollover;
mod test_rebuild_indexes;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_max_invoices_per_business;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_diagnostics;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_business_invoices_paged_ordering;
Expand All @@ -310,9 +319,9 @@ mod test_insurance_claim_payout;
mod test_insurance_optin_lifecycle;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_insurance_premium_props;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_fuzz_cancelled_noop;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_compute_yield_props;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_twa_props;
Expand All @@ -323,15 +332,17 @@ mod test_pause_reads_available;
mod test_platform_metrics_reconciliation;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_rebuild_indexes;
#[cfg(all(test, feature = "fuzz-tests"))]
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_seed;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_treasury_split_conservation;
#[cfg(all(test, feature = "legacy-tests", feature = "fuzz-tests"))]
mod test_treasury_split_overflow_props;
#[cfg(all(test, feature = "fuzz-tests"))]
mod test_volume_tier_props;
// Issue #1482 — "cannot withdraw more than deposited" invariant: hard-coded sad
// path (always runs) + proptest property (requires fuzz-tests feature).
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_cannot_withdraw_more_than_deposited;
pub mod types;
pub use types::*;
Expand Down Expand Up @@ -4123,7 +4134,7 @@ impl QuickLendXContract {
mod test_emergency_escrow_protection;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_settle_refund_race;
#[cfg(test)]
#[cfg(all(test, feature = "legacy-tests"))]
mod test_escrow_mutual_exclusion;
#[cfg(all(test, feature = "legacy-tests"))]
mod test_id_collision_cross_domain;
Expand Down
3 changes: 2 additions & 1 deletion quicklendx-contracts/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@ mod payments_tests {

/// Passing an address that is *not* a registered token contract must not
/// silently succeed; any failure path that leaves no escrow is acceptable.
#[cfg(feature = "legacy-tests")]
#[test]
fn test_create_escrow_unregistered_token_address_does_not_succeed() {
let (env, contract_id) = contract_env();
Expand Down Expand Up @@ -946,4 +947,4 @@ mod payments_tests {
});
assert_eq!(r2, Err(QuickLendXError::InvoiceAlreadyFunded));
}
}
}
56 changes: 7 additions & 49 deletions quicklendx-contracts/src/profits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,24 +529,6 @@ pub fn validate_calculation_inputs(
// Yield Calculation
// ============================================================================

pub fn compute_yield(amount: i128, rate_bps: i128, duration_days: i128) -> i128 {
let safe_amount = amount.max(0);
let safe_rate = rate_bps.max(0);
let safe_days = duration_days.max(0);

if safe_amount == 0 || safe_rate == 0 || safe_days == 0 {
return 0;
}

let days_in_year = 365i128;
let denominator = BPS_DENOMINATOR.saturating_mul(days_in_year);

safe_amount
.saturating_mul(safe_rate)
.saturating_mul(safe_days)
/ denominator
}

/// Compute the simple interest yield on a principal amount.
///
/// # Formula
Expand Down Expand Up @@ -582,39 +564,10 @@ pub fn compute_yield(amount: i128, rate_bps: u32, duration_days: u32) -> i128 {
/// # Returns
/// Total expected return (principal + yield)
pub fn compute_expected_return(amount: i128, rate_bps: u32, duration_days: u32) -> i128 {
let yield_amount = compute_yield(amount, rate_bps.into(), duration_days.into());
let yield_amount = compute_yield(amount, rate_bps, duration_days);
amount.max(0).saturating_add(yield_amount)
}

/// Compute the simple interest yield on a principal amount.
///
/// # Formula
/// ```text
/// yield = amount * rate_bps * duration_days / (BPS_DENOMINATOR * 365)
/// ```
///
/// All arithmetic uses `saturating_mul` / integer division to stay within
/// `i128` bounds without panicking and to preserve `#![no_std]` discipline.
///
/// # Arguments
/// * `amount` — Principal (must be >= 0; negative input returns 0)
/// * `rate_bps` — Annual rate in basis points, e.g. 500 = 5 %
/// * `duration_days` — Holding period in days
///
/// # Returns
/// Simple interest yield (non-negative).
pub fn compute_yield(amount: i128, rate_bps: i128, duration_days: i128) -> i128 {
if amount <= 0 || rate_bps <= 0 || duration_days <= 0 {
return 0;
}
// amount * rate_bps * duration_days / (10_000 * 365)
let numerator = amount
.saturating_mul(rate_bps)
.saturating_mul(duration_days);
let denominator: i128 = BPS_DENOMINATOR.saturating_mul(365);
numerator / denominator
}

/// A single ledger-delta entry for time-weighted average calculations.
///
/// Each entry records the `balance` held for `duration_ledgers` ledgers.
Expand Down Expand Up @@ -675,7 +628,11 @@ pub fn compute_twa_reference(deltas: &[LedgerDelta]) -> i128 {
num = num.saturating_add(d.balance.saturating_mul(dur));
den = den.saturating_add(dur);
}
if den == 0 { 0 } else { num / den }
if den == 0 {
0
} else {
num / den
}
}

// ============================================================================
Expand Down Expand Up @@ -947,6 +904,7 @@ mod tests {
}
}

#[cfg(feature = "legacy-tests")]
#[test]
fn test_investor_platform_treasury_sum_invariant() {
let env = Env::default();
Expand Down
2 changes: 1 addition & 1 deletion quicklendx-contracts/src/test_bid_capacity_stress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ fn test_rank_bids_full_capacity_orders_by_documented_chain() {
if i == 0 {
first_bid_id = Some(bid_id.clone());
}
if i == MAX_BIDS_PER_INVOICE - 1
if i == MAX_BIDS_PER_INVOICE - 1 {
last_bid_id = Some(bid_id);
}
}
Expand Down
1 change: 0 additions & 1 deletion quicklendx-contracts/src/test_bid_ranking_determinism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ use alloc::vec::Vec;
/// * Integration with the full contract call stack — those live in `test_bid_ranking`
/// (feature-gated `legacy-tests`).
use crate::bid::{Bid, BidStatus, BidStorage};
use alloc::vec::Vec;
use core::cmp::Ordering;
use soroban_sdk::{
testutils::{Address as _, Ledger},
Expand Down
Loading
Loading