Skip to content

Add a background job to auto-refund expired FUNDED bounties on-chain, not just flip a status flag locally #26

Description

@chonilius

Background

BountiesService.expireOverdue finds overdue bounties still in
OPEN/FUNDED/CLAIMED and flips their status to EXPIRED. Per the state
machine, EXPIRED -> REFUNDED is a valid subsequent transition, but
nothing in expireOverdue actually calls EscrowService.refund — it only
updates the Bounty.status column. There's also no visible scheduled job
(no @nestjs/schedule cron, confirm it's not a dependency) that even calls
expireOverdue automatically — investigate whether it's wired to run on
any schedule at all, or whether it's dead code only reachable via a manual
admin call.

Problem Statement

If a sponsor funds a bounty (locking real funds in escrow) and it expires
unclaimed/unmerged, the current behavior — even in the best case where
expireOverdue is actually invoked on a schedule — only marks it
EXPIRED in Postgres while the sponsor's funds remain locked in the escrow
contract indefinitely. The sponsor has no automatic recourse; someone must
manually notice the bounty is expired and manually call the refund flow
(assuming a controller endpoint for it even exists and is discoverable —
verify). For a platform whose core promise is "funds move automatically,"
silently stranding sponsor funds in escrow after expiry is a serious product
and trust failure.

Requirements

  • Wire up an actual recurring job (add @nestjs/schedule if not present,
    or document/implement an equivalent — a documented external cron hitting
    an admin endpoint is acceptable if justified) that calls
    expireOverdue on a sensible interval.
  • Extend expireOverdue (or add a follow-up step) to automatically trigger
    EscrowService.refund for each bounty that had funds locked
    (escrowId set) when it expires — respecting the existing state machine
    (EXPIRED -> REFUNDED) and the locking/transaction fixes from the other
    issues in this batch so this doesn't introduce a new race against a
    simultaneous claim/merge.
  • Handle partial failure: if the on-chain refund call fails for one bounty
    in a batch of expiring bounties, the rest must still be processed (don't
    let one failure abort the whole batch), and the failure must be visibly
    logged/retried, not silently dropped.
  • Add tests: an expired, funded bounty is automatically refunded on-chain
    (verified via the mocked SorobanClientService); an expired, never
    funded
    bounty (still OPEN, no escrow) is marked expired without
    attempting a refund; a batch with one failing refund still processes the
    rest.

Acceptance Criteria

  • A real scheduled job invokes expireOverdue-and-refund on an
    interval, not just a manually-reachable method.
  • Every expired bounty with locked funds is automatically refunded
    on-chain, verified by tests against the escrow layer, not just a
    status-column check.
  • One failing refund in a batch does not prevent the rest from
    processing, and failures are logged/retryable.
  • No new race introduced against concurrent claim/merge flows for the
    same bounty (reuse the locking pattern from the bounty-claim-race
    issue).

Technical Notes

Files: src/bounties/bounties.service.ts, src/escrow/escrow.service.ts,
package.json (add @nestjs/schedule if used), src/bounties/bounties.module.ts.

Difficulty Justification

This closes an actual fund-stranding gap in the product's core value
proposition, requires integrating a new scheduling mechanism correctly with
existing transaction/locking patterns, and requires careful partial-failure
handling across a batch operation touching a live smart contract — not a
simple "add a cron decorator" change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26architectureArchitecture/design issuebugSomething isn't workingenhancementNew feature or requestvery hardVery difficult task, expert-level effort required

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions