From 03bd20c56255c2971a29150a0c810d902145ec63 Mon Sep 17 00:00:00 2001 From: ytech001 Date: Sun, 18 Oct 2026 19:59:01 +0000 Subject: [PATCH] fix: address reporting, disputes, and script hygiene Persist escalation records and apply council votes to stored state. Return ReportNotFound when deleting an absent report, and ignore developer commit scripts after auditing them for embedded credentials. Fixes dultimateade/hermes#238 Fixes dultimateade/hermes#240 Fixes dultimateade/hermes#249 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 2 + commit.sh | 4 -- commit_and_push.ps1 | 15 ------ contracts/disputes/src/lib.rs | 32 ++++++++++--- contracts/disputes/tests/dispute_storage.rs | 35 ++++++++++++++ contracts/reporting/src/lib.rs | 31 +++++++++++- contracts/reporting/tests/auth_boundary.rs | 5 ++ .../reporting/tests/market_reports_index.rs | 48 +++++++++++++++++++ 8 files changed, 146 insertions(+), 26 deletions(-) delete mode 100644 commit.sh delete mode 100644 commit_and_push.ps1 diff --git a/.gitignore b/.gitignore index 7a5de424..3c865c56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ commit_out.txt .git_commit_msg.txt +commit_and_push.ps1 +commit.sh # Rust build artifacts target/ diff --git a/commit.sh b/commit.sh deleted file mode 100644 index 50f56a50..00000000 --- a/commit.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -git add . -git commit -m "docs: markets error catalog" -git push -u origin task/markets-errcat diff --git a/commit_and_push.ps1 b/commit_and_push.ps1 deleted file mode 100644 index 0fe8bf11..00000000 --- a/commit_and_push.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -# Git commit and push script - -cd 'c:\Users\LENOVO PC\Desktop\New folder (7)\hermes' - -# Stage all changes -git add . - -# Create commit -git commit -F '.\.git_commit_msg.txt' - -# Push to origin -git push -u origin feat/canonical-error-registry - -# Display status -git log --oneline -3 diff --git a/contracts/disputes/src/lib.rs b/contracts/disputes/src/lib.rs index f660f708..bd182c42 100644 --- a/contracts/disputes/src/lib.rs +++ b/contracts/disputes/src/lib.rs @@ -167,6 +167,7 @@ public struct EscalationRecord { enum DataKey { DisputeCounter, Dispute(u32), + Escalation(Symbol), } fn load_dispute(env: &Env, id: u32) -> Dispute { @@ -213,17 +214,17 @@ impl DisputesContract { } pub fn escalate_dispute( - _env: Env, + env: Env, policy: EscalationPolicy, market_id: Symbol, stake: u32, turnout: u32, community_outcome: DisputeOutcome, ) -> EscalationRecord { - if !Self::is_escalation_eligible(_env.clone(), policy, stake, turnout) { + if !Self::is_escalation_eligible(env.clone(), policy, stake, turnout) { panic!("market not eligible for escalation"); } - EscalationRecord { + let record = EscalationRecord { market_id, stake, turnout, @@ -232,15 +233,24 @@ impl DisputesContract { uphold_votes: 0, overturn_votes: 0, resolved: false, - } + }; + env.storage() + .persistent() + .set(&DataKey::Escalation(record.market_id.clone()), &record); + record } pub fn council_vote( - _env: Env, + env: Env, policy: EscalationPolicy, - mut record: EscalationRecord, + record: EscalationRecord, vote: u32, ) -> EscalationRecord { + let mut record: EscalationRecord = env + .storage() + .persistent() + .get(&DataKey::Escalation(record.market_id.clone())) + .unwrap_or_else(|| panic!("escalation not found")); if record.resolved { panic!("escalation already resolved"); } @@ -260,9 +270,19 @@ impl DisputesContract { } else if record.overturn_votes >= policy.council_threshold { record.resolved = true; } + env.storage() + .persistent() + .set(&DataKey::Escalation(record.market_id.clone()), &record); record } + /// Returns the persisted escalation record for a market, if one exists. + pub fn get_escalation(env: Env, market_id: Symbol) -> Option { + env.storage() + .persistent() + .get(&DataKey::Escalation(market_id)) + } + pub fn council_outcome(_env: Env, record: EscalationRecord) -> DisputeOutcome { if !record.resolved { panic!("council has not resolved the dispute"); diff --git a/contracts/disputes/tests/dispute_storage.rs b/contracts/disputes/tests/dispute_storage.rs index 2980aa24..cfa07093 100644 --- a/contracts/disputes/tests/dispute_storage.rs +++ b/contracts/disputes/tests/dispute_storage.rs @@ -1,5 +1,6 @@ use disputes::{ DisputeOutcome, DisputeStatus, DisputesContract, DisputesContractClient, + EscalationPolicy, COUNCIL_VOTE_UPHOLD, }; use soroban_sdk::{Env, Symbol}; @@ -33,4 +34,38 @@ fn dispute_lifecycle_is_persisted_and_uses_unique_ids() { let withdrawn = client.withdraw_dispute(&second, &creator); assert_eq!(withdrawn.status, DisputeStatus::Withdrawn); assert_eq!(client.get_dispute(&second.id), Some(withdrawn)); +} + +#[test] +fn council_votes_accumulate_in_persisted_escalation_record() { + let env = Env::default(); + let contract_id = env.register(DisputesContract, ()); + let client = DisputesContractClient::new(&env, &contract_id); + let market_id = Symbol::new(&env, "MARKET1"); + let policy = EscalationPolicy { + min_stake: 100, + quorum: 10, + council_threshold: 2, + }; + + let escalation = client.escalate_dispute( + &policy, + &market_id, + &100, + &9, + &DisputeOutcome::ChallengerWins, + ); + assert_eq!(client.get_escalation(&market_id), Some(escalation.clone())); + + let first_vote = client.council_vote(&policy, &escalation, &COUNCIL_VOTE_UPHOLD); + assert_eq!(first_vote.council_votes, 1); + assert_eq!(first_vote.uphold_votes, 1); + assert!(!first_vote.resolved); + + // A stale caller-supplied record must not discard the vote already stored. + let second_vote = client.council_vote(&policy, &escalation, &COUNCIL_VOTE_UPHOLD); + assert_eq!(second_vote.council_votes, 2); + assert_eq!(second_vote.uphold_votes, 2); + assert!(second_vote.resolved); + assert_eq!(client.get_escalation(&market_id), Some(second_vote)); } \ No newline at end of file diff --git a/contracts/reporting/src/lib.rs b/contracts/reporting/src/lib.rs index 60d03935..3a95bdaf 100644 --- a/contracts/reporting/src/lib.rs +++ b/contracts/reporting/src/lib.rs @@ -295,6 +295,7 @@ impl ReportingContract { /// # Errors /// /// * [`ReportingError::ReportingPaused`] — Reporting is currently paused. + /// * [`ReportingError::ReportNotFound`] — No report with the given ID exists. /// /// # Auth /// @@ -306,7 +307,35 @@ impl ReportingContract { ) -> Result<(), ReportingError> { admin.require_auth(); pause::require_not_paused(&env)?; - let _ = report_id; + + let report = env + .storage() + .persistent() + .get::(&DataKey::Report(report_id)) + .ok_or(ReportingError::ReportNotFound)?; + + let market_key = DataKey::MarketReports(report.market_id); + if let Some(mut market_reports) = env + .storage() + .persistent() + .get::>(&market_key) + { + let mut index = 0; + while index < market_reports.len() { + if market_reports.get(index) == Some(report_id) { + market_reports.remove(index); + break; + } + index += 1; + } + env.storage() + .persistent() + .set(&market_key, &market_reports); + } + + env.storage() + .persistent() + .remove(&DataKey::Report(report_id)); Ok(()) } diff --git a/contracts/reporting/tests/auth_boundary.rs b/contracts/reporting/tests/auth_boundary.rs index 1cc5475e..8fd6886c 100644 --- a/contracts/reporting/tests/auth_boundary.rs +++ b/contracts/reporting/tests/auth_boundary.rs @@ -366,6 +366,11 @@ fn test_delete_report_rejected_without_auth() { fn test_delete_report_accepted_with_admin_auth() { let env = Env::default(); let (client, admin) = register_and_init(&env); + let reporter = Address::generate(&env); + client + .submit_report(&reporter, &1u32, &report_data(&env), &report_hash(&env)) + .unwrap(); + let _ = env.auths(); client.delete_report(&admin, &1u32).unwrap(); diff --git a/contracts/reporting/tests/market_reports_index.rs b/contracts/reporting/tests/market_reports_index.rs index d97b1187..c3b539b3 100644 --- a/contracts/reporting/tests/market_reports_index.rs +++ b/contracts/reporting/tests/market_reports_index.rs @@ -214,6 +214,54 @@ fn test_get_report_not_found() { } } +/// `delete_report` should return `ReportNotFound` for non-existent IDs. +#[test] +fn test_delete_report_not_found() { + let env = Env::default(); + let (client, admin, _reporter) = register_and_init(&env); + + let result = client.try_delete_report(&admin, &999); + match result { + Err(e) => { + let err = e.to_string(); + assert!(err.contains("ReportNotFound") || err.contains("7")); + } + Ok(_) => panic!("Expected ReportNotFound error"), + } +} + +/// Deleting a report should remove it from storage and its market index. +#[test] +fn test_delete_report_removes_report_and_market_index_entry() { + let env = Env::default(); + let (client, admin, reporter) = register_and_init(&env); + let market_id = 42u32; + let report_id = client + .submit_report( + &reporter, + &market_id, + &String::from_str(&env, "Report to delete"), + &String::from_str(&env, "hash"), + ) + .unwrap(); + let remaining_report_id = client + .submit_report( + &reporter, + &market_id, + &String::from_str(&env, "Report to keep"), + &String::from_str(&env, "hash"), + ) + .unwrap(); + + client.delete_report(&admin, &report_id).unwrap(); + + assert!(client.try_get_report(&report_id).is_err()); + assert!(client.get_report(&remaining_report_id).is_ok()); + let market_reports = client.get_reports_by_market(&market_id); + assert_eq!(market_reports.len(), 1); + assert_eq!(market_reports.get(0), Some(remaining_report_id)); +} + // --------------------------------------------------------------------------- // Pause behavior // ---------------------------------------------------------------------------