This document maps each issue type and protocol domain to the specific tests,
evidence, and completion artifacts required before a PR is considered complete.
Use it alongside PULL_REQUEST_TEMPLATE.md,
which lists the base validation commands every PR must run.
Every PR must clear these before domain-specific requirements apply.
| Gate | Command |
|---|---|
| All tests pass | cargo test --workspace |
| Lints clean | cargo clippy --workspace --all-targets --locked -- -D warnings |
| Format correct | cargo fmt --all -- --check |
| Bindings build and parity | cd bindings && npm ci && npm run build && npm run test:parity |
Note on parity coverage:
parity.jschecks public method parity only — it does not check error enum parity. If your PR adds a newContractErrorvariant, you must also update the error map inbindings/src/index.tsmanually. The parity script will not catch the drift (open finding SR-2026-04-001).
A bug fix PR must prove the defect is reproducible and that the patch closes it.
| Domain | Required test module(s) | Test categories | Evidence to include in PR |
|---|---|---|---|
| Architecture / Lifecycle | lifecycle.rs, guard_tests.rs, chaos_recovery.rs |
Unit, regression | cargo test <module> output before patch (showing failure) and after (showing pass) |
| Oracle | security.rs |
Unit, negative | Test output showing the bug's error code before and the correct behaviour after |
| Economics | resolution.rs, overflow_tests.rs |
Unit, regression | State snapshot before and after — confirm no partial writes |
| Observability | event_coverage.rs |
Unit | Before/after test output; if bindings were wrong, include updated index.ts diff |
Definition of done:
- A new regression test named to match the bug exists in the relevant module.
- The test fails on
mainand passes on the patch branch. cargo test --workspaceis clean.
A feature PR must cover the happy path, at least one failure/revert path, and at least one edge-case boundary.
| Domain | Required test module(s) | Test categories | Evidence to include in PR |
|---|---|---|---|
| Architecture / Lifecycle | lifecycle.rs, guard_tests.rs |
Unit, boundary | cargo test <module> -- --nocapture showing new event or storage key written and cleaned up |
| Oracle | security.rs, mode_tests.rs |
Unit, negative | Negative test output showing correct error code on bad input |
| Economics | resolution.rs, property_invariants.rs |
Unit, property | Payout conservation check; cargo test property_invariants -- --nocapture |
| Observability | event_coverage.rs |
Unit | Test output showing new event topic pair emitted with correct payload field order |
Definition of done:
- Happy-path test, one revert-path test, and one boundary test exist.
- If a new public entrypoint is added: bindings are updated and
npm run test:paritypasses. - If new events are added:
docs/EVENT_SCHEMA.mdis updated before the PR is opened.
Protocol changes must demonstrate full invariant coverage for any invariant they touch. See the invariant coverage matrix in PROTOCOL_SPEC.md for the authoritative list (I1–I13) and which test files cover each.
| Domain | Required test module(s) | Test categories | Evidence to include in PR |
|---|---|---|---|
| Architecture / Lifecycle | lifecycle.rs, guard_tests.rs, chaos_recovery.rs, migration_versioning.rs |
Unit, chaos | Updated invariant row in PROTOCOL_SPEC.md; cargo test lifecycle -- --nocapture output |
| Oracle | security.rs, mode_tests.rs |
Unit, negative, property | Test output for stale, future-dated, deviation, and replay cases |
| Economics | resolution.rs, overflow_tests.rs, property_invariants.rs, cost_benchmarks.rs |
Unit, property, benchmark | cargo test cost_benchmarks -- --nocapture showing CPU/mem within budget; conservation invariant output |
| Observability | event_coverage.rs |
Unit | Updated docs/EVENT_SCHEMA.md diff in PR; npm run test:parity output |
Definition of done:
- All affected invariants (I1–I13) in
PROTOCOL_SPEC.mdhave updated evidence entries. - If accounting or payout logic changed:
property_invariants.rspayout conservation test passes. - If any hot path changed (
create_round,place_bet,resolve_round): benchmark output included in PR description showing CPU and memory remain within the Soroban budget (100_000_000CPU instructions,100 MiBmemory). Seecontracts/BENCHMARKS.md. - If storage keys or struct fields changed: see the Architecture domain-specific requirements in Part 2.
Security PRs must supply negative tests proving the mitigation holds. Positive happy-path tests are secondary; what matters is that the attack surface is demonstrably closed.
| Domain | Required test module(s) | Test categories | Evidence to include in PR |
|---|---|---|---|
| Architecture / Lifecycle | guard_tests.rs, pause.rs, chaos_recovery.rs |
Negative, chaos | Test output showing the correct ContractError variant and numeric code returned; security clippy output |
| Oracle | security.rs |
Negative, replay | Negative test output for replay, stale, future-dated, and zero-price cases; confirm nonce consumed under DataKey::ConsumedOracleNonce |
| Economics | overflow_tests.rs, resolution.rs |
Negative, overflow | Test output showing PayoutOverflow (25) — not generic Overflow (11) — on payout arithmetic failure |
| Observability | security.rs, event_coverage.rs |
Negative | Confirm no sensitive data leaks into event payloads; cargo audit --deny warnings output |
Security clippy — run before every security PR:
cargo clippy --workspace --all-targets --locked -- \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::integer_arithmetic \
-W clippy::arithmetic_side_effects \
-W clippy::cast_possible_truncation \
-W clippy::cast_sign_lossDefinition of done:
- Negative test exists for the specific attack vector, asserting the correct error code by numeric value (not just by name, since bindings consumers see the number).
- Security clippy output included in PR body showing no new
-Dviolations. cargo audit --deny warningsis clean.- PR description states the threat being addressed and the worst-case impact if left unpatched.
The tests are the deliverable. The PR must demonstrate that the added tests cover a genuine gap, not a path that was already implicitly covered.
| Domain | Required test module(s) | Test categories | Evidence to include in PR |
|---|---|---|---|
| Architecture / Lifecycle | lifecycle.rs, guard_tests.rs, chaos_recovery.rs |
Unit, chaos | Show that no existing test would have caught the scenario; cargo test <module> -- --nocapture output |
| Oracle | security.rs |
Negative | Describe what oracle behaviour was previously untested |
| Economics | property_invariants.rs, overflow_tests.rs |
Property, overflow | cargo test property_invariants -- --nocapture showing the new invariant assertion |
| Observability | event_coverage.rs |
Unit | Show that the event field or ordering was previously unasserted |
Definition of done:
- Tests fail (or the gap is explained) before the addition, and pass after.
- Test names follow the existing
test_<behaviour>_<condition>convention used in the codebase (e.g.test_place_bet_twice_same_round). - No new public contract methods or storage keys are introduced — if the gap requires a contract change, open a separate Protocol Improvement issue.
These requirements stack on top of the per-issue-type requirements above. Apply whichever domain(s) your change touches.
Covers: create_round, cancel_round, resolve_round, the single-active-round
invariant, storage key additions and removals, schema migration, TTL management,
and the participant list.
| Change | Additional requirement |
|---|---|
| Any create/cancel/resolve change | Add or update a test in chaos_recovery.rs covering pause-then-resume or cancel-then-create across the change |
New DataKey variant (persistent, long-lived) |
Add a TTL extension test in ttl_tests.rs; call _extend_persistent_ttl at every read/write site |
New DataKey variant (short-lived, round-scoped) |
Verify the key is deleted in resolve_round and cancel_round; add an assertion to storage_benchmarks.rs |
#[contracttype] struct field added or removed |
MAJOR version bump (XDR encoding changes); add a migration path in MIGRATION.md; bump CURRENT_SCHEMA_VERSION; add a test in migration_versioning.rs |
DataKey variant renamed or removed |
MAJOR version bump; document cutover in MIGRATION.md; verify legacy fallback reads still work during migration window |
| Schema migration entrypoint | Must require no active round (MigrationActiveRound guard); test both the guard and the successful migration in migration_versioning.rs |
Covers: resolve_round, update_oracle_heartbeat, OraclePayload validation,
deviation guardrails, stale detection, and nonce deduplication.
| Change | Additional requirement |
|---|---|
Any resolve_round change |
Test all four payload rejection paths: zero price, future timestamp, stale timestamp, wrong round_id |
| Oracle payload field added or reordered | MAJOR version bump; document new semantics in MIGRATION.md and integration docs |
| Deviation guardrail logic | Test both below-threshold (passes) and above-threshold (fails with OracleDeviationExceeded (41)) |
| Override arm/clear logic | Test that the one-shot flag is consumed and cleared after a single use |
| Heartbeat / liveness change | Test all three status codes (0, 1, 2) and the invalid-status rejection (InvalidOracleStatus (36)) |
Field naming caveat:
OraclePayload.round_id: u32maps toRound.start_ledger, not the monotonicRound.round_id: u64. This is an accepted design ambiguity (SR-2026-04-003). PR descriptions touching oracle payload handling must state which field the payload is validated against, to avoid oracle operator confusion.
Covers: balance accounting, payout formulas (UpDown proportional, Precision closest/tie), risk control caps, pending winnings accumulation, and overflow handling.
| Change | Additional requirement |
|---|---|
| Payout formula change | Update property_invariants.rs conservation test; run cost_benchmarks.rs with --nocapture and include output |
| New risk control cap | Test the cap boundary (amount exactly at limit passes, amount one stroop over fails with the correct error code) |
| Balance or pending-winnings path | Use payout_add/payout_mul — not raw checked_add — so overflow surfaces as PayoutOverflow (25), not Overflow (11) |
| Precision tie-breaking change | Test a 3-way tie with a non-divisible pot; verify the remainder goes to the first winner and total distributed equals the full pot |
| Unchanged-price refund path | Test that every participant receives their exact stake back and the pool fields are zero after resolution |
| Any partial-write risk | Add a test that asserts storage state is unchanged after a failure return — no intermediate write should persist |
Covers: on-chain events, docs/EVENT_SCHEMA.md, TypeScript bindings ABI, and
the bindings/src/parity.js error-enum gap.
| Change | Additional requirement |
|---|---|
| New event | Add a test in event_coverage.rs asserting the topic pair and each payload field by position; update docs/EVENT_SCHEMA.md before opening the PR |
| Changed event payload field | MAJOR version bump; update docs/EVENT_SCHEMA.md; notify indexer/frontend consumers in PR description |
| Changed event topic string | MAJOR version bump |
| New public entrypoint | Regenerate or update bindings/src/index.ts; confirm npm run test:parity passes; include bindings/dist/index.d.ts diff in PR |
New ContractError variant |
Manually add the variant to the error map in bindings/src/index.ts (the parity script does not check enum parity); include the updated map in the PR diff |
Removed or renumbered ContractError variant |
MAJOR version bump — clients that match the numeric code will break |
The following examples show what to paste in the PR description as evidence.
$ cargo test --package xelma-contract resolution::test_resolution_unchanged_price_refunds_all -- --nocapture
running 1 test
test tests::resolution::test_resolution_unchanged_price_refunds_all ... ok
test result: ok. 1 passed; 0 failed
Paste the output once for the patch branch. Explain in one sentence what the pre-patch behaviour was (e.g. "Before this fix, an unchanged price incorrectly paid out winners instead of refunding all participants").
$ cargo test --package xelma-contract cost_benchmarks -- --nocapture
[bench] create_round cpu= 2_847_123 mem= 1_048_576
[bench] place_bet cpu= 3_102_489 mem= 1_048_576
[bench] resolve_round cpu= 18_334_201 mem= 4_194_304
[bench] claim_winnings cpu= 1_983_204 mem= 524_288
Include the PROTOCOL_SPEC.md diff showing the updated invariant row:
-| I8 | Settlement conservation | payout/refund helpers | `resolution.rs`, `property_invariants.rs` | Covered |
+| I8 | Settlement conservation | payout/refund helpers | `resolution.rs`, `property_invariants.rs`, `overflow_tests.rs` | Covered |$ cargo test --package xelma-contract security::test_oracle_nonce_reuse_rejected -- --nocapture
running 1 test
test tests::security::test_oracle_nonce_reuse_rejected ... ok
test result: ok. 1 passed; 0 failed
$ cargo clippy --workspace --all-targets --locked -- \
-D clippy::unwrap_used -D clippy::expect_used -D clippy::panic \
-D clippy::integer_arithmetic
Checking xelma-contract v0.1.0
Finished in 3.4s — 0 warnings, 0 errors
State the error code asserted: "Test asserts OracleNonceReused (33) is
returned on the second submission of the same nonce for a round, confirming
the ConsumedOracleNonce(round_id, nonce) key is written and checked."
| Document | When to consult |
|---|---|
PROTOCOL_SPEC.md |
Invariant coverage matrix (I1–I13), trust model, threat model |
PULL_REQUEST_TEMPLATE.md |
Base PR checklist and governance items |
COMPATIBILITY_POLICY.md |
MAJOR / MINOR / PATCH classification rules |
SECURITY_REVIEW.md |
Open findings (SR-2026-04-001, SR-2026-04-002) and accepted risks |
contracts/BENCHMARKS.md |
How to record benchmark baselines and tighten guardrails |
docs/EVENT_SCHEMA.md |
Canonical event topic pairs, payload field order, and units |
docs/storage_lifecycle.md |
Long-lived vs short-lived key classification and TTL policy |
MIGRATION.md |
Schema version migration history and consumer steps |