Focused analysis for #30.
refund (contracts/escrow/src/lib.rs) is the only state-changing
function in the entire system callable by a completely arbitrary,
unauthenticated caller once env.ledger().timestamp() >= escrow.deadline.
No equivalent exists in milestones or maintenance-pool — both
cancel_milestone and withdraw are admin-only with no timeout escape
hatch (worth noting as a real inconsistency across the three contracts'
sponsor-protection guarantees, discussed at the end).
Yes. Whoever submits the transaction pays that transaction's Stellar
resource fee (a small but non-zero, non-refundable cost). refund has
no tip/reward mechanism — the caller gets nothing back for calling it;
100% of the escrowed amount goes to escrow.sponsor regardless of who
submitted the transaction. So calling refund for a stranger is a
strictly negative-EV action for the caller: real cost, zero benefit.
Both, but for different reasons, and the honest answer requires separating "who realistically calls this in the common case" from "what is the fallback actually protecting against."
In the common case, the realistic caller is one of:
mergefi-backenditself. The README says it "likely does [callrefund] for UX (so sponsors don't have to trigger it manually)" — the backend already holds signing keys and infrastructure to submit transactions forrelease/allocate/withdraw, so calling the permissionlessrefundpath costs it nothing extra to automate.- The sponsor themselves. They have a direct economic incentive — it's
their own money coming back — and can trigger it via the same wallet
they used to call
fund.
Neither of these needs refund to be permissionless — the backend
could just as easily be given admin.require_auth() for this path too
(it already has that for the pre-deadline case), and the sponsor could
self-authorize like extend_deadline (added in this PR) does. Random
altruistic third parties, with no relationship to the sponsor and no
reward, are not a realistic source of calls — the negative-EV
argument above means no rational disinterested party calls this. So in
the day-to-day case, "anyone can call it" is not load-bearing at
all; it's cosmetic relative to what actually happens on-chain.
The fallback is load-bearing, but only in the case it's designed for:
backend unavailability. If mergefi-backend's operator ceases
operating, loses its signing key, or is otherwise unable or unwilling to
submit the refund transaction, the sponsor can still always call
refund themselves once the deadline passes — this doesn't even need
permissionlessness, since the sponsor could self-authorize. Where
permissionlessness earns its keep is one level further: if the sponsor
also can't or won't act (unresponsive, funds controlled by a multisig
that's hard to coordinate quickly, sponsor is an organization that's
since dissolved, etc.), a third party who has some other reason to
care — e.g. the maintainer who was hoping to get paid and wants the
issue reopened for a fresh bounty, a community member helping unwind a
stalled sponsorship, or a future replacement service standing in for a
defunct mergefi-backend — can still return the funds to the sponsor's
address with no coordination or special permission needed. That's a
genuine bankruptcy/custody-remoteness property: no single party's
unavailability can permanently strand a sponsor's expired funds. It's
a real guarantee, just one that matters in the tail case, not the
median case.
No — not in the sense of causing the sponsor harm they didn't already agree to. Two things bound this:
- Funds always go to the address stored in the record
(
escrow.sponsor), never the caller. There is no parameter the caller controls that affects where money moves. So the only possible harm is timing, not redirection or theft. - The deadline is the sponsor's own prior commitment. The sponsor
chose
deadlinethemselves atfund()time. Refund becoming callable-by-anyone at that exact moment is the contract keeping the promise the sponsor asked it to keep ("if this isn't resolved by time T, give my money back") — not a third party overriding the sponsor's preference. Calling it right atTis not materially different from the sponsor having set an alarm and calling it themselves atT; a third party doing it a few seconds earlier than the sponsor got around to it isn't a new harm, it's the same outcome arriving slightly sooner.
The real gap isn't griefing — it's that the sponsor has no way to
change their mind after fund(). If a sponsor watches a PR get close
to merging right as the deadline approaches and would rather wait a bit
longer, the current contract gives them zero mechanism to express that
— deadline is write-once. That's a legitimate design gap the issue
asks to be resolved.
The issue asks whether to "implement a sponsor-settable auto-extend / grace-period flag... balancing against not undermining the sponsor-protective guarantee." Two shapes were considered:
-
Admin/backend-controlled grace period. Rejected. Letting the admin unilaterally delay
refund's permissionless window would reintroduce exactly the dependency the permissionless design exists to remove — a sponsor's guaranteed recovery time would no longer be guaranteed if the backend (the same party whose unavailability is the scenario refund protects against) could also postpone it. Worse, it's a strict downgrade for sponsors trying to escape an unresponsive/compromised backend, which is the one scenario where permissionlessness matters most. -
Sponsor-controlled, monotonic-only extension — implemented as
extend_deadlinein this PR.escrow.sponsor.require_auth(), callable only whilestatus == Funded, and only accepted ifnew_deadlineis strictly later than both the current stored deadline and the current ledger time (Error::InvalidDeadlineotherwise). This directly answers the issue's question with a real mechanism: a sponsor who wants more runway can extend it themselves, at any point before the funds are paid out or refunded, including after the original deadline has already passed (re-closing a window that had opened, if nobody has claimed it yet). Because only the sponsor can call it and it can only push the deadline later, it can't be used by the admin/backend or any third party to trap funds against the sponsor's will, and it can't be used to retroactively undo arefundthat already executed (blocked by theFunded-only status check, same as the existing early-refund admin path). This preserves the exact property that makesrefund's permissionlessness valuable (nobody but the sponsor controls their own timeline) while closing the "sponsor has no say once deadline passes" gap the issue raises.
milestones::cancel_milestone and maintenance-pool::withdraw are
both strictly admin-only with no timeout/escape-hatch concept at all —
unlike escrow, a sponsor whose milestone or maintenance-pool admin goes
permanently unresponsive has no path to recover undistributed funds
except an off-chain intervention (e.g. a contract upgrade or manual
backend action outside this system). This is a real asymmetry across
the three contracts' sponsor-protection guarantees. It's out of scope
for this PR (adding a deadline-based recovery concept to milestones/
maintenance-pool is a modeling change, not an access-control fix — pool
deposits in particular have no natural single "deadline" the way a
single-issue escrow does), but worth a dedicated design issue if the
product wants parity. Not filed as a new issue here since it would
need product input (what would "deadline" even mean for an open-ended,
repeatedly-topped-up maintenance pool?) rather than a purely technical
decision.