Skip to content

escrow: support multi-sponsor crowdfunding for a single issue_id - #63

Merged
chonilius merged 12 commits into
MergeFi:mainfrom
circleboyslimited:fix/escrow-crowdfunding
Aug 16, 2026
Merged

escrow: support multi-sponsor crowdfunding for a single issue_id#63
chonilius merged 12 commits into
MergeFi:mainfrom
circleboyslimited:fix/escrow-crowdfunding

Conversation

@circleboyslimited

Copy link
Copy Markdown
Contributor

Summary

Closes #57

escrow::fund previously accepted exactly one sponsor: Address and rejected a second fund call against the same issue_id outright, so there was no on-chain way for more than one sponsor to co-fund the same issue. This adds that support.

Design

Full reasoning in docs/escrow-crowdfunding-design.md. Summary of the three decisions the issue asked to be made explicitly:

  • Contribution model: fund still creates the escrow (unchanged AlreadyFunded guard on a second call). A new contribute(issue_id, sponsor, amount) is the entrypoint every sponsor after the first uses — it reuses the escrow's already-recorded token, so a top-up can't silently use a different asset. Each contribution is stored as its own Contribution { sponsor, amount } entry (DataKey::Contribution(issue_id, index)), the same per-entry shape maintenance-pool::Deposit already uses, rather than one growing Vec on Escrow.
  • Refund: iterates every recorded contribution and pays each Contribution.sponsor exactly their own Contribution.amount back — no proportional-split math needed, since each contribution is stored as an exact amount rather than a share.
  • extend_deadline: now takes an explicit caller: Address and accepts any current contributor (not unanimous or contribution-weighted consent) — extending only ever delays refund's permissionless path, never redirects funds or changes anyone's share.
  • Unbounded-collection guard (Unbounded recipients: Vec<(Address, u32)> in release/release_issue risks resource-limit transaction failure #8/Unbounded growth of Milestone.allocations Map degrades and eventually threatens milestone usability #9): MAX_SPONSORS = 20 caps contributor_count, bounding refund's and extend_deadline's per-contributor loops to a small, predictable constant.

Changes

  • contracts/escrow/src/types.rs: Escrow drops sponsor, gains contributor_count: u32; new Contribution struct; new DataKey::Contribution(issue_id, index).
  • contracts/escrow/src/error.rs: new TooManySponsors error.
  • contracts/escrow/src/lib.rs: fund records contribution index 0; new contribute; refund iterates contributions; extend_deadline takes a caller param and checks any-contributor membership; new get_contribution view getter.
  • contracts/escrow/src/test.rs: 10 new tests (multi-sponsor fund/refund/release, contribute's auth/validation/cap guards, any-contributor extend_deadline + non-contributor rejection, get_contribution enumeration), plus updates the 4 pre-existing extend_deadline tests for the new signature.
  • README.md: escrow API reference and data model updated to match.

Test plan

  • cargo test --workspace — 28/28 escrow (18 pre-existing + 10 new) + 7 maintenance-pool + 10 milestones, all passing.
  • cargo clippy --workspace --tests — clean.
  • cargo fmt --check --all — clean.

…MergeFi#57)

Focused design analysis for MergeFi#57: the contribution model (fund creates,
contribute appends), why refund needs exact per-contributor reimbursement
rather than proportional splitting, the any-contributor extend_deadline
semantics, and the MAX_SPONSORS bound rationale — written before the
implementation that follows in subsequent commits.
Adds Contribution { sponsor, amount } and DataKey::Contribution(issue_id,
index) as the per-contributor storage shape (MergeFi#57), and a new
TooManySponsors error for the cap added in a later commit. Escrow gains
a contributor_count field; fund() is updated to set it to 1 for the
first (and, until the next commit, still only) contributor. The
contribution ledger itself isn't written yet — this is purely the data
model addition.
Adds MAX_SPONSORS (20) and starts writing to the contribution ledger:
fund() now records its sponsor/amount as Contribution(issue_id, 0),
treating the original funder uniformly as the first contributor rather
than special-casing them relative to sponsors who join later via
contribute() (added next).
The second half of the create/append split (MergeFi#57): fund() remains the
sole entrypoint that creates an escrow (unchanged AlreadyFunded guard);
contribute() is the new entrypoint every sponsor after the first uses to
add more funds, capped at MAX_SPONSORS distinct contributions per
escrow. Not yet wired into refund/extend_deadline — those still only
know about the original single sponsor field, updated in the next
commits.
…ponsor

refund() now pays every contributor exactly their own recorded amount
(MergeFi#57), rather than the full escrow.amount to a single escrow.sponsor.
For a single-sponsor escrow this is exactly equivalent to the old
behavior (contributor_count is always 1, so the loop runs once) —
verified by the unmodified existing refund tests still passing. escrow.
sponsor is still read by extend_deadline, updated in the next commit.
…e sponsor field

Breaking API change (MergeFi#57): extend_deadline now takes an explicit caller
parameter and accepts any address recorded as a contributor for that
issue_id, rejecting non-contributors with Unauthorized — a direct
generalization of the old escrow.sponsor.require_auth() rule to however
many sponsors have now co-funded the escrow. With refund and
extend_deadline both now working purely off the contribution ledger,
Escrow's single sponsor field is redundant and removed; fund() no
longer writes it. Updates the 4 existing extend_deadline tests for the
new signature (auth still comes from the sponsor address, just passed
explicitly now).
…contributors

Lets off-chain callers (mergefi-backend, indexers) enumerate the full
contribution ledger for an issue_id via 0..escrow.contributor_count,
mirroring the existing get_escrow/get_admin/get_treasury getter pattern.
Completes the implementation surface of MergeFi#57's design; remaining commits
add test coverage and update docs.
Small doc-only follow-up to the crowdfunding implementation (MergeFi#57):
fund()'s doc comment still described the old single-sponsor-only
behavior; updates it to explain the AlreadyFunded/contribute split now
that a second fund() call and a contribute() call mean different
things.
…wdfunding (MergeFi#57)

Adds the two core crowdfunding regression tests: three sponsors
co-funding one issue_id with unequal amounts get back exactly their own
contribution on refund (not an even split, not the full amount to one
sponsor), and release() correctly pays out the combined total across
all contributors, same fee math as a single-sponsor escrow.
…w, terminal status, MAX_SPONSORS cap (MergeFi#57)

Five tests exercising contribute()'s validation surface: rejects without
sponsor auth, rejects amount <= 0, rejects an unknown issue_id, rejects
once the escrow is already Paid, and rejects the 21st distinct
contribution once MAX_SPONSORS (20) is reached — the resource-bound
guard the design doc calls out as addressing the same unbounded-
collection concern raised elsewhere in this codebase (MergeFi#8/MergeFi#9).
…n, and get_contribution (MergeFi#57)

Three tests: a second (non-original) contributor can successfully
extend the shared deadline, a stranger who never contributed is
rejected with Unauthorized even with valid auth for themselves, and
get_contribution correctly enumerates each recorded contributor by
index and returns EscrowNotFound past the last one.
…ution() and extend_deadline's new signature (MergeFi#57)

Brings the README's escrow section back in sync with the implemented
API: adds contribute and get_contribution to the function list and
descriptions, updates extend_deadline's signature and description for
the caller param and any-contributor semantics, and updates the Escrow/
Contribution data model block (dropped sponsor field, added
contributor_count and the new Contribution struct).
@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

@circleboyslimited is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@chonilius

Copy link
Copy Markdown
Contributor

all Ci passed. well done

@chonilius
chonilius merged commit 086dbfb into MergeFi:main Aug 16, 2026
2 of 3 checks passed
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.

escrow: no crowdfunding support — a single issue_id can only ever be funded by one sponsor address

2 participants