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
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
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 invariant —
max(yesLiability, noLiability) <= USDC.balanceOf(engine)under random betsUSDC 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 cap —
effectiveSkewBpsalways returns values in [3000, 9000] bpsPartial 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()andredeem()Sequential lifecycle (bet → resolve → redeem → LP withdraw) completes without reentrancy issues
nonReentrantguard 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:
Post-bet solvency check —
max(yesLiab, noLiab) <= USDC.balanceOf(this)after everyplaceBet()Dynamic skew cap —
abs(yesLiab - noLiab) <= totalLpDeposited * effectiveSkewBps / 10000Piecewise integration — large bets split into 16 steps, repricing at each step to prevent LP drain
Tests include:
test_fuzz_solvencyInvariant— random bets with 256+ runstest_fuzz_dynamicSkewCapSolvency— random LP deposits + betstest_audit_dustBetsCannotBreakSolvency— 20 dust bets at minimum sizetest_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 onlyOutcomeToken
mint()/burn()restricted to engine onlyOutcomeToken
setEngine()restricted to deployer, one-time locksetEngine(address(0))rejectedDouble
setEngine()call rejectedNon-deployer
setEngine()rejectedLpVault 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 statesweepLosing()only in Resolved/WithdrawablewithdrawLp()only in Withdrawableredeem()only in Resolved/WithdrawableTimestamp boundary transitions (Open → CloseOnly)
Withdraw buffer delay (Resolved → Withdrawable)
No backward transitions
Slippage / Front-Running Protection
minSharesOutparameter rejects bets when price moves unfavorablyImpossibly high
minSharesOutalways revertsFront-run simulation: large bet moves price, victim's tight slippage catches it
Zero
minSharesOutalways 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 + protocolFeeBpsrejects > 10000
Rounding and Precision
WAD→USDC truncation accumulation bounded across multiple partial redeems
Dust shares that truncate to 0 USDC revert with
NoWinningsFee 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 usesweepLosing())Sweeping winning tokens via
sweepLosing()revertssweepLosing(0)reverts withZeroAmountredeem(0)reverts withZeroAmountdepositLp(0)reverts withZeroAmountwithdrawLp(0)reverts withZeroAmountInsufficient 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 trackingResolution + 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,
recoverMarketlifecycle
Frontend Tests (314 tests)
Vitest unit tests for pure functions in src/lib/. No RPC or contract interaction — pure input/output testing.
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:
Contracts: When files under
contracts/src/orcontracts/test/are staged → runsforge test(full suite)Frontend: When
src/**/*.{ts,tsx}files are staged → runsvitest 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
backingEffbut solvency invariant still holds; benefits LPsLpVault FIFO queue blocking — operational risk mitigated by
emergencyRemoveMarket(); bounded to 50 iterationsAdmin 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