Skip to content

feat: implemented auto-refund fallback mechanism on expired campaigns - #563

Open
opratem wants to merge 3 commits into
Fundable-Protocol:mainfrom
opratem:feat/auto-refund
Open

feat: implemented auto-refund fallback mechanism on expired campaigns#563
opratem wants to merge 3 commits into
Fundable-Protocol:mainfrom
opratem:feat/auto-refund

Conversation

@opratem

@opratem opratem commented Jul 29, 2026

Copy link
Copy Markdown

Description

Implements the complete auto-refund fallback mechanism for the campaign-funding Soroban smart contract. When a campaign's deadline passes without meeting its minimum funding target, any caller can trigger expiry to mark the campaign as failed, after which contributors can individually reclaim their full escrowed contributions.

Related Issue

Fixes #507

Type of Change

  • New feature
  • Documentation update

Changes Made

  • Implemented trigger_expiry() function — permissionless deadline expiration trigger:

    • Validates campaign is in Active state (CampaignNotActive error if not)
    • Validates ledger.timestamp() >= deadline (DeadlineNotReached error if not)
    • Transitions to Successful if total_raised >= min_target, otherwise Failed
    • Emits CampaignStatusChanged event on every transition
    • No require_auth() — intentionally callable by anyone (contributor, bot, third party)
  • Implemented refund() function — per-contributor escrow recovery:

    • Requires contributor.require_auth()
    • Only callable on Failed campaigns (CampaignNotFailed error otherwise)
    • Uses check-effects-interactions pattern (clears storage before token transfer to prevent double-refunds)
    • Returns full contribution amount — no protocol fee deducted on refunds
    • Emits RefundIssued event
  • Extended Error enum with typed codes:

    • DeadlineNotReached = 9
    • CampaignNotFailed = 10
    • NoContributionFound = 12
    • CampaignNotSuccessful = 11, AlreadyClaimed = 13, TargetExceeded = 16 (supporting codes)
  • Storage TTL managementextend_ttl(LEDGER_THRESHOLD, LEDGER_BUMP) applied to all persistent reads and writes (~30/31 day windows at 5 s/ledger)

  • Full Rust doc comments on all public functions (trigger_expiry, refund, create_campaign, contribute, claim_funds, and all query/admin setters)

Testing

  • Added 12 targeted unit tests covering trigger_expiry and refund:
    • trigger_expiry (6 tests): sets Successful when target met, sets Failed when target not met, handles zero contributions, rejects calls before deadline, rejects already-resolved campaigns, confirms permissionless caller works
    • refund (6 tests): full refund success, multiple contributors all refunded, rejects active campaigns, rejects successful campaigns, rejects callers with no contribution, prevents double-refund
  • All existing tests (initialize, create_campaign, contribute, claim_funds, admin setters, fee precision) continue to pass
  • Test snapshots committed for all 40+ test cases under test_snapshots/
cargo test -p campaign-funding
# Test Suites: 1 passed, 1 total
# Tests:       40 passed, 40 total

Function Signatures

/// Evaluate an Active campaign once its deadline has passed and
/// transition it to either Successful or Failed.
pub fn trigger_expiry(env: Env, campaign_id: u64);

/// Claim a full refund after a failed campaign.
pub fn refund(env: Env, contributor: Address, campaign_id: u64);

Checklist

  • Code follows Soroban SDK best practices
  • Self-reviewed my code
  • Commented complex code sections
  • Updated documentation (Rust doc comments)
  • No new compiler warnings
  • Added tests

Screenshots

N/A — smart contract, no UI changes

Additional Notes

trigger_expiry is deliberately permissionless. Requiring auth from the creator would allow a failing campaign creator to block refunds indefinitely by simply not calling it. Making it open ensures contributors can always recover their funds after a failed campaign, even without creator cooperation.

Summary by CodeRabbit

  • New Features

    • Added campaign-based token funding with configurable targets, deadlines, and contribution tracking.
    • Campaigns automatically succeed when fully funded and can be finalized as successful or failed after expiry.
    • Creators can claim funds from successful campaigns with configurable fees.
    • Contributors can receive refunds from failed campaigns, with safeguards against duplicate claims.
    • Added administrative controls for fee rates and fee collection settings.
  • Tests

    • Added comprehensive coverage for campaign creation, contributions, expiry, claims, refunds, validation, and fee calculations.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@opratem 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

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@opratem, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 26911497-c118-40ed-9e0e-c8e144be1139

📥 Commits

Reviewing files that changed from the base of the PR and between 7d16295 and 465fa77.

📒 Files selected for processing (2)
  • contracts/Cargo.toml
  • contracts/campaign-funding/src/lib.rs
📝 Walkthrough

Walkthrough

Adds a new campaign-funding Soroban contract with campaign creation, token contributions, expiry transitions, fund claims, refunds, administration, TTL handling, Rust tests, and ledger snapshot fixtures.

Changes

Campaign Funding

Layer / File(s) Summary
Contract package and data model
contracts/Cargo.toml, contracts/campaign-funding/Cargo.toml, contracts/campaign-funding/src/lib.rs
Registers the crate, configures Soroban dependencies, and defines campaign storage, lifecycle states, events, errors, fees, and TTL constants.
Campaign lifecycle and administration
contracts/campaign-funding/src/lib.rs
Implements initialization, campaign creation, contributions, expiry evaluation, claims, refunds, queries, admin setters, and storage helpers.
Initialization and campaign creation validation
contracts/campaign-funding/src/lib.rs, contracts/campaign-funding/test_snapshots/tests/*create_campaign*, contracts/campaign-funding/test_snapshots/tests/*initialize*
Tests initialization rules, campaign validation, campaign ID increments, and persisted campaign state.
Contribution and expiry state transitions
contracts/campaign-funding/src/lib.rs, contracts/campaign-funding/test_snapshots/tests/*contribute*, contracts/campaign-funding/test_snapshots/tests/*trigger_expiry*
Validates contribution amounts, escrow balances, hard-cap success, contributor accumulation, and expiry success or failure transitions.
Claim and refund safeguards
contracts/campaign-funding/src/lib.rs, contracts/campaign-funding/test_snapshots/tests/*claim_funds*, contracts/campaign-funding/test_snapshots/tests/*refund*
Covers fee deductions, zero-fee claims, invalid or repeated claims, successful refunds, multi-contributor refunds, and repeated-refund prevention.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: auto-refund handling for expired campaigns.
Linked Issues check ✅ Passed The added trigger_expiry, refund logic, error handling, TTL updates, and tests align with issue #507 requirements.
Out of Scope Changes check ✅ Passed The new crate manifest and snapshot files are supporting test/integration work and stay within the campaign-funding scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (5)
contracts/campaign-funding/src/lib.rs (5)

442-452: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc lists an error the function cannot return.

claim_funds enforces creator identity via require_auth(), so Error::Unauthorized is never emitted (and appears unused across the contract). Drop it from the doc or remove the variant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/campaign-funding/src/lib.rs` around lines 442 - 452, Update the
`claim_funds` documentation to remove the `Error::Unauthorized` entry from its
`# Errors` list, since authorization is enforced through `require_auth()` rather
than that error variant. Leave the documented `CampaignNotSuccessful` and
`AlreadyClaimed` errors unchanged.

211-225: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Move admin.require_auth() before state inspection.

Minor ordering nit: authorization currently runs after the AlreadyInitialized/FeeTooHigh checks. Not exploitable here (both checks are read-only), but auth-first is the safer convention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/campaign-funding/src/lib.rs` around lines 211 - 225, The initialize
function should authenticate the caller before inspecting initialization state
or validating the fee rate. Move admin.require_auth() to the beginning of
initialize, before the DataKey::Admin check, while preserving the existing
validation and storage behavior.

1318-1352: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test name doesn't match what is asserted.

test_refund_multiple_contributors_all_refunded sets up three contributors on id (which reaches min_target and therefore can never be refunded) and then only refunds a single contributor on a second campaign. Either refund and assert all three contributors on a genuinely failed campaign, or rename to reflect the single-refund scenario.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/campaign-funding/src/lib.rs` around lines 1318 - 1352, The test
test_refund_multiple_contributors_all_refunded does not match its
single-contributor assertion. Rename it to describe the id2 single-refund
scenario, or revise the setup and assertions to refund and verify all
contributors on a genuinely failed campaign; preserve the existing refund
behavior being tested.

1161-1177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not actually verify permissionlessness.

The comment says "Called with no auth mocking", but env.mock_all_auths() is active at Line 1166, so the test would pass even if trigger_expiry required auth. Drop the mock (or use env.set_auths(&[])) so the assertion has meaning.

♻️ Proposed change
     fn test_trigger_expiry_permissionless() {
         // A random third party (neither creator nor contributor) can call
         // trigger_expiry — the function requires no auth.
         let env = Env::default();
         env.mock_all_auths();
         set_time(&env, 1_000);
         let (_, client, _, _) = setup_contract(&env);
         let creator = Address::generate(&env);
         let token = Address::generate(&env);
 
         let id = client.create_campaign(&creator, &token, &10_000, &5_000, &2_000);
         set_time(&env, 3_000);
-        // Called with no auth mocking — just default env.
+        // Clear mocked auths so a missing auth requirement would fail the call.
+        env.set_auths(&[]);
         client.trigger_expiry(&id);
         assert_eq!(client.get_campaign(&id).status, CampaignStatus::Failed);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/campaign-funding/src/lib.rs` around lines 1161 - 1177, Remove the
active authorization mocking from test_trigger_expiry_permissionless, or
explicitly clear authorizations with env.set_auths(&[]) before calling
trigger_expiry. Preserve the existing setup and status assertion so the test
genuinely verifies the call succeeds without authorization.

549-586: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

get_fee_collector panics with an untyped host error when uninitialized.

Line 585's bare unwrap() produces a missing-value host error instead of Error::NotInitialized, inconsistent with the rest of the surface.

♻️ Proposed change
     pub fn get_fee_collector(env: Env) -> Address {
         env.storage()
             .instance()
             .get(&DataKey::FeeCollector)
-            .unwrap()
+            .unwrap_or_else(|| panic_with_error!(&env, Error::NotInitialized))
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/campaign-funding/src/lib.rs` around lines 549 - 586, Update
get_fee_collector to handle an absent FeeCollector value explicitly and return
the contract’s Error::NotInitialized error instead of using a bare unwrap.
Preserve returning the stored Address when the value is initialized and match
the established error behavior for uninitialized state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 1023-1032: Update test_contribute_to_nonexistent_campaign to
assert the numeric contract error emitted for CampaignNotFound rather than the
enum variant name CampaignNotActive. Use the corresponding CampaignNotFound
discriminant from the contract error enum and match the full host panic string
if required by the test framework.

---

Nitpick comments:
In `@contracts/campaign-funding/src/lib.rs`:
- Around line 442-452: Update the `claim_funds` documentation to remove the
`Error::Unauthorized` entry from its `# Errors` list, since authorization is
enforced through `require_auth()` rather than that error variant. Leave the
documented `CampaignNotSuccessful` and `AlreadyClaimed` errors unchanged.
- Around line 211-225: The initialize function should authenticate the caller
before inspecting initialization state or validating the fee rate. Move
admin.require_auth() to the beginning of initialize, before the DataKey::Admin
check, while preserving the existing validation and storage behavior.
- Around line 1318-1352: The test test_refund_multiple_contributors_all_refunded
does not match its single-contributor assertion. Rename it to describe the id2
single-refund scenario, or revise the setup and assertions to refund and verify
all contributors on a genuinely failed campaign; preserve the existing refund
behavior being tested.
- Around line 1161-1177: Remove the active authorization mocking from
test_trigger_expiry_permissionless, or explicitly clear authorizations with
env.set_auths(&[]) before calling trigger_expiry. Preserve the existing setup
and status assertion so the test genuinely verifies the call succeeds without
authorization.
- Around line 549-586: Update get_fee_collector to handle an absent FeeCollector
value explicitly and return the contract’s Error::NotInitialized error instead
of using a bare unwrap. Preserve returning the stored Address when the value is
initialized and match the established error behavior for uninitialized state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ef6457b-1a14-4876-a954-e0f24437c153

📥 Commits

Reviewing files that changed from the base of the PR and between 375c936 and 7d16295.

📒 Files selected for processing (42)
  • contracts/Cargo.toml
  • contracts/campaign-funding/Cargo.toml
  • contracts/campaign-funding/src/lib.rs
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_double_claim.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_on_failed_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_claim_funds_zero_fee.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_accumulates.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_after_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_auto_succeed_on_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_exceeds_hard_cap.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_multiple_contributors.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_to_nonexistent_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_contribute_zero_amount.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_deadline_in_past.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_ids_increment.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_min_target_exceeds_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_not_initialized.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_min_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_create_campaign_zero_target.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_fee_calculation_precision.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_fee_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_initialize_twice_fails.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_double_refund_prevented.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_multiple_contributors_all_refunded.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_no_contribution.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_active_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_on_successful_campaign.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_refund_success.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_collector.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_set_fee_rate_too_high.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_already_resolved.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_before_deadline.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_permissionless.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_failed_when_target_not_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_sets_successful_when_target_met.1.json
  • contracts/campaign-funding/test_snapshots/tests/test_trigger_expiry_with_zero_contributions_fails.1.json

Comment thread contracts/campaign-funding/src/lib.rs
@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

1 similar comment
@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

@opratem

opratem commented Aug 3, 2026

Copy link
Copy Markdown
Author

conflicts resolved

@coderabbitai coderabbitai Bot mentioned this pull request Aug 3, 2026
10 tasks
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.

[Contract] Implement Auto-Refund Fallback Mechanism on Expired Campaigns

2 participants