Skip to content

fix: let admin call freeze_for_dispute directly - #51

Merged
JamesVictor-O merged 1 commit into
Ads-Bazaar:mainfrom
olathedev:fix/39-freeze-dispute-admin-race
Jul 27, 2026
Merged

fix: let admin call freeze_for_dispute directly#51
JamesVictor-O merged 1 commit into
Ads-Bazaar:mainfrom
olathedev:fix/39-freeze-dispute-admin-race

Conversation

@olathedev

Copy link
Copy Markdown
Contributor

Problem

Closes #39.

claim_payment had no awareness of disputes, letting a creator front-run an unfavorable resolve_dispute outcome by claiming first. Most of the fix for this had already landed by the time this issue was picked up, via PR #49's freeze_for_dispute / raise_dispute wiring:

  • freeze_for_dispute sets a per-application frozen flag (no longer todo!())
  • claim_payment rejects a frozen application with Error::PayoutFrozen, regardless of proof_approved or the deadline
  • resolve_dispute clears frozen when it settles an application

What was still missing, per this issue's "Rules" section:

freeze_for_dispute should be callable by the contract's own admin (matching resolve_dispute's existing auth model) at minimum; extending it to the dispute-resolution contract is a separate, larger integration ... and shouldn't block this fix.

In the current code, freeze_for_dispute is reachable only through dispute-resolution::raise_dispute, which in turn only lets the creator or campaign business call it. Admin had no direct path to freeze a payout — unlike resolve_dispute, which admin can already call unilaterally. That's a real gap: an admin who spots a suspicious claim-in-flight (outside the normal raise_dispute flow) had no way to intervene before this PR.

Fix

  • freeze_for_dispute now takes an explicit caller: Address argument, require_auth()'d and checked against either the configured dispute_contract or admin (mirrors the explicit-address-plus-comparison pattern already used by require_admin/resolve_dispute elsewhere in this contract).
  • dispute-resolution::raise_dispute now passes its own contract address (env.current_contract_address()) as caller when it invokes escrow.freeze_for_dispute(...), preserving the existing behavior for the normal dispute-raising flow.
  • Updated doc comments on freeze_for_dispute, the Application.frozen field, and the DisputeFrozen event to describe the dual auth path instead of only the dispute-resolution contract.

Test coverage added

  • admin_can_freeze_directly — admin freezes without going through raise_dispute; the frozen application then correctly rejects claim_payment.
  • freeze_rejects_uninvolved_stranger — replaces the old freeze_requires_dispute_contract_auth mock-auth test now that the function takes an explicit caller; asserts a caller that's neither admin nor the dispute contract gets Error::Unauthorized and the application stays unfrozen.
  • admin_resolve_dispute_settles_and_clears_freeze extended to also assert that, after resolve_dispute clears the freeze and marks the application Paid, a further claim_payment attempt still correctly fails (via the ordinary already-Paid guard, not PayoutFrozen) — this was one of the acceptance-criteria test cases not yet covered.

All existing freeze_for_dispute tests updated to pass the new caller argument (the bootstrap-configured dispute contract address, matching production usage).

Test plan

  • cargo test --workspace — 82 + 17 tests pass in campaign-escrow / dispute-resolution
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo build --workspace --target wasm32v1-none --release — builds

…olution

claim_payment already rejects a frozen application (PayoutFrozen), and
resolve_dispute already clears the flag when it settles — that race-closing
logic landed with PR Ads-Bazaar#49's freeze_for_dispute/raise_dispute wiring, before
this issue was picked up.

What was still missing per Ads-Bazaar#39's "Rules" section: freeze_for_dispute was
reachable only through the dispute-resolution contract's raise_dispute
(itself only callable by the creator or business), so admin had no direct
way to freeze a payout — unlike resolve_dispute, which admin can already
call on its own. Give freeze_for_dispute an explicit caller argument,
authorized as either the configured dispute_contract or admin, so an
emergency freeze doesn't require routing through raise_dispute.

Also fills a small test gap: verifies that after resolve_dispute clears a
freeze and marks the application Paid, a further claim_payment attempt still
correctly fails (via the ordinary already-Paid guard).

Closes Ads-Bazaar#39
@JamesVictor-O
JamesVictor-O merged commit 2d7e09c into Ads-Bazaar:main Jul 27, 2026
JamesVictor-O pushed a commit that referenced this pull request Jul 27, 2026
cancel_campaign, expire_campaign, and reclaim_surplus all require business.require_auth(), which is correct day-to-day (no hidden admin release path) but leaves escrow permanently stuck if a business's signing key is ever lost, rotated away from, or the account otherwise abandoned.

Adds emergency_recover_campaign: admin-only, gated on completion_deadline plus a new EMERGENCY_RECOVERY_GRACE_PERIOD (~6 months) on top, so it's deliberately much harder to reach than expire_campaign and can never substitute for the normal exit paths. It only ever sweeps the unallocated remainder (escrow_balance - committed_payouts), mirroring the existing cancel/expire/reclaim pattern exactly — a payout already committed to an approved creator stays reserved and claimable regardless of how long the business has been unreachable. Recovered funds go to treasury rather than back to business, since the whole premise is that the business's address is unreachable. Emits a distinct EmergencyRecovery event so the path stays auditable on its own.

Closes #48

Verified locally in an isolated worktree before merge:
- cargo fmt --all -- --check
- cargo build --workspace
- cargo test --workspace (103 tests: 86 escrow + 17 dispute-resolution)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --workspace --target wasm32v1-none --release
- Confirmed via git merge-tree that this combines cleanly with #51 (merged just before it) — freeze_for_dispute's new caller argument and this PR's emergency_recover_campaign coexist correctly with no regression.
@JamesVictor-O

Copy link
Copy Markdown
Contributor

Merged, thanks — good catch on the gap and a clean, minimal fix. The explicit-caller-plus-comparison pattern matches resolve_dispute exactly, and updating both call sites (the new admin path and dispute-resolution's raise_dispute) in the same PR avoided leaving anything broken in between.

Verified before merging: fmt, full workspace build, all 99 tests, clippy -D warnings, and the wasm32v1-none release build. CI is green on main: https://github.com/Ads-Bazaar/ads-bazaar-contract/actions/runs/30230403166

JamesVictor-O pushed a commit that referenced this pull request Jul 29, 2026
…resolution (#54)

Adds contracts/campaign-escrow/tests/integration.rs — 15 tests that register both CampaignEscrowContract and DisputeResolutionContract in the same Env and exercise real Soroban cross-contract call semantics (auth checks, balance correctness, and the freeze/claim lifecycle) that single-contract unit tests can't see.

Covers: dual initialization wiring, raise_dispute freezing the payout across the contract boundary (both creator- and business-raised), frozen-payout claim rejection, full dispute-record field verification, balance-level assertions for all three admin resolve_dispute outcomes (PayCreator/RefundBusiness/Split), stranger rejection on freeze_for_dispute, the admin emergency-bypass path from #51, cross-creator isolation, duplicate-raise rejection, already-paid rejection, and independent per-creator disputes on the same campaign.

Closes #45

Verified locally in an isolated worktree before merge:
- cargo fmt --all -- --check — this initially failed on a few lines in the new integration.rs; applied rustfmt and pushed the fix directly to the PR branch (maintainer-edits were enabled)
- cargo build --workspace
- cargo test --workspace (87 escrow unit + 15 integration + 17 dispute-resolution = 119 tests)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --workspace --target wasm32v1-none --release
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: claim_payment can race resolve_dispute, letting a creator front-run an unfavorable outcome

2 participants