Problem
campaign-escrow::resolve_dispute is documented as a deliberate, working interim path that settles a committed payout without going through dispute-resolution at all (lib.rs:833-838):
"Admin-resolved settlement for a single creator's committed-but-not-yet-paid application, as a simplified interim path alongside the arbiter-resolved dispute-resolution contract... a separate admin-only shortcut that works today without it."
That's a reasonable design — but it means when this path is used for a payout that does have an open dispute in dispute-resolution (i.e. raise_dispute was called first, which is the normal/expected flow per that contract's own doc comments), dispute-resolution's record of it is left permanently stale. Nothing ever calls dispute-resolution::clear_open_dispute (storage.rs:113-119, doc comment says "Called once resolve_dispute is implemented" — grepping the whole crate confirms it is never called anywhere), and campaign-escrow::resolve_dispute has no awareness of dispute-resolution at all.
Concrete failure scenario
- Creator calls
dispute-resolution::raise_dispute — this freezes the payout in escrow via freeze_for_dispute and writes a Dispute { status: Raised, outcome: Pending, resolved_at: None }.
- Admin settles it directly via
campaign-escrow::resolve_dispute(admin, campaign_id, creator, PayCreator) — funds move, application.status = Paid, application.frozen = false.
dispute-resolution::get_dispute(dispute_id) still returns status: Raised, outcome: Pending, resolved_at: None forever. There's no code path that will ever update it — dispute-resolution::resolve_dispute (the arbiter flow) is a separate todo!() stub, and even once implemented, it has no reason to be called for a dispute the admin already settled through the other contract.
Any indexer or frontend that treats dispute-resolution::get_dispute as the source of truth for "is this dispute still open" will show a stale, indefinitely-"Raised" dispute for money that has already been paid out — a real data-integrity gap for anyone building on top of this contract's event/read surface, even though the underlying funds themselves are safely resolved.
This is distinct from #42 (wiring the arbiter-resolved flow) — it's about the admin bypass path that already works today leaving the other contract's state permanently wrong, independent of whether the arbiter flow ever gets built.
Expected behaviour
When campaign-escrow::resolve_dispute settles a payout, any corresponding open dispute in dispute-resolution should be closed out — either via a cross-contract call from escrow back into dispute-resolution, or by exposing a way for indexers to reliably detect this case (e.g. clearly documenting that dispute-resolution::get_dispute is not authoritative once the admin bypass has been used, and giving them a cheap way to check — e.g. cross-referencing campaign-escrow::get_application(...).frozen == false against a still-Raised dispute).
Files
contracts/campaign-escrow/src/lib.rs — resolve_dispute
contracts/dispute-resolution/src/lib.rs / storage.rs — expose a callable "close dispute" path if going the cross-contract route
Acceptance criteria
Problem
campaign-escrow::resolve_disputeis documented as a deliberate, working interim path that settles a committed payout without going throughdispute-resolutionat all (lib.rs:833-838):That's a reasonable design — but it means when this path is used for a payout that does have an open dispute in
dispute-resolution(i.e.raise_disputewas called first, which is the normal/expected flow per that contract's own doc comments),dispute-resolution's record of it is left permanently stale. Nothing ever callsdispute-resolution::clear_open_dispute(storage.rs:113-119, doc comment says "Called onceresolve_disputeis implemented" — grepping the whole crate confirms it is never called anywhere), andcampaign-escrow::resolve_disputehas no awareness ofdispute-resolutionat all.Concrete failure scenario
dispute-resolution::raise_dispute— this freezes the payout in escrow viafreeze_for_disputeand writes aDispute { status: Raised, outcome: Pending, resolved_at: None }.campaign-escrow::resolve_dispute(admin, campaign_id, creator, PayCreator)— funds move,application.status = Paid,application.frozen = false.dispute-resolution::get_dispute(dispute_id)still returnsstatus: Raised, outcome: Pending, resolved_at: Noneforever. There's no code path that will ever update it —dispute-resolution::resolve_dispute(the arbiter flow) is a separatetodo!()stub, and even once implemented, it has no reason to be called for a dispute the admin already settled through the other contract.Any indexer or frontend that treats
dispute-resolution::get_disputeas the source of truth for "is this dispute still open" will show a stale, indefinitely-"Raised" dispute for money that has already been paid out — a real data-integrity gap for anyone building on top of this contract's event/read surface, even though the underlying funds themselves are safely resolved.This is distinct from #42 (wiring the arbiter-resolved flow) — it's about the admin bypass path that already works today leaving the other contract's state permanently wrong, independent of whether the arbiter flow ever gets built.
Expected behaviour
When
campaign-escrow::resolve_disputesettles a payout, any corresponding open dispute indispute-resolutionshould be closed out — either via a cross-contract call from escrow back intodispute-resolution, or by exposing a way for indexers to reliably detect this case (e.g. clearly documenting thatdispute-resolution::get_disputeis not authoritative once the admin bypass has been used, and giving them a cheap way to check — e.g. cross-referencingcampaign-escrow::get_application(...).frozen == falseagainst a still-Raiseddispute).Files
contracts/campaign-escrow/src/lib.rs—resolve_disputecontracts/dispute-resolution/src/lib.rs/storage.rs— expose a callable "close dispute" path if going the cross-contract routeAcceptance criteria
campaign-escrow's admin-resolve bypass should reconcile with an opendispute-resolutionrecord (cross-contract close-out, or a documented "not authoritative" caveat + a supported way to detect the mismatch)dispute-resolution, resolves it viacampaign-escrow::resolve_dispute, and asserts the dispute-resolution record reflectsResolveddispute-resolution::get_dispute's doc comment, plus a read-only way for callers to detect the stale case