Skip to content

fix: oracle monotonic timestamps, guardian removal multisig, build + doc fixes (#774, #775, #776, #777) - #780

Merged
nonsobethel0-dev merged 2 commits into
Parashield-Protocol:mainfrom
Labi-Joy:fix/labi-joy-risk-oracle-claims-policy
Oct 21, 2026
Merged

nonsobethel0-dev merged 2 commits into
Parashield-Protocol:mainfrom
Labi-Joy:fix/labi-joy-risk-oracle-claims-policy

Conversation

@Labi-Joy

@Labi-Joy Labi-Joy commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #774
Closes #775
Closes #776
Closes #777

What changed

Four Wave 10 fixes bundled per the maintainer's recent multi-issue pattern. Two land as behavior changes with tests (#774, #775), two land as documentation + the baseline build fixes needed to reach them (#776, #777).

#775 — OracleVerifier readings must advance strictly forward in time

submit_data / batch_submit_data / submit_data_batch previously accepted readings whose timestamp was <= a prior reading from the same oracle for the same (data_type, key). A replayed or out-of-order submission from a flaky feed could overwrite a fresher reading already folded into the aggregate. Added StorageKey::LastReadingTimestamp(data_type, key, oracle), a new Error::StaleReadingTimestamp (code 37), and two helpers (require_strictly_newer_reading / record_reading_timestamp) that gate the submission before store_plain_reading and advance the high-water mark only after acceptance. Guard scoped per (data_type, key, oracle), same guard in both batch entry points.

#774 — RiskPool guardian removal routes through the remaining guardians

set_guardians was admin-only and let a single admin key wipe the guardian set in one call. The function now rejects any update that drops a current guardian (GuardianRemoveBlocked, code 49). Removal routes through a new propose/approve flow (propose_remove_guardian / approve_remove_guardian / cancel_pending_guardian_removal / get_pending_guardian_removal) that mirrors the existing propose_new_admin + approve_admin_change pattern. The guardian being removed cannot vote on their own exit. MIN_GUARDIANS = 2 enforced both at propose and at execution so the committee can't collapse to a single key. Threshold is capped at the new set size on removal so subsequent approvals stay reachable.

Only two new error codes were added — Soroban's #[contracterror] enum is spec-capped at 50 variants on this contract. NotGuardian and NoPendingUpgrade are reused for "address is not a guardian" and "no pending removal" to stay under the cap.

#776 — ClaimsProcessor claim-equality boundary

The AC calls out "accept claim = premium" / "handle boundary correctly". Walking the gates in process_claim, evaluate_and_settle, and the installment-schedule check: every claim-vs-coverage comparison already uses != or strict >, so claim.coverage_amount == policy.coverage_amount, paid_amount == claim.coverage_amount, and total_amount == claim.coverage_amount are permitted today. The defect reported is a documentation gap, not a logic one. Added an explicit doc block on process_claim describing the equality semantics so a future refactor to >=/<= is caught at review.

#777 — PolicyEngine upgrade preserves policy state

Soroban's env.deployer().update_current_contract_wasm swaps the code without touching storage; every entry in instance(), persistent(), and temporary() survives. The existing upgrade + run_migrations + migrate_v1_to_v2 scaffold already handles the schema-change case. Added a doc block on upgrade listing which StorageKeys are preserved, spelling out the order of operations (run migrations → stamp version → swap WASM), and documenting the operator upgrade procedure end-to-end.

Baseline build fixes (needed to reach #776 / #777 in test)

main fails to compile parashield-policy-engine and parashield-claims-processor as of 77c7c51. Fixed inline so this PR actually builds the touched crates:

policy-engine (3 errors):

  • s.as_ref().as_bytes() on L471 needed a type annotation.
  • Error::UnregisteredOracleDataType referenced at L495 but not declared.
  • Error::InvalidInput referenced at L1095 but not declared.

claims-processor (6 errors):

  • Error::EvidenceLimitExceeded referenced at L2092 but not declared.
  • Error::InvalidEvidence referenced at L2097 and L2107 but not declared.
  • StorageKey::ClaimEvidence(u128) referenced at L2110 and L2117 but not declared.
  • Error::InvalidToken referenced at L2231 but not declared.

Added the missing enum variants with doc comments. No behavior changed in the library code paths that reference them — they were already written to panic through these names, there just weren't declarations for the names.

Verification

$ cargo build -p parashield-policy-engine -p parashield-claims-processor \
              -p parashield-risk-pool -p parashield-oracle-verifier
Finished `dev` profile [unoptimized + debuginfo] target(s)   # was failing on main

$ cargo test -p parashield-risk-pool --lib test_guardians
test result: ok. 8 passed; 0 failed

$ cargo test -p parashield-oracle-verifier --lib test_monotonic
test result: ok. 5 passed; 0 failed

$ cargo test -p parashield-oracle-verifier --lib
test result: FAILED. 123 passed; 10 failed  # same 10 pre-existing fails on main

Full oracle-verifier baseline on main is 118 / 10; after this PR 123 / 10 — the 10 failures are unchanged pre-existing baseline, the +5 new passing tests are the monotonic suite. No regressions.

Note on cargo tests for #776 and #777

I did not land cargo tests for #776 or #777 because the test harnesses on main are pre-existing broken against the current library code:

  • claims-processor: 35 compile errors across test.rs / test_integration.rs / test_claim_uniqueness.rs / test_advanced.rs — buy_policy signature drift, missing expected_token field on CreateProductParams, missing get_pending_guardian_config client method, u64 vs i64 compares.
  • policy-engine: 128 compile errors across test.rs / test_advanced.rs / test_product_names.rs — references to symbols that no longer exist.

Fixing those harnesses would be a sizable follow-up on top of this PR. Flagging explicitly so the maintainer can prioritize.

…rashield-Protocol#774, Parashield-Protocol#775)

Two security hardenings on the oracle and risk-pool contracts.

Parashield-Protocol#775 — OracleVerifier readings must advance strictly forward in time.

submit_data / batch_submit_data / submit_data_batch previously accepted
readings whose timestamps were earlier than or equal to a prior reading
from the same oracle for the same (data_type, key). A replayed or
out-of-order submission from a flaky feed could therefore overwrite a
fresher reading already folded into the aggregate. The gate uses the
`timestamp` the oracle self-reports, not the ledger timestamp, so a
late-arriving stale reading passed every existing gate (confidence,
range, max-data-age, future-buffer) and still slipped in behind a
newer value.

Added `StorageKey::LastReadingTimestamp(data_type, key, oracle)` and
two helpers: `require_strictly_newer_reading` panics with the new
`StaleReadingTimestamp` (code 37) when an incoming timestamp is `<=`
the recorded high-water mark; `record_reading_timestamp` advances the
mark only after every other gate has accepted the submission, so a
rejection does not silently poison the next legitimate reading. The
guard is scoped per (data_type, key, oracle), so an unrelated key or
oracle is unaffected.

Covered by `oracle-verifier/src/test_monotonic.rs`:
- first submission is accepted;
- a strictly-later timestamp is accepted;
- an equal timestamp is rejected (37);
- an earlier timestamp is rejected (37);
- the guard is scoped per key — a late reading on one key does not
  block a submission on another.

Parashield-Protocol#774 — RiskPool guardian removal routes through the remaining guardians.

`set_guardians` was admin-only and let the admin alone replace or wipe
the guardian set, so a compromised admin key could dismantle the
multisig in one call. The function now rejects any update that drops a
current guardian (`GuardianRemoveBlocked`, code 49) — the admin can
expand the committee or raise the threshold freely, but shrinking is
routed through the propose/approve flow.

Added `propose_remove_guardian(admin, g)` / `approve_remove_guardian(g)`
/ `cancel_pending_guardian_removal(admin)` / `get_pending_guardian_removal`,
mirroring the admin-transfer multisig pattern that already exists on
this contract (`propose_new_admin` + `approve_admin_change`). The
guardian scheduled for removal cannot vote on their own exit.
`MIN_GUARDIANS = 2` is enforced both at propose time and re-verified at
execution so the committee can never collapse to a single key, and the
threshold is capped at the new set size to keep subsequent approvals
reachable. A new `PendingGuardianRemoval` instance-storage slot holds
the proposal (reused across `cancel` and `get`).

Only two new error codes were added — the Soroban `#[contracterror]`
spec is capped at 50 variants, so this touches the existing budget
instead of adding four. `NotGuardian` and `NoPendingUpgrade` are
reused for "address is not a guardian" and "no pending removal" to
stay under the cap.

Covered by `risk-pool/src/test_guardians.rs`:
- the initial `set_guardians` call accepts any set;
- `set_guardians` can expand once a set exists;
- `set_guardians` cannot shrink (49);
- a single approval is not enough when threshold is 2;
- removal applies when the threshold is met;
- removal below `MIN_GUARDIANS` is refused at propose time (50);
- a non-guardian cannot approve (23);
- admin can cancel a pending removal.

Verification:
- `cargo build -p parashield-risk-pool -p parashield-oracle-verifier` clean.
- `cargo test -p parashield-risk-pool test_guardians` → 8/8 pass.
- `cargo test -p parashield-oracle-verifier test_monotonic` → 5/5 pass.
- Full `cargo test -p parashield-oracle-verifier --lib`: 123 pass / 10
  fail; same 10 pre-existing failures as `main` (118/10) plus the 5
  new passing monotonic tests. No regressions.

Closes Parashield-Protocol#774
Closes Parashield-Protocol#775
@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@Labi-Joy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…eak build (Parashield-Protocol#776, Parashield-Protocol#777)

Two further Wave 10 issues — pinned by documentation and the baseline
compile fixes needed to even reach them in test.

Parashield-Protocol#776 — ClaimsProcessor claim-equality boundary.

The AC says "accept claim = premium" and "handle boundary correctly".
Walking the gates in `process_claim`, `evaluate_and_settle`, and the
installment-schedule check: every claim-vs-coverage comparison uses `!=`
or strict `>`, so `claim.coverage_amount == policy.coverage_amount`,
`paid_amount == claim.coverage_amount`, and
`total_amount == claim.coverage_amount` are all permitted today. The
AC is already satisfied by the current code; the defect reported in
the issue is a documentation gap, not a logic one.

Added a doc block on `process_claim` that explicitly states the
equality semantics so a future refactor to `>=`/`<=` is caught at
review and a reader scanning the function knows which strictness
each gate uses.

I would have shipped a cargo test for this boundary too, but the
claims-processor test harness is pre-existing broken on `main` (35
compile errors across `test.rs` / `test_integration.rs` /
`test_claim_uniqueness.rs` / `test_advanced.rs` — API drift against
`buy_policy`, missing `expected_token` field on CreateProductParams,
missing `get_pending_guardian_config` client method, `u64 vs i64`
compares). Fixing that is out of scope for this PR.

Parashield-Protocol#777 — PolicyEngine upgrade preserves policy state.

Soroban's `env.deployer().update_current_contract_wasm` swaps the
contract's code without touching storage — every entry in
`instance()`, `persistent()`, and `temporary()` survives the swap,
which is already how `upgrade` behaves on this contract. The AC's
"preserve state" / "verify storage compatibility" / "document the
upgrade procedure" items are satisfied by:

- Expanded doc comment on `upgrade` describing which StorageKeys are
  preserved (policies, products, pending-oracle-change records),
  spelling out the order of operations (run migrations → stamp
  version → swap WASM), and listing the operator procedure
  end-to-end.
- The existing `run_migrations` / `migrate_v1_to_v2` scaffold is
  called out as the extension point for schema changes that need to
  rewrite legacy entries.

Same test-harness constraint applies here: policy-engine's test
module has 128 pre-existing compile errors (`test.rs`,
`test_advanced.rs`, `test_product_names.rs` all reference symbols
that no longer exist), so a cargo test that exercises
`run_migrations` against stored policies can't be landed in this PR.

Baseline build fixes (needed to even reach Parashield-Protocol#776 / Parashield-Protocol#777 code paths).

`main` fails to compile `parashield-policy-engine` and
`parashield-claims-processor` as of `77c7c51`:

policy-engine (3 errors):
- `s.as_ref().as_bytes()` on line 471 needed a type annotation.
  Rewrote as `let s_str: &str = s.as_ref(); s_str.as_bytes()`.
- `Error::UnregisteredOracleDataType` referenced at line 495 but not
  declared.
- `Error::InvalidInput` referenced at line 1095 but not declared.

claims-processor (6 errors):
- `Error::EvidenceLimitExceeded` referenced at :2092 but not declared.
- `Error::InvalidEvidence` referenced at :2097 and :2107 but not
  declared.
- `StorageKey::ClaimEvidence(u128)` referenced at :2110 and :2117 but
  not declared.
- `Error::InvalidToken` referenced at :2231 but not declared.

Added the missing enum variants with doc comments. No behavior
changed in the library code paths that reference them — they were
already written to panic through these names, there just weren't
declarations for the names.

Verification:
- `cargo build -p parashield-policy-engine -p parashield-claims-processor
   -p parashield-risk-pool -p parashield-oracle-verifier` is now clean on
   this branch (was failing on `main`).
- `cargo test -p parashield-risk-pool --lib test_guardians` → 8/8
  pass (regression check on the Parashield-Protocol#774 work).
- `cargo test -p parashield-oracle-verifier --lib test_monotonic` →
  5/5 pass (regression check on the Parashield-Protocol#775 work).

Closes Parashield-Protocol#776
Closes Parashield-Protocol#777
@Labi-Joy Labi-Joy changed the title fix: enforce monotonic oracle readings and guard guardian removal (#774, #775) fix: oracle monotonic timestamps, guardian removal multisig, build + doc fixes (#774, #775, #776, #777) Oct 21, 2026
@nonsobethel0-dev
nonsobethel0-dev merged commit 3d963c1 into Parashield-Protocol:main Oct 21, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants