This document describes the comprehensive property-based testing suite for SwiftRemit's fee calculation logic, designed to catch edge cases, overflows, and mathematical inconsistencies through fuzzing.
Property-based testing uses randomly generated inputs to verify that mathematical properties hold across a wide range of scenarios. Unlike traditional unit tests that check specific cases, property tests verify invariants that should always be true.
Uses fast-check library with 1000+ test cases per property.
-
Fee Bounds
- Fees never exceed the original amount
- Fees are always at least
MIN_FEE(1 stroop) - Maximum fee (100% bps) equals the amount
-
Monotonic Behavior
- Fees increase monotonically with fee basis points
- Fees increase monotonically with amount (when not floored)
-
Mathematical Consistency
amount = platformFee + protocolFee + netAmount- Net amount is never negative
- Fee breakdown validation
-
Dynamic Fee Tiers
- Tier 1 (< 1000 USDC): Full fee rate
- Tier 2 (1000-10000 USDC): 80% of base rate
- Tier 3 (> 10000 USDC): 60% of base rate
- Proper tier boundary handling
-
Edge Cases
- Zero fee basis points → MIN_FEE
- Maximum safe integer handling
- Boundary value testing
- Invalid input rejection
Uses proptest library with 1000+ test cases per property.
-
Fee Calculation Properties
// Fee never exceeds amount prop_assert!(fee <= amount); // Fee is at least minimum prop_assert!(fee >= MIN_FEE); // Exact formula verification let expected = (amount * fee_bps as i128 / FEE_DIVISOR).max(MIN_FEE); prop_assert_eq!(calculated_fee, expected);
-
Overflow Protection
// Large values should either succeed or return overflow error match calculate_fee_by_strategy(large_amount, &strategy) { Ok(fee) => { /* verify fee is valid */ } Err(ContractError::Overflow) => { /* acceptable */ } Err(other) => prop_assert!(false, "Unexpected error: {:?}", other) }
-
Dynamic Fee Tier Verification
// Verify tier discounts are applied correctly let tier1_fee = calculate_fee_by_strategy(500_0000000, &strategy)?; let tier2_fee = calculate_fee_by_strategy(5000_0000000, &strategy)?; let tier3_fee = calculate_fee_by_strategy(20000_0000000, &strategy)?; // Verify tier ordering for normalized amounts prop_assert!(norm_tier1 >= norm_tier2 >= norm_tier3);
# Standard testing (1000 cases per property)
cd backend
npm test -- fee-calculation-property.test.ts
# Quick validation (100 cases)
cd backend
npm test -- fee-calculation-property.test.ts --reporter=verbose# Quick validation (10 test cases)
PROPTEST_CASES=10 cargo test fee_service_property_tests --lib -- --nocapture
# Standard fuzzing (100 test cases per property - default)
cargo test fee_service_property_tests --lib -- --nocapture
# Intensive fuzzing (1000+ test cases)
PROPTEST_CASES=1000 cargo test fee_service_property_tests --lib -- --nocapture
# Run specific test
cargo test prop_percentage_fee_never_negative --lib -- --nocapture
# Verbose output (shows generated values)
PROPTEST_VERBOSE=1 cargo test fee_service_property_tests --lib -- --nocapture# Run all property-based tests
./run-property-tests.sh// TypeScript generators
fc.integer({ min: 1, max: Number.MAX_SAFE_INTEGER }) // Valid amounts
fc.integer({ min: 0, max: 10000 }) // Valid basis points
fc.integer({ min: 100, max: 1000000 }) // Reasonable amounts// Rust generators
prop_compose! {
fn valid_amount()(amount in 1i128..=i128::MAX/MAX_FEE_BPS as i128) -> i128 {
amount
}
}Both test suites include specific tests for overflow conditions:
- Large amounts near
i128::MAX/Number.MAX_SAFE_INTEGER - High fee basis points that could cause multiplication overflow
- Boundary conditions where
amount * fee_bpsapproaches limits
Special focus on tier boundaries for dynamic fees:
let boundary1 = 1000_0000000i128; // Tier 1/2 boundary
let boundary2 = 10000_0000000i128; // Tier 2/3 boundary
// Test just below and at boundaries
let just_below = boundary1 - 1;
let fee_below = calculate_fee_by_strategy(just_below, &strategy)?;
let fee_at = calculate_fee_by_strategy(boundary1, &strategy)?;fc.assert(
fc.property(/* generators */, (/* params */) => {
// Property assertions
}),
{ numRuns: 1000 } // Run 1000 random test cases
);proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn property_name(/* generators */) {
// Property assertions
}
}- Comprehensive Coverage: Tests thousands of input combinations automatically
- Edge Case Discovery: Finds corner cases that manual testing might miss
- Regression Prevention: Catches regressions across the entire input space
- Mathematical Verification: Ensures fee calculations maintain mathematical properties
- Overflow Protection: Verifies safe arithmetic operations
- Confidence: Provides high confidence in fee calculation correctness
- Non-negativity: All fees and amounts are non-negative
- Bounds checking: Fees don't exceed reasonable limits
- Monotonicity: Increasing inputs produce non-decreasing outputs
- Consistency: Mathematical relationships are preserved
- Minimum floor: All fees respect the minimum fee requirement
- Percentage accuracy: Percentage calculations are mathematically correct
- Tier behavior: Dynamic tiers apply correct discounts
- Breakdown consistency: Fee components sum to the total amount
- All property assertions pass across 1000+ test cases
- No unexpected errors or panics
- Consistent behavior across input ranges
When a property test fails:
- Shrinking: The framework automatically finds the minimal failing case
- Reproduction: Failed cases can be reproduced with specific seeds
- Root Cause: Examine the specific input values that caused failure
- Fix Verification: Re-run tests to verify fixes
These property tests should be integrated into the continuous integration pipeline:
# Example CI configuration
- name: Run Property-Based Tests
run: |
cd backend && npm test -- fee-calculation-property.test.ts
PROPTEST_CASES=500 cargo test fee_service_property_tests --lib -- --nocapture --test-threads=1For nightly/stress testing:
- name: Intensive fee fuzzing
if: github.event_name == 'schedule'
run: |
PROPTEST_CASES=5000 cargo test fee_service_property_tests --lib -- --nocaptureExpected runtimes (approximate):
- TypeScript (1000 cases): ~30-60 seconds
- Rust (10 cases): ~2-3 seconds
- Rust (100 cases): ~20-30 seconds
- Rust (500 cases): 2-3 minutes
- Rust (1000 cases): 4-5 minutes
Times vary based on system performance and compilation cache.
The test suite includes helper functions to verify calculations:
// TypeScript
function calculateExpectedFee(amount: number, bps: number): number {
return Math.max(MIN_FEE, Math.floor((amount * bps) / 10000));
}// Rust
fn manual_percentage_fee(amount: i128, bps: u32) -> Option<i128> {
let product = (amount as i128).checked_mul(bps as i128)?;
let fee = product.checked_div(FEE_DIVISOR)?;
Some(fee.max(MIN_FEE))
}Formula: fee = max(MIN_FEE, (amount × bps) / 10000)
- Cross-Language Verification: Compare TypeScript and Rust implementations
- Performance Properties: Verify computational complexity bounds
- Stateful Testing: Test sequences of fee calculations
- Integration Properties: Test fee calculations in full transaction flows
- Metamorphic Testing: Verify relationships between different fee strategies
- Corridor-specific fee validation
- Volume discount validation
- Multi-token fee calculations
This comprehensive property-based testing approach provides strong assurance that the fee calculation logic is mathematically sound, handles edge cases correctly, and protects against overflows and other arithmetic errors.