Automated Testing

MarkIt maintains a comprehensive automated test suite covering smart contract correctness, security regression, economic invariants, and frontend logic. All tests run on every commit via a pre-commit hook.

Test Suite Overview

Layer
Framework
Tests
Files

Smart Contracts

Foundry (forge)

283

5

Frontend

Vitest

314

10

Total

597

15

Run all contract tests: cd contracts && forge test Run all frontend tests: cd frontend && npm test


Smart Contract Tests

Test Files

File
Tests
Scope

SecurityAudit.t.sol

120

Security regression, exploit defense, fuzz testing

LpVault.t.sol

81

Vault share math, queue, deployment, collection

MarketEngine.t.sol

63

Core engine: pricing, fees, state machine, solvency

CreateMarket.t.sol

12

Market creation, seed prices, factory lifecycle

MarketFactory.t.sol

7

Factory deployment, multi-market independence

Fuzz Tests

22 fuzz tests run 256+ iterations each with randomized inputs. These stress-test invariants that must hold under any combination of parameters:

  • Solvency invariantmax(yesLiability, noLiability) <= USDC.balanceOf(engine) under random bets

  • USDC conservation — total USDC across all accounts equals total minted (no leakage or creation)

  • LP share accounting — shares track deposits correctly regardless of deposit sizes

  • Withdrawal ordering — total LP payout is deterministic regardless of withdrawal order

  • Dynamic skew capeffectiveSkewBps always returns values in [3000, 9000] bps

  • Partial redemption accuracy — dust bounded across chunked redeems

  • CloseOnly enforcement — only skew-reducing bets allowed under random market conditions

  • Interleaved withdraw/redeem — random ordering of LP withdrawals and winner redemptions

  • Extreme seed solvency — seeds in range [-10000e18, +10000e18] never break solvency

  • Donation + bet solvency — random USDC donations + random bets maintain invariants

  • Multi-user lifecycle conservation — random bet sizes, random outcomes, full lifecycle

  • Dual-outcome LP safety — LP never loses on both outcomes simultaneously


Security Categories Tested

Reentrancy Protection

All write functions on MarketEngine and LpVault use OpenZeppelin's ReentrancyGuard (nonReentrant modifier). Tests verify:

  • CEI (Checks-Effects-Interactions) ordering on sweepLosing() and redeem()

  • Sequential lifecycle (bet → resolve → redeem → LP withdraw) completes without reentrancy issues

  • nonReentrant guard is shared across all external functions

Solvency Invariant (Triple-Layer)

The most critical property: the contract can always pay the worst-case outcome. Enforced by three independent mechanisms, all tested:

  1. Post-bet solvency checkmax(yesLiab, noLiab) <= USDC.balanceOf(this) after every placeBet()

  2. Dynamic skew capabs(yesLiab - noLiab) <= totalLpDeposited * effectiveSkewBps / 10000

  3. Piecewise integration — large bets split into 16 steps, repricing at each step to prevent LP drain

Tests include:

  • test_fuzz_solvencyInvariant — random bets with 256+ runs

  • test_fuzz_dynamicSkewCapSolvency — random LP deposits + bets

  • test_audit_dustBetsCannotBreakSolvency — 20 dust bets at minimum size

  • test_withdrawal_nearSolvencyLimit — operations near the solvency edge

LP Withdrawal Safety (Audit Finding C-1)

LP withdrawals must reserve USDC for unredeemed winners. 10+ tests cover:

  • LP withdraws before any winner redeems

  • LP withdraws after partial redemptions

  • Multiple LPs with unredeemed winners

  • LP gets zero when fully underwater

  • Interleaved LP withdrawals and winner redemptions (random ordering via fuzz)

  • LP pool never shrinks as redemptions occur

  • All LPs withdraw before any redeem — winners still whole

Access Control

  • resolve() restricted to owner only

  • OutcomeToken mint()/burn() restricted to engine only

  • OutcomeToken setEngine() restricted to deployer, one-time lock

  • setEngine(address(0)) rejected

  • Double setEngine() call rejected

  • Non-deployer setEngine() rejected

  • LpVault admin functions restricted to owner

State Machine Enforcement

The market state machine (Created → Funded → Open → CloseOnly → Resolved → Withdrawable) is tested for:

  • No bets in Created state

  • No bets after resolution (Resolved/Withdrawable)

  • No LP deposits after resolution

  • resolve() only in CloseOnly state

  • sweepLosing() only in Resolved/Withdrawable

  • withdrawLp() only in Withdrawable

  • redeem() only in Resolved/Withdrawable

  • Timestamp boundary transitions (Open → CloseOnly)

  • Withdraw buffer delay (Resolved → Withdrawable)

  • No backward transitions

Slippage / Front-Running Protection

  • minSharesOut parameter rejects bets when price moves unfavorably

  • Impossibly high minSharesOut always reverts

  • Front-run simulation: large bet moves price, victim's tight slippage catches it

  • Zero minSharesOut always succeeds (documents user risk trade-off)

Donation / Flash Loan Attack Surface

Direct USDC transfers to the engine inflate backingEff (the pricing denominator). This is a documented design choice that benefits LPs. Tests verify:

  • Donation + large one-sided bet: solvency holds through full lifecycle

  • Donation + bets on both sides: no double-extraction possible

  • Mid-trading donations: solvency maintained

  • Fuzz: random donation + random bet amounts always maintain solvency

  • Direct transfer does not create phantom LP shares

  • Donation expands backingEff, dampening price impact

Fee System

  • Protocol fee transferred immediately to treasury on every bet (never held in contract)

  • Protocol fee rounds UP (favors protocol) — verified with exact arithmetic

  • LP fee distributed via Synthetix-style accumulator — no fee sniping

  • Late LP depositor gets zero pre-existing fees (anti-sniping)

  • Multi-LP deposits at different times: earlier depositors earn proportionally more

  • Fee precision maintained across 200+ small bets

  • Capital + fees never exceed available LP pool

  • Fee cap boundary: baseFeeBps + feeK + protocolFeeBps rejects > 10000

Rounding and Precision

  • WAD→USDC truncation accumulation bounded across multiple partial redeems

  • Dust shares that truncate to 0 USDC revert with NoWinnings

  • Fee rounding across many small bets compared to single large bet

  • Two independent fromWad() truncations per LP withdrawal: bounded to 1-2 wei dust

Constructor Validation

8 tests verify the constructor rejects invalid parameters:

  • Past close time

  • Resolve time before close time

  • Zero minLpUsdc / minBetUsdc

  • baseFeeBps < minFeeBps

  • Zero YES/NO token addresses

  • Oversized closeOnly window

  • Fee sum exceeding 100%

CloseOnly Enforcement

  • YES skew → YES bet blocked, NO bet (skew-reducing) allowed

  • NO skew → NO bet blocked, YES bet (skew-reducing) allowed

  • Balanced market → both sides allowed in CloseOnly

  • Fuzz: random skew states, only skew-reducing bets pass

Dynamic Skew Cap

  • Breakpoint values at LP capital thresholds ($1K, $10K, $100K, $500K, $1M)

  • Linear interpolation between breakpoints

  • Monotonically decreasing cap as pool grows

  • Small pool permissive (90% cap)

  • Large pool restrictive (30% cap)

  • Bet inflows do NOT change the cap (uses totalLpDeposited, not contract balance)

  • Additional LP deposit tightens the cap

  • Fuzz: bounds always in [3000, 9000] bps

Extreme Seed Imbalance

  • Extreme positive seed (500Ke18): YES price near ceiling, contrarian bet solvent

  • Extreme negative seed (-500Ke18): NO price near ceiling, contrarian bet solvent

  • Fuzz: seeds in [-10000e18, +10000e18], bets on both sides, solvency holds

Token Misuse

  • Redeeming losing tokens via redeem() reverts (must use sweepLosing())

  • Sweeping winning tokens via sweepLosing() reverts

  • sweepLosing(0) reverts with ZeroAmount

  • redeem(0) reverts with ZeroAmount

  • depositLp(0) reverts with ZeroAmount

  • withdrawLp(0) reverts with ZeroAmount

  • Insufficient LP shares revert

USDC Conservation

  • Full lifecycle: sum of all USDC across all accounts + engine + treasury = total minted

  • Fuzz: random bet sizes, random outcomes, conservation holds

  • Zero-bet resolution: LP gets exact capital back

SafeERC20 / Approval Checks

  • Zero allowance blocks placeBet()

  • Insufficient allowance blocks depositLp()


LpVault Test Coverage (81 tests)

The LpVault test suite covers the aggregated LP vault contract:

  • Share math: initial 1:1 deposit, subsequent deposits at NAV, share price movement (profit/loss/neutral)

  • Market deployment: createAndFund (2/5/10 market batches), access control, float tracking

  • Resolution + collection: atomic resolve + collect + queue processing, fee recording, permissionless collect()

  • Withdrawal queue: FIFO ordering, NAV snapshot lock-in, float exhaustion, partial fulfillment

  • Anti-snipe: minimum holding period enforcement

  • Emergency operations: market removal (unblocks queue, writes off capital), factory ownership transfer

  • Accounting invariants: NAV = float + totalDeployed, share price correctness

  • Audit fixes: ghost share dilution, stale-proof snapshots, partial fulfillment on insolvency, float drain + recovery, recoverMarket lifecycle


Frontend Tests (314 tests)

Vitest unit tests for pure functions in src/lib/. No RPC or contract interaction — pure input/output testing.

File
Tests
Target

format.test.ts

WAD/USDC conversions, display formatting

parseOutcomeLabels.test.ts

Outcome text parsing from market questions

errors.test.ts

Contract error → user-friendly message mapping

teams.test.ts

NBA team metadata, abbreviation matching

costBasis.test.ts

localStorage P&L tracking

slippage.test.ts

Off-chain piecewise slippage estimation (16-step mirror)

skewCap.test.ts

TypeScript mirror of effectiveSkewBps() curve

lpExposure.test.ts

LP scenario P&L under each resolution outcome

viewModel.test.ts

Raw hook data → display ViewModel transformation

vaultViewModel.test.ts

26

Share price, NAV, APR, pool share, canWithdraw logic


Pre-Commit Hook

A Husky pre-commit hook runs automatically on every commit:

  1. Contracts: When files under contracts/src/ or contracts/test/ are staged → runs forge test (full suite)

  2. Frontend: When src/**/*.{ts,tsx} files are staged → runs vitest related --run (only tests that import changed files)

Each check skips if no relevant files are staged.


What Is NOT Tested (Accepted Risk)

These items are documented design choices, not bugs:

  • Sandwich attacks with minSharesOut=0 — user error (they opted out of slippage protection)

  • Flash loan price dampening — donations inflate backingEff but solvency invariant still holds; benefits LPs

  • LpVault FIFO queue blocking — operational risk mitigated by emergencyRemoveMarket(); bounded to 50 iterations

  • Admin key compromise — out of scope for contract-level testing; operational security concern

  • Oracle manipulation — MarkIt uses admin-triggered resolution, not automated oracles (Phase 1)

Last updated