Problem
escrow/src/lib.rs has exactly three state-changing operations: create_escrow, release_escrow, and refund_escrow (lines 86-194). There is no way for depositor and beneficiary to mutually agree to unwind an escrow early — e.g. a trade falls through by mutual agreement before unlock_time, with no arbiter configured (arbiter: None). In that scenario, funds are locked until either unlock_time (beneficiary can then release_escrow to themself, but they may not want the funds — they want to give them back!) or unlock_time + REFUND_GRACE_SECONDS (depositor can self-refund), whichever the parties don't actually want.
Why it matters
Without an arbiter, two cooperating parties have no way to short-circuit an escrow they both agree should be canceled — they're stuck waiting out unlock_time + REFUND_GRACE_SECONDS in the worst case (over a week past the original unlock point) even though there's no actual dispute. This is a real usability gap for the common "we agreed to cancel the deal" case, distinct from the adversarial cases the existing release/refund logic is designed around.
Proposed fix
Add a mutual_cancel_escrow(env: Env, escrow_id: u64, depositor: Address, beneficiary: Address) -> Result<(), EscrowError> requiring both depositor.require_auth() and beneficiary.require_auth() in the same call (Soroban supports multiple auth entries in one invocation), refunding the full amount to depositor (or, alternatively, requiring the parties to specify a split — simpler to always refund depositor in full and let the beneficiary's consent alone be the safety check, since if they wanted differently they wouldn't consent).
Edge cases
- Must still respect
assert_locked — an already-released/refunded escrow can't be mutually cancelled.
- If an
arbiter is set, should mutual cancellation still be available (bypassing the arbiter entirely since both principals agree), or should it be disallowed in favor of routing through the arbiter? Given the arbiter's whole purpose is dispute resolution and there's no dispute in the mutual-consent case, allowing it seems reasonable, but worth explicit maintainer sign-off since it changes the arbiter's implied authority.
- Should emit its own event (
escrow_cancelled) distinct from emit_escrow_refunded so indexers can distinguish "cooperative cancel" from "unilateral depositor refund after grace period," since they have different risk/trust implications.
Testing strategy
Add test_mutual_cancel_requires_both_signatures (fails if either party's auth is missing), test_mutual_cancel_refunds_depositor, test_mutual_cancel_rejected_if_already_released, and test_mutual_cancel_rejected_if_already_refunded.
Problem
escrow/src/lib.rshas exactly three state-changing operations:create_escrow,release_escrow, andrefund_escrow(lines 86-194). There is no way fordepositorandbeneficiaryto mutually agree to unwind an escrow early — e.g. a trade falls through by mutual agreement beforeunlock_time, with no arbiter configured (arbiter: None). In that scenario, funds are locked until eitherunlock_time(beneficiary can thenrelease_escrowto themself, but they may not want the funds — they want to give them back!) orunlock_time + REFUND_GRACE_SECONDS(depositor can self-refund), whichever the parties don't actually want.Why it matters
Without an arbiter, two cooperating parties have no way to short-circuit an escrow they both agree should be canceled — they're stuck waiting out
unlock_time + REFUND_GRACE_SECONDSin the worst case (over a week past the original unlock point) even though there's no actual dispute. This is a real usability gap for the common "we agreed to cancel the deal" case, distinct from the adversarial cases the existingrelease/refundlogic is designed around.Proposed fix
Add a
mutual_cancel_escrow(env: Env, escrow_id: u64, depositor: Address, beneficiary: Address) -> Result<(), EscrowError>requiring bothdepositor.require_auth()andbeneficiary.require_auth()in the same call (Soroban supports multiple auth entries in one invocation), refunding the full amount todepositor(or, alternatively, requiring the parties to specify a split — simpler to always refund depositor in full and let the beneficiary's consent alone be the safety check, since if they wanted differently they wouldn't consent).Edge cases
assert_locked— an already-released/refunded escrow can't be mutually cancelled.arbiteris set, should mutual cancellation still be available (bypassing the arbiter entirely since both principals agree), or should it be disallowed in favor of routing through the arbiter? Given the arbiter's whole purpose is dispute resolution and there's no dispute in the mutual-consent case, allowing it seems reasonable, but worth explicit maintainer sign-off since it changes the arbiter's implied authority.escrow_cancelled) distinct fromemit_escrow_refundedso indexers can distinguish "cooperative cancel" from "unilateral depositor refund after grace period," since they have different risk/trust implications.Testing strategy
Add
test_mutual_cancel_requires_both_signatures(fails if either party's auth is missing),test_mutual_cancel_refunds_depositor,test_mutual_cancel_rejected_if_already_released, andtest_mutual_cancel_rejected_if_already_refunded.