Skip to content

Escrow: add a residual-release op so organizers can recover unawarded prize funds #24

Description

@0xdevcollins

Summary

Add an on-chain operation that lets a hackathon event owner recover the unawarded residual of an escrow event after select_winners. Today there is no way to get those funds back without canceling the whole event (which is only valid before winners are selected).

Why

The app now lets organizers run a task-first Winners flow where they pick a winner per prize and can deliberately leave a prize unawarded ("no submission earned the grand prize"), and prizes can also be unfilled when there are fewer eligible projects than prize slots. In both cases the prize's funds stay in the on-chain event.

The current escrow fund-out paths (EscrowOpKind) are:

  • SELECT_WINNERS — pays only the winners in the provided list. Anything not awarded stays in the event balance.
  • The paged cancel flow (START_CANCEL / PROCESS_CANCEL_BATCH / FINALIZE_CANCEL) — refunds everyone (partners-first, owner residual on finalize), and is only valid before winners are selected.

So once select_winners runs and the hackathon is COMPLETED, the unawarded residual is stranded with no recovery path. The app currently discloses this honestly at confirm ("That money stays in the prize pool and is not paid out."), but there is no way to act on it.

Proposed contract change

Add an owner-only op, e.g. release_residual(event_id, op_id) (name TBD), callable after select_winners, that transfers the remaining event balance out:

  • Recommended payout policy: partners-first, owner residual — mirror the existing cancel/finalize residual logic so partner-contributed funds are returned to contributors before the owner takes the remainder. (Confirm whether contributions should be refunded or whether only the owner's own surplus is releasable — see open questions.)
  • Idempotent + op_id-tracked like the other ops; safe to retry; guarded so it can only run once and only on a Completed/post-select_winners event.
  • Should be a no-op (or revert cleanly) when residual is zero.

App wiring (follow-up, boundless-nestjs + boundless)

Once the contract op exists:

  • boundless-nestjs: add an EscrowOpKind (e.g. RELEASE_RESIDUAL), EscrowContractClientService.buildReleaseResidual, orchestrator begin-method, a HackathonOrganizerService.releaseResidual, an organizer endpoint, and settlement handling in the escrow subscriber.
  • boundless (Winners page): a post-publish "Recover unawarded funds" action (confirm/OTP, owner/admin only), and update the confirm-time disclosure copy from "stays in the prize pool" to reflect that residual is now recoverable.

Acceptance criteria

  • After winners are paid, the owner can recover exactly the unawarded remainder of the event balance.
  • Partner-contributed unallocated funds are handled per the agreed policy (refund vs owner-release).
  • The op is idempotent, op_id-reconciled via EscrowOp, and cannot double-spend.

Open questions

  • For unawarded prizes funded partly by partner contributions: refund those contributors, or release everything to the owner? (Leaning: refund partners-first to match cancel semantics.)
  • Should this be one bulk "release all residual" or per-placement? (Bulk is simpler and sufficient for the current UX.)

References

  • App disclosure: boundless-nestjs hackathon-results.service.ts publishResults (no-winners gate) + the Winners confirm dialog in boundless app/(landing)/organizations/[id]/hackathons/[hackathonId]/winners/page.tsx.
  • Withhold support: Hackathon.withheldPlacementIds + winner-allocator.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions