Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
commit_out.txt
.git_commit_msg.txt
commit_and_push.ps1
commit.sh

# Rust build artifacts
target/
Expand Down
4 changes: 0 additions & 4 deletions commit.sh

This file was deleted.

15 changes: 0 additions & 15 deletions commit_and_push.ps1

This file was deleted.

32 changes: 26 additions & 6 deletions contracts/disputes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public struct EscalationRecord {
enum DataKey {
DisputeCounter,
Dispute(u32),
Escalation(Symbol),
}

fn load_dispute(env: &Env, id: u32) -> Dispute {
Expand Down Expand Up @@ -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,
Expand All @@ -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");
}
Expand All @@ -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<EscalationRecord> {
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");
Expand Down
35 changes: 35 additions & 0 deletions contracts/disputes/tests/dispute_storage.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use disputes::{
DisputeOutcome, DisputeStatus, DisputesContract, DisputesContractClient,
EscalationPolicy, COUNCIL_VOTE_UPHOLD,
};
use soroban_sdk::{Env, Symbol};

Expand Down Expand Up @@ -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));
}
31 changes: 30 additions & 1 deletion contracts/reporting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ impl ReportingContract {
/// # Errors
///
/// * [`ReportingError::ReportingPaused`] — Reporting is currently paused.
/// * [`ReportingError::ReportNotFound`] — No report with the given ID exists.
///
/// # Auth
///
Expand All @@ -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>(&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::<DataKey, Vec<u32>>(&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(())
}

Expand Down
5 changes: 5 additions & 0 deletions contracts/reporting/tests/auth_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
48 changes: 48 additions & 0 deletions contracts/reporting/tests/market_reports_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down
Loading