The QuickLendX platform implements a configurable fee system with treasury routing capabilities. The system applies a default 2% platform fee on invoice transactions during settlement, with fees automatically routed to a configured treasury address.
- Default Rate: 2% (200 basis points)
- Maximum Rate: 10% (1000 basis points)
- Admin-only Configuration: Only platform administrators can modify fee rates
- Real-time Updates: Fee changes take effect immediately for new transactions
To prevent protocol misconfiguration or malicious fee hikes, the platform fee is strictly capped at the contract level.
- Hard Cap: Any update attempt exceeding 1000 BPS (10%) will result in an
InvalidFeeBasisPointserror. - Integrity: Every update emits a
platform_fee_config_updated(topic:fee_cfg) event containing bothold_fee_bpsandnew_fee_bpsto ensure off-chain auditability. - Optimization: If the proposed new fee is identical to the current fee, the system performs a "no-op," meaning no storage is updated and no event is emitted.
- Automatic Routing: Platform fees are automatically routed to the configured treasury address
- Fallback Mechanism: If no treasury is configured, fees are sent to the contract address
- Secure Configuration: Only administrators can set or update the treasury address
- Event Tracking: All treasury configuration changes are logged via
treasury_configured(topic:trs_cfg) events.
The protocol supports various types of fees (Platform, Processing, Verification, LatePayment, etc.) through configurable fee structures.
- Strict Validation: All fee structures must respect a hard cap of 1000 BPS for the base fee.
- Event Audit: Updates to any fee structure emit a
fee_structure_updated(topic:fee_str) event including theFeeType, the old BPS, and the new BPS.
- Tiered Discounts: User transaction volume determines a discount applied to fee calculations: Standard (0), Silver (5%), Gold (10%), Platinum (15%).
- Tier Thresholds: Volume is accumulated via
update_user_transaction_volume.
- Admin-only Config: Fee structures and platform fee BPS are updated only by admin.
- Internal Validation:
validate_fee_params(base_fee_bps, min_fee, max_fee)enforces:base_fee_bps <= 1000(10% max)min_fee >= 0max_fee >= min_fee
- Error Codes: Rejection of invalid BPS returns
InvalidFeeBasisPoints(Contract Error 105).
The platform enforces strict consistency validations on min/max fee bounds per fee type and across the entire fee structure system to prevent misconfiguration and ensure reasonable fee scaling.
The validate_fee_structure_consistency() function enforces the following rules for each fee type individually:
-
Range Validity:
min_fee <= max_fee- Ensures the fee bounds define a valid range where all calculated fees fit.
- Violation returns
InvalidAmounterror.
-
Non-negative Values: Both
min_feeandmax_feemust be >= 0- Fees cannot be negative (that would represent a rebate, not a fee).
- Violation returns
InvalidAmounterror.
-
Reasonable Bounds:
max_feemust not exceed 100x the base fee calculation- For Platform, Processing, and Verification fees: max ≤ base_fee_bps × 100 × 100
- For EarlyPayment and LatePayment fees: max ≤ base_fee_bps × 500 × 100 (more flexible for incentives/penalties)
- Prevents fee structures where the cap is disproportionate to the base rate.
- Violation returns
InvalidFeeConfigurationerror.
-
Absolute Protocol Maximum:
max_fee <= 10,000,000,000,000(10M stroops)- Hard cap prevents fees from consuming entire user balances.
- Protects against configuration errors or overflow scenarios.
- Violation returns
InvalidFeeConfigurationerror.
The validate_cross_fee_consistency() function enforces invariants across multiple fee structures:
-
LatePayment Floor Rule: LatePayment fees must not undercut Platform fees
- If a LatePayment fee is configured, its
min_feemust not be less than the Platform fee'smin_fee. - Ensures late payment penalties don't accidentally become cheaper than regular payments.
- Violation returns
InvalidFeeConfigurationerror.
- If a LatePayment fee is configured, its
-
Total Active Min Fees Limit: Sum of all active fee structures'
min_feemust not exceed 2,500,000,000,000 (2.5M stroops)- Prevents misconfiguration where multiple fee types combine to create excessive minimum charges.
- Formula:
total_active_min_fees = Σ(min_fee for all active fee types) <= PROTOCOL_MAX_TOTAL_MIN_FEES - Violation returns
InvalidFeeConfigurationerror.
-
No Type Overlap: Each fee type serves a distinct purpose
- Platform fees for general transaction overhead
- Processing fees for specialized processing
- Verification fees for identity/business verification
- EarlyPayment fees for incentivizing early repayment
- LatePayment fees for penalizing late repayment
All consistency checks are performed in update_fee_structure() before any state mutations:
pub fn update_fee_structure(
env: &Env,
admin: &Address,
fee_type: FeeType,
base_fee_bps: u32,
min_fee: i128,
max_fee: i128,
is_active: bool,
) -> Result<FeeStructure, QuickLendXError> {
admin.require_auth();
if base_fee_bps > MAX_FEE_BPS {
return Err(QuickLendXError::InvalidFeeBasisPoints);
}
// Apply per-type consistency checks
Self::validate_fee_structure_consistency(
&fee_type,
base_fee_bps,
min_fee,
max_fee
)?;
// Apply cross-type consistency checks
Self::validate_cross_fee_consistency(env, &fee_type, min_fee, max_fee)?;
// ... continue with fee structure update
}| Validation | Error Code | Interpretation |
|---|---|---|
| min_fee > max_fee | InvalidAmount |
Invalid range; bounds must respect ordering |
| min_fee < 0 or max_fee < 0 | InvalidAmount |
Negative fees not allowed |
| max_fee > protocol limit | InvalidFeeConfiguration |
Exceeds absolute protocol bound |
| max_fee > 100x base_fee | InvalidFeeConfiguration |
Unreasonable scaling for type |
| LatePayment min < Platform min | InvalidFeeConfiguration |
Late payments undercut regular fees |
| Total active min fees too high | InvalidFeeConfiguration |
Excessive combined minimums |
Valid Configuration:
FeeStructure {
fee_type: Platform,
base_fee_bps: 200, // 2%
min_fee: 100, // 100 stroops
max_fee: 500_000, // Reasonable cap (within 100x multiplier)
is_active: true,
}
Invalid Configuration (max_fee < min_fee):
FeeStructure {
fee_type: Processing,
base_fee_bps: 100,
min_fee: 1000, // 1000 stroops
max_fee: 500, // ERROR: max < min
is_active: true,
}
Invalid Configuration (exceeds protocol maximum):
FeeStructure {
fee_type: Verification,
base_fee_bps: 100,
min_fee: 100,
max_fee: 15_000_000_000_000, // ERROR: > 10M stroops absolute max
is_active: true,
}
pub struct PlatformFeeConfig {
pub fee_bps: u32,
pub treasury_address: Option<Address>,
pub updated_at: u64,
pub updated_by: Address,
}pub struct FeeStructure {
pub fee_type: FeeType,
pub base_fee_bps: u32,
pub min_fee: i128,
pub max_fee: i128,
pub is_active: bool,
pub updated_at: u64,
pub updated_by: Address,
}-
configure_treasury(treasury_address: Address)- Sets the treasury address for fee routing.
- Emits
trs_cfgevent.
-
update_platform_fee_bps(new_fee_bps: u32)- Updates the platform fee rate.
- Enforces 10% hard cap.
- Emits
fee_cfgevent with transition details.
-
update_fee_structure(fee_type, base_fee_bps, min_fee, max_fee, is_active)- Updates specific fee mechanics.
- Enforces 10% hard cap on
base_fee_bps. - Emits
fee_strevent.
- Fee Calculation: System calculates platform fee based on profit.
- Fund Distribution: Investor receives
payment_amount - platform_fee. Treasury receivesplatform_fee.
Core logic in profits.rs ensures:
- No dust:
investor_return + platform_fee == safe_payment. - Overflow-safe arithmetic: Uses saturating i128 math.
- Investor-favored rounding: Integer floor division.
All administrative functions require admin.require_auth().
Strict boundary checks prevent "silent misconfiguration" where a typo could lead to excessive fees.
| Topic | Event Name | Payload | Rationale |
|---|---|---|---|
fee_cfg |
Platform Fee Updated | (old_bps, new_bps, admin, ts) |
Tracks platform-wide fee changes |
fee_str |
Fee Structure Updated | (fee_type, old_bps, new_bps, admin, ts) |
Tracks specific structural changes |
trs_cfg |
Treasury Configured | (treasury_addr, admin, ts) |
Tracks where funds are routed |
fee_upd |
Legacy Profit Fee | (bps, ts, admin) |
(Used in profits.rs module) |
InvalidFeeBasisPoints: Rejection of BPS > 1000.InvalidAmount: Rejection of negative amounts or inconsistent min/max bounds.NotAdmin: Unauthorized modification attempt.InvalidFeeConfiguration: Map sum does not equaltotal_amount, or revenue shares do not sum to 10,000 BPS.StorageKeyNotFound: Reading fee config before the fee system has been initialized.
The system is designed for backward compatibility, with new event structures providing more detail than legacy versions without breaking core settlement logic.
The QuickLendX fee system is validated through extensive automated testing covering volume accumulation, tier transitions, and settlement scenarios. All tests are located in src/test_fees_extended.rs.
The test suite provides 95%+ code coverage with 40+ comprehensive tests covering:
These tests verify that user transaction volumes are correctly tracked and persisted across the contract lifecycle.
test_volume_accumulates_single_transaction: Validates that a single transaction correctly increments total volume and transaction count.test_volume_accumulates_multiple_transactions: Confirms cumulative volume tracking across 3+ sequential transactions with varying amounts.test_volume_persists_after_state_retrieval: Ensures volume data is durably stored and survives state queries and additional updates.test_volume_large_accumulation_no_overflow: Validates saturating arithmetic handles transactions at 10^12 stroops without panicking.test_volume_transaction_count_increments: Confirms transaction counter increments deterministically with each volume update.
Security Validations:
- No integer overflow panics on large amounts
- Volume is monotonically increasing (never decreases)
- Transaction count increments exactly once per call
These tests verify that volume thresholds correctly trigger tier promotions and apply appropriate fee discounts.
test_tier_transition_standard_to_silver: User at 0 volume transitions to Silver (5% discount) at 100 billion stroops threshold.test_tier_transition_silver_to_gold: User at Silver tier transitions to Gold (10% discount) at 500 billion stroops.test_tier_transition_gold_to_platinum: User at Gold tier transitions to Platinum (15% discount) at 1 trillion stroops.test_tier_monotonic_no_downgrade: Confirms tiers never downgrade; Platinum users remain Platinum even after single stroops.test_fee_discount_increases_with_tier: Validates fee amounts decrease monotonically as tiers progress (Standard > Silver > Gold > Platinum).test_tier_discount_values_correct: Confirms exact discount percentages: Standard 0%, Silver 5%, Gold 10%, Platinum 15%.
Security Validations:
- Tier transitions are monotonic (no downgrade or artificial tier reset)
- Threshold crossings are precise (100B, 500B, 1T stroops)
- Discounts compound only on non-LatePayment fees
- Tier state is recoverable via
get_user_volume_data()
These tests simulate real-world invoice settlement sequences with multiple payments, tier changes, and fee recalculations.
test_fee_calculation_consistent_multiple_settlements: Confirms fee amounts remain identical across settlements when tier is unchanged.test_fee_reduction_after_tier_upgrade_settlement: Validates fees decrease after user enters higher tier mid-settlement.test_cumulative_volume_through_settlement_lifecycle: Simulates 3-round settlement process: Standard → Silver → Gold tier progression.test_fee_calculation_deterministic_after_settlements: Calls fee calculation 4 times (3 at same tier, 1 after immaterial volume bump); expects identical results.test_revenue_accumulation_through_settlements: Collects fees across 2 settlements, verifies revenue distribution (50% treasury, 25% developer, 25% platform).test_settlement_with_tier_change_and_fee_update: Combined scenario: tier promotion + platform fee BPS change in same settlement round.
Security Validations:
- Fee calculations are deterministic (same inputs → same outputs)
- Volume accumulation is atomic (no partial updates)
- Revenue collection and distribution balance (no dust or loss)
- Settlement state transitions are idempotent
These tests verify tier-based fee reductions are correctly applied to fee calculations.
test_volume_tier_standard_no_discount: User with 0 volume receives 0% discount (Standard tier).test_fee_discount_percentage_silver_5_percent: Silver tier users receive exactly 5% fee reduction.test_fee_discount_percentage_gold_10_percent: Gold tier users receive exactly 10% fee reduction.test_fee_discount_percentage_platinum_15_percent: Platinum tier users receive exactly 15% fee reduction.
Security Validations:
- Discount percentages are exact (integer math maintains precision)
- Discounts do not apply to LatePayment fees (only penalize)
- Discounts apply before early-payment incentives
These tests ensure fee calculations produce identical results for the same inputs and contract state.
test_transaction_fee_same_inputs_are_deterministic: Same user, amount, timing flags produce identical fees across 3+ calculations.test_rounding_with_odd_amounts: Fee calculations with non-divisible amounts (333, 777 stroops) are consistent and positive.test_transaction_fee_small_amount_uses_minimums_before_modifiers: 1 stroop correctly clamps to min fees (250), then applies early discount (→240).test_transaction_fee_large_amount_uses_maximums_before_tier_discount: 100M stroop amounts clamp to max (1.36M after Platinum discount).
Security Validations:
- Clamping order is deterministic: calculate BPS → clamp to [min, max] → apply tier discount → apply timing modifiers
- Floor division rounding is consistent
- No floating-point precision errors
These tests verify fee system initialization and configuration changes persist correctly.
test_initialize_fee_system_sets_defaults: First initialization creates Platform (200 BPS), Processing (50 BPS), Verification (100 BPS) fee structures.test_multiple_fee_updates_sequence: Updating platform fee to 300 → 500 → 150 BPS is persisted correctly.test_treasury_persists_across_updates: Setting treasury address, then updating fee BPS, preserves treasury routing.test_fee_structures_unchanged_after_rejected_reinit: Updating custom fee structure (e.g., Platform → 300 BPS), then rejecting re-initialization, preserves custom value.
Security Validations:
- Initialization guard prevents re-initialization (idempotency)
- Fee structures survive invalid operations (graceful error handling)
- Treasury configuration is immutable once set (no accidental misrouting)
These tests verify fee collection and revenue split distribution.
test_revenue_all_to_treasury: All collected fees (100% share) route to treasury.test_revenue_all_to_platform: All collected fees (100% share) remain in platform.test_revenue_asymmetric_distribution: 45% treasury / 45% developer / 10% platform split distributes correctly.test_revenue_distribution_sum_equals_collected: Distributed amounts sum exactly to collected amount (no dust).
Security Validations:
- Share amounts sum to 10,000 BPS (100%) exactly
- No fees are lost in rounding (platform gets remainder)
- Distribution is atomic (succeeds or fails completely)
These tests verify early-payment incentives and late-payment penalties.
test_early_payment_fee_reduction: Early payment flag reduces Platform fee by 10% discount.test_late_payment_fee_increase: LatePayment fee structure (when present) increases by 20% surcharge with late flag.test_early_and_late_payment_combined: Early flag applies only to Platform, late flag only to LatePayment (orthogonal).test_payment_timing_combined_early_priority: Early-payment discount takes precedence over all other modifiers.
Security Validations:
- Modifiers apply after min/max clamping
- Modifier order is fixed (tier → early → late)
- Platform fee early discount is always 10% (hard-coded)
- LatePayment surcharge is always 20% (hard-coded)
All tests use realistic stroops amounts and thresholds:
| Tier | Volume Threshold | Fee Discount |
|---|---|---|
| Standard | 0 | 0% |
| Silver | 100_000_000_000 (100B) | 5% |
| Gold | 500_000_000_000 (500B) | 10% |
| Platinum | 1_000_000_000_000 (1T) | 15% |
| Fee Type | Default BPS | Min Fee | Max Fee |
|---|---|---|---|
| Platform | 200 (2%) | 100 | 1_000_000 |
| Processing | 50 (0.5%) | 50 | 500_000 |
| Verification | 100 (1%) | 100 | 100_000 |
cd quicklendx-contracts
# Run all fee tests
cargo test test_fees_extended --lib -- --nocapture
# Run a specific test category
cargo test test_volume_accumulates --lib -- --nocapture
cargo test test_tier_transition --lib -- --nocapture
cargo test test_revenue_accumulation --lib -- --nocapture
# Run with verbose output
cargo test -- --nocapture --test-threads=1The test suite achieves 95%+ code coverage for the fees module:
- FeeManager implementation: 40+ tests
- Volume tracking: 5 core tests + 3 discount tests = 8 pathways
- Tier transitions: 6 tests covering all tier pairs
- Settlement sequences: 6 tests with multi-round scenarios
- Determinism: 4 tests validating idempotency
- Edge cases: Zero amounts, overflow protection, min/max bounds
- Security invariants: Access control, validation, event emission
- Tests use mock authentication (
env.mock_all_auths()), which bypasses real Soroban signature validation. Production deployments rely on Soroban's native access control. - Volume thresholds are hard-coded in the contract. To change tier boundaries, contract redeployment is required.
- Fee discount percentages are fixed and cannot be adjusted per-tier without contract updates.
- Adaptive tiers: Add on-chain voting or governance for tier threshold adjustments.
- Time-decay discounts: Implement volume reset periods (annual reconciliation).
- Per-tier analytics: Track fee savings and platform impact by tier.
- Custom fee structures: Admin-configurable per-invoice-type fee schedules.
This section documents the hard invariants enforced by the contract and validated by the test suite in test_fees_extended.rs and test_profit_fee.rs.
| Property | Value | Error on violation |
|---|---|---|
Maximum fee_bps |
1000 (10%) | InvalidFeeBasisPoints |
Minimum fee_bps |
0 | — |
min_fee |
≥ 0 | InvalidAmount |
max_fee |
≥ min_fee |
InvalidAmount |
Absolute max_fee cap |
10,000,000,000,000 stroops | InvalidFeeConfiguration |
For every call to calculate_profit(investment_amount, payment_amount):
- No dust:
investor_return + platform_fee == payment_amount(exact, no remainder) - Non-negative fee:
platform_fee >= 0 - Fee bounded by profit:
platform_fee <= max(0, payment_amount - investment_amount) - Principal preserved:
investor_return >= 0; whenpayment_amount >= investment_amount,investor_return >= investment_amount - Zero fee on no-profit: when
payment_amount <= investment_amount,platform_fee == 0
All fee arithmetic uses integer floor division (Rust's default / for i128):
platform_fee = floor(gross_profit * fee_bps / BPS_DENOMINATOR)
= (gross_profit * fee_bps) / 10_000 // integer division
Consequences:
- The platform absorbs any fractional remainder (never the investor).
- The smallest profit that yields
fee=1at 2% (200 bps) isprofit=50(since50*200/10000 = 1). profit=49at 2% yieldsfee=0(since49*200/10000 = 0.98, truncated to 0).
fee_bps |
gross_profit |
platform_fee |
Notes |
|---|---|---|---|
| 200 | 1 | 0 | 1*200/10000 = 0.02 → 0 |
| 200 | 49 | 0 | 49*200/10000 = 0.98 → 0 |
| 200 | 50 | 1 | 50*200/10000 = 1.0 → 1 |
| 200 | 9999 | 199 | 9999*200/10000 = 199.98 → 199 |
| 1000 | 100 | 10 | 100*1000/10000 = 10.0 → 10 |
| 999 | 10000 | 999 | 10000*999/10000 = 999.0 → 999 |
| 1 | 9999 | 0 | 9999*1/10000 = 0.9999 → 0 |
| 1 | 10000 | 1 | 10000*1/10000 = 1.0 → 1 |
- All intermediate arithmetic uses
i128(max ≈ 1.7 × 10³⁸). - Saturating multiplication is used where overflow is possible.
- The test
test_overflow_safety_large_amountsvalidates amounts ati128::MAX / 2satisfy the no-dust invariant.
- No hidden fee extraction: fee is computed solely from
gross_profit * fee_bps / 10_000; no additional charges are possible. - Deterministic: identical inputs always produce identical outputs (no randomness, no timestamp dependency in the formula).
- Admin-gated config: only the admin address can change
fee_bps; all changes emit an auditable event. - Immediate effect: fee config changes apply to the very next
calculate_profitcall with no delay or buffering.