Skip to content

fix: subtract cancelled campaign amount from total_raised_global at cancellation time - #714

Open
Nife-tanny wants to merge 1 commit into
Iris-IV:mainfrom
Nife-tanny:fix/cancel_campaign_global_raised
Open

fix: subtract cancelled campaign amount from total_raised_global at cancellation time#714
Nife-tanny wants to merge 1 commit into
Iris-IV:mainfrom
Nife-tanny:fix/cancel_campaign_global_raised

Conversation

@Nife-tanny

Copy link
Copy Markdown

Closes #455

Root Cause

When a campaign was cancelled, total_raised_global was only decremented on a per-contributor basis inside claim_refund. If contributors never called claim_refund, the cancelled campaign's refundable amount remained permanently included in the platform-wide total — inflating statistics and, more critically, blocking accept_token_update indefinitely.

Fix

  1. cancel_campaign and admin_cancel_campaign: subtract campaign.amount_raised from total_raised_global at cancellation time using checked arithmetic, removing the full claimable amount before any individual refund occurs.

  2. claim_refund: skips the total_raised_global decrement when the campaign is cancelled (is_cancelled == true), because the amount was already removed at cancellation time. For expired/failed (non-cancelled) campaigns, the decrement still happens as before — no double subtraction.

Invariant

total_raised_global after cancellation = previous global total − cancelled campaign's amount_raised

A cancelled campaign's refund liability is no longer represented in total_raised_global. Individual refund claims do not subtract the same amount again.

Tests Added / Updated

  • Cancellation removes claimable amount from global total
  • Unclaimed refund does not remain globally raised (reproduces the original bug)
  • Refund claim after cancel does not double-decrement (critical — Alice 600 + Bob 400 = 1000, cancel, both claim)
  • Multiple campaigns cancel accounting (A=100, B=200 → cancel both → 0)
  • Zero-value campaign cancel (no underflow)
  • Token migration no longer blocked by unclaimed cancelled refund
  • Updated test_get_platform_stats_returns_aggregates (total drops from 700→400 after cancelling campaign with 300)
  • Cleared comment on test_get_creator_stats_returns_aggregatesget_creator_stats sums campaign.amount_raised directly, not total_raised_global, so it still reports all 700
  • Renamed test_token_swap_blocked_with_unrefunded_cancelled_campaigntest_token_swap_succeeds_after_cancelled_campaign_with_unclaimed_refund

Validation

cargo fmt --all --check  ✓
cargo clippy --all-targets -- -D warnings  ✓
cargo test --workspace  403 passed, 2 failed

Pre-existing Failures (unrelated to this change)

Two tests in src/tests/test_campaign_update.rs fail with HostError: Error(Object, UnexpectedSize) — a Soroban SDK event-vector unpacking issue where an event payload's tuple size mismatches at runtime. These failures existed before this change and are not related to the total_raised_global accounting fix:

  • tests::test_campaign_update::test_update_campaign_emits_title_and_description
  • tests::test_campaign_update::test_update_campaign_event_tracks_latest_description

Changed Files

File Lines
src/campaigns/cancel.rs +25, −5
src/contributions.rs +9, −7
src/tests/test_admin.rs +8, −18
src/tests/test_queries.rs +191, −2

4 files changed, 237 insertions, 21 deletions

…ancellation time

Closes Iris-IV#455

## Root Cause

When a campaign was cancelled, total_raised_global was only decremented
on a per-contributor basis inside claim_refund. If contributors never
called claim_refund, the cancelled campaign's refundable amount remained
permanently included in the platform-wide total, inflating statistics and —
more critically — blocking accept_token_update indefinitely.

## Fix

1. cancel_campaign and admin_cancel_campaign now subtract
   campaign.amount_raised from total_raised_global at cancellation time
   using checked arithmetic, removing the full claimable amount before
   any individual refund occurs.

2. claim_refund now skips the total_raised_global decrement when the
   campaign is cancelled (is_cancelled == true), because the amount was
   already removed at cancellation time. For expired/failed (non-cancelled)
   campaigns, the decrement still happens as before.

## Invariant

After cancellation: total_raised_global == previous_global - cancelled_campaign.amount_raised

A cancelled campaign's refund liability is no longer represented in
total_raised_global. Individual refund claims do not subtract the same
amount again, preventing double subtraction.

## Tests Added

- Cancellation removes claimable amount from global total
- Unclaimed refund does not remain globally raised (reproduces the bug)
- Refund claim after cancel does not double-decrement
- Multiple campaigns cancel accounting (A=100, B=200, cancel both)
- Zero-value campaign cancel (no underflow)
- Token migration no longer blocked by unclaimed cancelled refund
@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@Nife-tanny 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

@davidmaronio davidmaronio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really solid test coverage here, the five regression tests in test_queries.rs (double-decrement, multi-campaign accounting, zero-value underflow) are exactly the kind of coverage this area needs, and the checked_sub with Error::Overflow is the right arithmetic hygiene.

but there's a safety regression hiding in the semantics change:

  1. src/campaigns/cancel.rs (both cancel paths) + src/contributions.rs claim_refund: main deliberately uses total_raised_global as the escrow-liveness gate for token migration. accept_token_update in src/admin.rs (line 361-371 on main) blocks the swap while total_raised_global != 0 precisely because "cancel_campaign drops the active count immediately, but contributor refunds stay escrowed until each contributor calls claim_refund, which pays out in the current token". by subtracting at cancellation time and skipping the decrement in claim_refund for cancelled campaigns, this PR lets the admin swap tokens while refunds are still escrowed in the old token. after the swap, claim_refund transfers the NEW token, which the contract doesn't hold for those contributors. their refunds either trap or drain unrelated new-token balances.

  2. src/tests/test_admin.rs: the PR rewrites test_token_swap_blocked_with_unrefunded_cancelled_campaign into its opposite. that test existed to pin exactly this hazard (issue #407 follow-up), so flipping it is deleting the guard rather than satisfying it. fix: keep total_raised_global as the escrow gate, and solve the stats-inflation side of #455 differently, either a separate display counter for platform stats, or a dedicated outstanding-escrow counter that accept_token_update checks while the stats query subtracts cancelled amounts.

  3. heads up that this overlaps #604 and #718 in the #438 family, worth coordinating with the maintainers on which approach wins before reworking, so effort isn't duplicated.

also note the CI red is partly stale-base: the two test_campaign_update failures are main's 4-tuple event payload change that your branch predates, a rebase clears those.

gate: rework the escrow gating as above, then resolve the conflicts with main and get CI green before this can move.

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] cancel_campaign does not decrement total_raised_global upfront — global stat overstated until all refunds claimed

2 participants