Comprehensive property-based and boundary tests for the Tossd coinflip contract's wager limit validation, ensuring strict enforcement of minimum and maximum wager limits with no off-by-one errors.
Added detailed doc comments explaining the fund-safety critical wager validation logic:
/// # Wager Limit Enforcement (Fund Safety Critical)
///
/// The wager limits are enforced using strict inequality checks to ensure
/// exact boundary behavior:
///
/// - **Accepted Range**: `wager >= config.min_wager && wager <= config.max_wager`
/// - **Rejected Below**: `wager < config.min_wager` → `Error::WagerBelowMinimum`
/// - **Rejected Above**: `wager > config.max_wager` → `Error::WagerAboveMaximum`
Key Points for Auditors:
- The validation uses
<and>operators, making inclusive bounds explicit - Guards execute BEFORE state mutation, ensuring atomicity
- Prevents both underbet (fund loss) and overbet (reserve depletion) scenarios
Located in property_tests module with 200 test cases each.
- Purpose: Ensure no wagers below MIN_WAGER can slip through
- Strategy: Generate random MIN values, then subtract offset to create invalid wagers
- Verification: All must return
Error::WagerBelowMinimum - Fund Safety: Prevents underbets that could fail to cover fees
#[test]
fn prop_wager_below_minimum_rejected(
min_wager in 1_000_000i128..50_000_000i128,
wager_offset in 1i128..1_000_000i128,
) {
// Generate invalid wager < min_wager
// Verify: Error::WagerBelowMinimum
}- Purpose: Prevent overbets that could exceed contract reserves
- Strategy: Generate random MAX values, then add offset to create invalid wagers
- Verification: All must return
Error::WagerAboveMaximum - Fund Safety: Prevents reserve depletion scenarios
#[test]
fn prop_wager_above_maximum_rejected(
min_wager in 1_000_000i128..50_000_000i128,
max_wager in 50_000_001i128..500_000_000i128,
wager_offset in 1i128..1_000_000i128,
) {
// Generate invalid wager > max_wager
// Verify: Error::WagerAboveMaximum
}- Purpose: Verify the lower bound is INCLUSIVE
- Strategy: Test with wager == min_wager across random configuration ranges
- Verification: All must succeed (return Ok)
- Off-by-One Check: Essential - prevents players from being unable to place minimum bets
#[test]
fn prop_wager_at_minimum_boundary_accepted(
min_wager in 1_000_000i128..50_000_000i128,
) {
// Test: wager == min_wager
// Verify: Ok (accepted)
}- Purpose: Verify the upper bound is INCLUSIVE
- Strategy: Test with wager == max_wager across random configuration ranges
- Verification: All must succeed (return Ok)
- Off-by-One Check: Essential - players should be able to bet the maximum
#[test]
fn prop_wager_at_maximum_boundary_accepted(
min_wager in 1_000_000i128..50_000_000i128,
max_wager in 50_000_001i128..500_000_000i128,
) {
// Test: wager == max_wager
// Verify: Ok (accepted)
}- Purpose: All wagers in [min, max] range must be accepted
- Strategy: Generate random bounds and random offset within valid range
- Verification: All must succeed
- Coverage: Tests the entire valid region, not just extremes
#[test]
fn prop_wagers_within_bounds_accepted(
min_wager in 1_000_000i128..50_000_000i128,
max_wager in 50_000_001i128..500_000_000i128,
wager_offset in 0i128..100_000_000i128,
) {
// wager = min_wager + (wager_offset % range)
// Verify: Ok (within bounds)
}Explicit edge-case tests with hardcoded values for clear contract specification.
- Input:
min_wager = 1_000_000,test_wager = 999_999 - Expected:
Error::WagerBelowMinimum - Rationale: The absolute minimum boundary — one stroop below must fail
- Input:
max_wager = 100_000_000,test_wager = 100_000_001 - Expected:
Error::WagerAboveMaximum - Rationale: The absolute maximum boundary — one stroop above must fail
- Input:
min_wager = 1_000_000,test_wager = 1_000_000 - Expected: Success (Ok)
- Assurance: The minimum boundary is inclusive
- Input:
max_wager = 100_000_000,test_wager = 100_000_000 - Expected: Success (Ok)
- Assurance: The maximum boundary is inclusive
- Input:
wager = (min + max) / 2 - Expected: Success (Ok)
- Coverage: Interior point test — ensures not just boundaries work
- Purpose: Rejection must apply regardless of Heads/Tails choice
- Strategy: Test invalid wager with Side::Heads
- Invariant: Wager validation is orthogonal to game choice
- Consistency Guarantee: No hidden rejection paths based on side
- Purpose: Failed validation must not persist any game state
- Strategy: Attempt invalid wager, then verify no PlayerGame entry exists
- Critical Check: Atomicity of guards — no partial state contamination
- Security: Prevents inconsistent contract state on validation failure
All tests use the actual Error enum defined in lib.rs:
Error::WagerBelowMinimum = 1Error::WagerAboveMaximum = 2
No incorrect error codes are used, ensuring proper client-side error handling.
cd contract
cargo test --lib property_tests
cargo test --lib testsOr run all tests with verbose output:
cargo test --lib -- --nocaptureEach test:
- Creates a fresh
Env::default()Soroban environment - Registers the CoinflipContract
- Initializes with specific min/max bounds
- Funds reserves to avoid
InsufficientReserveserrors - Mocks all authentications with
env.mock_all_auths()
This ensures tests are isolated and don't interfere with each other.
✅ Off-by-One Errors: All 12 tests collectively verify >= min and <= max
✅ Error Types: Uses correct Error enum variants from contract
✅ Environment Mocking: Proper Env setup with mock auth and reserve funding
✅ State Atomicity: Validation guards before any persistence
✅ Boundary Inclusive: Both min and max wagers are explicitly tested as accepted
✅ False Negatives: Property tests ensure no valid wager is rejected
✅ False Positives: Property tests ensure no invalid wager is accepted
| Scenario | Test Case | Input | Expected Result |
|---|---|---|---|
| Far below minimum | prop_wager_below_minimum_rejected |
wager << min | WagerBelowMinimum |
| 1 below minimum | test_wager_exactly_one_below_minimum_rejected |
min - 1 | WagerBelowMinimum |
| At minimum (inclusive) | prop_wager_at_minimum_boundary_accepted |
min | Ok |
| Within range | prop_wagers_within_bounds_accepted |
min < w < max | Ok |
| At maximum (inclusive) | prop_wager_at_maximum_boundary_accepted |
max | Ok |
| 1 above maximum | test_wager_exactly_one_above_maximum_rejected |
max + 1 | WagerAboveMaximum |
| Far above maximum | prop_wager_above_maximum_rejected |
wager >> max | WagerAboveMaximum |
The start_game() function doc comments now include:
- Explicit Bound Semantics: Shows
>=and<=in comments to match<and>guards - Fund Safety Rationale: Explains why bounds matter (fees, reserves)
- Off-by-One Emphasis: Highlights that bounds are inclusive
- Atomicity Guarantee: States guards run before mutation
The test suite:
- ✅ Does not modify core contract logic
- ✅ Adds only test code to the test modules
- ✅ Uses available framework (proptest already in Cargo.toml)
- ✅ Is clean, well-documented, and efficient
- ✅ Provides comprehensive coverage of boundary conditions
- ✅ Ensures end-to-end fund safety via property-based testing