Repository navigation
fix(escrow): centralize the reclaim boundary and return only held funds - #262
Conversation
Expiration handling: every entrypoint that settled `deadline + grace_period` inline now reads one saturating `grace_end` helper, surfaced to clients as the new `refund_opens_at` view. The raw `+` could panic on an overflowing sum, while `reclaim`'s `checked_add(deadline as i128, grace_period as i128)? as u64` could truncate a wrapped sum back to a past instant and open a reclaim while the arbiter could still release. Creation now rejects a grace period that would saturate the boundary with `Error::InvalidInput`, so no escrow can be created with a reclaim path that never opens. Reclaim mechanics: `refund` had no party check at all — any address could settle it. It is now sender-only, like `refund_timelock` and `reclaim`, and all three accept `Created` alongside `Funded`/`Expired` so the reclaim paths agree with the `is_refundable` view. Every path that empties an escrow (`refund`, `refund_timelock`, `reclaim`, `cancel`, `release`, `override_release`) now moves only the un-released remainder through one `disburse_remainder` helper, pro-rated per asset, instead of `transfer_all`. Previously a reclaim after a partial recipient withdrawal asked the contract for the full original amount, which would fail — or, with another escrow's funds in custody, drain it. `release_milestone` mirrors its bookkeeping onto `escrow.released_amount` so partially released milestone escrows are covered too, and `override_release` no longer leaves that field stale. Also: an unfunded `Created` escrow can no longer be driven into a token transfer it cannot cover by `claim` or `cancel`, and two misplaced doc comments (`close`'s summary above `cancel`, `validate_assets`' summary above `closes_at`) are back where they belong. Tests: 8 added (46 -> 54) covering the boundary second-by-second, the missing `refund` sender gate, remainder-only reclaims after a partial withdrawal and after a partial milestone release, cancel/release after a partial withdrawal, the unfunded reclaim path, and grace-period overflow rejection at creation.
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
|
@DevRushd 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! 🚀 |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
1 similar comment
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
CI Checks Failed — Action NeededThe continuous integration checks on this pull request are failing. I can't merge until all required checks pass. Failing checks:
What to do: review the failing checks in the "Checks" tab above, fix the issues in your code, and push the changes. |
Merge Conflict — Action NeededThis pull request has merge conflicts with the base branch ( What to do: update your branch by merging or rebasing against |
Closes #225
What this does
Refactors escrow expiration handling so the reclaim boundary exists once, and reworks fund reclaim so every settlement path returns only what the escrow still holds.
1. One reclaim boundary, computed safely
grace_end(escrow)=deadline.saturating_add(grace_period)replaces seven inlinedeadline + grace_periodcomputations (claimx2,release,expire,refund,refund_timelock,is_refundable) plusreclaim's ad-hocchecked_add(deadline as i128, grace_period as i128)? as u64and the first half ofcloses_at.+panics on an overflowingu64sum; thei128roundtrip is worse — it computes a validi128sum and then truncates it back tou64, which can wrap the boundary to a past instant and open a reclaim while the arbiter can still release.refund_opens_at(id)exposes the boundary to clients (pairs with the existingrefund_window_closes_at, which now measures from it).create_with_refund_window(and thereforecreate) andinitialize_timelockreject agrace_periodthat would saturate the boundary withError::InvalidInput, before any state is written, so no escrow can be created whose reclaim path never opens (permanent lockup).2.
refundwas missing its party checkrefundcalledcaller.require_auth()and then verified nothing aboutcaller— any address could force-settle someone else's escrow after the deadline. It is now sender-only, matchingrefund_timelockandreclaim(funds always return to the depositor).refundandreclaimalso acceptCreatedalongsideFunded | Expired, so the reclaim paths agree with theis_refundableview that already advertisedCreated | Funded | Expired. Unfunded timelock escrows can now be settled instead of being stuck betweenInvalidStateand a trapping transfer.3. Reclaims return only the remainder (
disburse_remainder)Previously
reclaimandcancelusedtransfer_all, sending each asset's original amount even when the recipient had already withdrawn part of a vested escrow. With 6_000 of a 10_000 escrow left in custody that transfer fails; with a second escrow's funds sitting in the same contract it would silently drain them.All six paths that empty an escrow —
refund,refund_timelock,reclaim,cancel,release,override_release— now go through one helper that pro-ratesamount * (funded_amount - released_amount) / funded_amountper asset and emits the shared("transfer", "executed")event for every amount actually moved (refund/refund_timelockpreviously emitted none).Supporting fixes required to make the invariant true:
release_milestonemirrorsset.released_amountontoescrow.released_amount(previously only stored on the final milestone), so a partially released milestone escrow can no longer look untouched.override_releaserecordsreleased_amount = funded_amountinstead of leaving it stale on aReleasedrecord.claimon an unfundedCreatedescrow returnsInvalidStateinstead of attempting a transfer the contract cannot cover;cancelon the same record now transitions cleanly with no transfer.Doc corrections
close's summary sat abovecancel, andvalidate_assets' summary sat abovecloses_at; both are restored to their functions.release's doc claimed partial releases are supported while the implementation always pays the whole remainder; the doc now describes the realrelease_amountsemantics (upper bound, remainder paid) — behaviour is unchanged.release's "before the deadline" corrected to "until the reclaim boundary is reached", and the module docs gained a "Reclaim rules" section.Tests
contracts/escrow/src/test.rsgains 8 tests (46 → 54, all green):refund_opens_at_reports_the_reclaim_boundaryGraceActive,expire→InvalidState,is_refundablefalse) and exactly at it (all three open, refund succeeds)refund_rejected_for_non_senderUnauthorized; sender still succeedsreclaim_after_partial_withdrawal_returns_only_the_remainderreclaim_after_partial_milestone_release_returns_only_the_remainderreleased_amountis mirroredcancel_after_partial_withdrawal_returns_only_the_remainderrelease_after_partial_withdrawal_pays_only_the_remainderunfunded_timelock_escrow_has_a_clean_reclaim_pathis_refundable/refundagree onCreated;claimrefuses instead of trappingabsurd_grace_period_is_refused_at_creationcreateandinitialize_timelockrejectu64::MAXgrace withInvalidInput, no tokens pulled, id counter untouchedValidation (CI commands)
No emojis; no collaborator/contributor additions.