Skip to content

feat(cancel_escrow): add cancel_escrow_split_refund allocation algori… - #376

Merged
godamongstmen897 merged 129 commits into
Goldii-locks:mainfrom
Jessepriase:feat/cancel-escrow-refund-allocation
Sep 1, 2026
Merged

feat(cancel_escrow): add cancel_escrow_split_refund allocation algori…#376
godamongstmen897 merged 129 commits into
Goldii-locks:mainfrom
Jessepriase:feat/cancel-escrow-refund-allocation

Conversation

@Jessepriase

Copy link
Copy Markdown
Contributor

…thm (#292)

Define refund distribution pathways for split-refund claims arising from cancel_escrow initiations. The new function is a pure calculator that computes how the escrowed balance is split between client and freelancer using caller-supplied BPS ratios, following the exact same pattern as multisig_split_refund and emergency_pause_split_refund.

Production changes (lib.rs):

  • Add CancelSplitRefundCalculatedEvent struct (mirrors SplitRefundCalculatedEvent) Fields: client_refund, freelancer_payout, client_refund_bps, freelancer_payout_bps
  • Add pub fn cancel_escrow_split_refund(env, total_amount, client_refund_bps, freelancer_payout_bps) -> Result<RefundAllocation, Error>
    • Guard: total_amount <= 0 -> Error::InvalidAmount
    • Guard: client_refund_bps + freelancer_payout_bps != BPS_SCALE -> Error::InvalidRatio
    • Guard: u32 overflow on BPS addition -> Error::InvalidRatio
    • Uses split_round_nearest: client share is round-nearest, freelancer receives the exact remainder so first + second == total_amount always
    • Emits symbol_short!("cxlspref") with CancelSplitRefundCalculatedEvent
    • Pure calculator — no storage reads/writes, no auth required

Tests added (test.rs) — 17 new tests (396 -> 413):

Correct percentage calculation:
- test_cancel_escrow_split_refund_full_client_refund
- test_cancel_escrow_split_refund_full_freelancer_payout
- test_cancel_escrow_split_refund_equal_split
- test_cancel_escrow_split_refund_70_30
- test_cancel_escrow_split_refund_tiny_client_share

No-value-lost rounding guarantee:
- test_cancel_escrow_split_refund_odd_total_rounds_nearest_no_value_lost
- test_cancel_escrow_split_refund_large_prime_total_preserved
- test_cancel_escrow_split_refund_single_stroop_total

Validation guards:
- test_cancel_escrow_split_refund_zero_amount_fails
- test_cancel_escrow_split_refund_negative_amount_fails
- test_cancel_escrow_split_refund_bps_not_summing_to_scale_fails
- test_cancel_escrow_split_refund_bps_overflow_fails
- test_cancel_escrow_split_refund_both_bps_zero_fails

Event emission:
- test_cancel_escrow_split_refund_emits_event
- test_cancel_escrow_split_refund_event_payload_correct

BPS echoing:
- test_cancel_escrow_split_refund_bps_echoed_in_allocation

Standalone calculator:
- test_cancel_escrow_split_refund_works_without_initialization

All 413 tests pass.
closes #292

zeemscript and others added 11 commits August 26, 2026 16:14
…tests (Goldii-locks#290)

Harden cancel_escrow with two missing validation rules and add a full
test suite covering every guard, happy path, post-cancel state, event
structure, and milestone isolation.

Production changes (lib.rs):
- Add EmergencyPaused guard: cancel_escrow now returns Error::Paused when
  the contract is emergency-paused, consistent with all other user-facing
  endpoints
- Add duplicate-cancel guard: a second call while CancelLock is already
  active returns Error::EscrowLocked, preventing race conditions and
  redundant lock-sets

Tests added (test.rs) — 20 new tests:

  Invalid address guards:
    - test_cancel_escrow_zero_account_address_rejected
    - test_cancel_escrow_zero_contract_address_rejected

  Not-initialized guard:
    - test_cancel_escrow_not_initialized_fails

  Not-funded guard:
    - test_cancel_escrow_not_funded_fails

  Unauthorized guards:
    - test_cancel_escrow_stranger_unauthorized
    - test_cancel_escrow_arbiter_unauthorized
    - test_cancel_escrow_admin_unauthorized

  Emergency-paused guard:
    - test_cancel_escrow_while_paused_fails

  Duplicate-cancel guard:
    - test_cancel_escrow_duplicate_call_fails
    - test_cancel_escrow_freelancer_duplicate_after_client_fails

  Happy paths:
    - test_cancel_escrow_client_succeeds
    - test_cancel_escrow_freelancer_succeeds

  Post-cancel state validation:
    - test_cancel_escrow_blocks_fund
    - test_cancel_escrow_blocks_mark_delivered
    - test_cancel_escrow_blocks_approve_milestone
    - test_cancel_escrow_blocks_raise_dispute

  Event validation:
    - test_cancel_escrow_emits_exactly_one_event
    - test_cancel_escrow_event_contains_correct_caller

  Milestone state isolation:
    - test_cancel_escrow_does_not_mutate_milestones
    - test_cancel_escrow_all_milestones_released_still_succeeds

All 218 tests pass.
…thm (Goldii-locks#292)

Define refund distribution pathways for split-refund claims arising
from cancel_escrow initiations. The new function is a pure calculator
that computes how the escrowed balance is split between client and
freelancer using caller-supplied BPS ratios, following the exact same
pattern as multisig_split_refund and emergency_pause_split_refund.

Production changes (lib.rs):
- Add CancelSplitRefundCalculatedEvent struct (mirrors SplitRefundCalculatedEvent)
  Fields: client_refund, freelancer_payout, client_refund_bps, freelancer_payout_bps
- Add pub fn cancel_escrow_split_refund(env, total_amount, client_refund_bps,
  freelancer_payout_bps) -> Result<RefundAllocation, Error>
  * Guard: total_amount <= 0 -> Error::InvalidAmount
  * Guard: client_refund_bps + freelancer_payout_bps != BPS_SCALE -> Error::InvalidRatio
  * Guard: u32 overflow on BPS addition -> Error::InvalidRatio
  * Uses split_round_nearest: client share is round-nearest, freelancer
    receives the exact remainder so first + second == total_amount always
  * Emits symbol_short!("cxlspref") with CancelSplitRefundCalculatedEvent
  * Pure calculator — no storage reads/writes, no auth required

Tests added (test.rs) — 17 new tests (396 -> 413):

  Correct percentage calculation:
    - test_cancel_escrow_split_refund_full_client_refund
    - test_cancel_escrow_split_refund_full_freelancer_payout
    - test_cancel_escrow_split_refund_equal_split
    - test_cancel_escrow_split_refund_70_30
    - test_cancel_escrow_split_refund_tiny_client_share

  No-value-lost rounding guarantee:
    - test_cancel_escrow_split_refund_odd_total_rounds_nearest_no_value_lost
    - test_cancel_escrow_split_refund_large_prime_total_preserved
    - test_cancel_escrow_split_refund_single_stroop_total

  Validation guards:
    - test_cancel_escrow_split_refund_zero_amount_fails
    - test_cancel_escrow_split_refund_negative_amount_fails
    - test_cancel_escrow_split_refund_bps_not_summing_to_scale_fails
    - test_cancel_escrow_split_refund_bps_overflow_fails
    - test_cancel_escrow_split_refund_both_bps_zero_fails

  Event emission:
    - test_cancel_escrow_split_refund_emits_event
    - test_cancel_escrow_split_refund_event_payload_correct

  BPS echoing:
    - test_cancel_escrow_split_refund_bps_echoed_in_allocation

  Standalone calculator:
    - test_cancel_escrow_split_refund_works_without_initialization

All 413 tests pass.
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@Jessepriase 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! 🚀

Learn more about application limits

Jessepriase and others added 18 commits August 27, 2026 15:10
…tests (Goldii-locks#293)

Expand CancelEscrowInitiatedEvent with full operational context so
downstream indexers can reconstruct the complete cancellation state
from the event payload alone, without querying contract storage.

Production changes (lib.rs):
- Expand CancelEscrowInitiatedEvent with six new fields:
    caller_is_client bool   — true if initiator is the client, false if freelancer
    client           Address — registered client address
    freelancer       Address — registered freelancer address
    token            Address — escrow token contract address
    milestone_count  u32    — number of milestones at cancellation time
    total_amount     i128   — aggregate milestone total (pre-release)
- Update cancel_escrow event emission to populate all new fields,
  deriving caller_is_client from (caller == meta.client)

No other code changed.

Tests added (test.rs) — 14 new tests (396 -> 410):

  Event count:
    - test_cancel_escrow_event_emitted_exactly_once

  contract_id field:
    - test_cancel_escrow_event_contract_id_correct

  caller field:
    - test_cancel_escrow_event_caller_is_client_address
    - test_cancel_escrow_event_caller_is_freelancer_address

  caller_is_client role field:
    - test_cancel_escrow_event_caller_is_client_true_for_client
    - test_cancel_escrow_event_caller_is_client_false_for_freelancer

  client / freelancer / token fields:
    - test_cancel_escrow_event_client_field_correct
    - test_cancel_escrow_event_freelancer_field_correct
    - test_cancel_escrow_event_token_field_correct

  milestone_count field:
    - test_cancel_escrow_event_milestone_count_single
    - test_cancel_escrow_event_milestone_count_multiple

  total_amount field:
    - test_cancel_escrow_event_total_amount_correct_single_milestone
    - test_cancel_escrow_event_total_amount_correct_multi_milestone

  Full indexer round-trip:
    - test_cancel_escrow_event_full_indexer_parse

All 410 tests pass.
…es (Goldii-locks#295)

Require both client and freelancer to approve before cancel_escrow sets
CancelLock. A single-signature call records approval only and returns
immediately — the escrow remains fully operational until the second
party signs.

Production changes (lib.rs):
- Add DataKey::CancelApproval — persistent u32 bitmask (bit 0 = client,
  bit 1 = freelancer) tracking which parties have signed
- Add CancelApprovalRecordedEvent — emitted on first/partial approval
  with the current mask (symbol: cxlappr)
- Add CancelApprovalRevokedEvent — emitted when a party withdraws their
  approval (symbol: cancelrev)
- Rewrite cancel_escrow two-phase logic:
    * Zero-address guard unchanged
    * Single call records caller_bit in CancelApproval mask, emits
      cxlappr, returns Ok — lock NOT set
    * Duplicate call from same party returns Error::InvalidStatus
    * Second call (mask == 3) clears CancelApproval, sets CancelLock,
      emits final cancel event
    * Call while lock already active returns Error::EscrowLocked
- Add revoke_cancel_approval(env, caller) -> Result<(), Error>
    * Party can withdraw their approval before lock fires
    * Returns InvalidStatus if no approval recorded or lock already active
    * Returns Unauthorized for non-parties
    * Emits cancelrev event with updated mask

cancel_escrow_test.rs — updated two existing tests:
- test_cancel_escrow_sets_lock_and_emits_event: updated to call both
  parties and assert lock fires only after second signature
- test_cancel_escrow_by_freelancer_succeeds: updated to use two-phase
  sequence (freelancer first, then client)

New tests (test.rs) — 19 new tests (396 -> 415):

  Single-signature reverts:
    - test_cancel_escrow_single_client_signature_does_not_lock
    - test_cancel_escrow_single_freelancer_signature_does_not_lock
    - test_cancel_escrow_duplicate_same_party_reverts
    - test_cancel_escrow_duplicate_freelancer_reverts

  Two-party approval completes cancel:
    - test_cancel_escrow_client_then_freelancer_locks
    - test_cancel_escrow_freelancer_then_client_locks
    - test_cancel_escrow_third_call_after_lock_fails

  Approval bitmask and events:
    - test_cancel_escrow_first_signature_emits_approval_event
    - test_cancel_escrow_freelancer_first_emits_mask_2
    - test_cancel_escrow_second_signature_emits_cancel_event
    - test_cancel_escrow_approval_key_cleared_after_lock

  revoke_cancel_approval:
    - test_revoke_cancel_approval_client_before_second_sig
    - test_revoke_then_reapprove_completes_cancel
    - test_revoke_cancel_approval_without_prior_approval_fails
    - test_revoke_cancel_approval_after_lock_fails
    - test_revoke_cancel_approval_non_party_unauthorized
    - test_revoke_cancel_approval_emits_event

  Unauthorized callers:
    - test_cancel_escrow_arbiter_cannot_sign
    - test_cancel_escrow_admin_cannot_sign

All 415 tests pass.
…ocks#298)

Audited tax_withholding_deductions's existing validation against this
issue's requirement ("assert that bad setups are rejected immediately
with descriptive error types") and found the existing coverage already
extensive: NotInitialized, NotFunded, InvalidMilestone, InvalidRatio,
InvalidAmount (zero balance, overflow), and InvalidStatus for
Released/Refunded milestones are all implemented and tested (test.rs,
tax_withholding_tests.rs).

One real gap: the status check only excluded Released/Refunded, not
Disputed — unlike raise_dispute_inner and resolve_dispute elsewhere in
this file, which both treat Disputed as its own case via an exhaustive
match. A disputed milestone's funds are meant to be frozen pending
resolve_dispute; tax_withholding_deductions could still compute and
persist a TaxWithholdingRecord for one, moving money around that freeze.
No existing test combined raise_dispute with tax_withholding_deductions,
so this was uncovered on both the implementation and test side.

Fixes the gap by converting the two equality checks to an exhaustive
match over MilestoneStatus (so a future new variant fails to compile
here instead of silently falling through as allowed), matching the
established pattern elsewhere in this file, and adds a test exercising
it via the existing dual-signature test fixture.

Not touched: admin_tax_withholding_deductions, a separately-implemented
sibling function (per its own doc comment, "arrived from a separate PR
under the same name") that has no status check at all — a larger gap,
but outside this issue's named scope (Module/Component:
tax_withholding_deductions).
Drop the redundant MilestoneReleased(index) temporary-flag write from
multisig_admin_override_release. The flag's only reader
(is_milestone_released_flag) is dead code, and the persisted Released
status on the milestone is the authoritative completion signal. This
reduces the distinct storage keys written by the call from three
(Milestone, MilestoneReleased, MultisigLocked) to two (Milestone,
MultisigLocked), matching multisig_admin_override_refund.

Adds test_multisig_admin_override_release_reduced_storage_footprint to
assert the temporary flag is no longer written while token transfer,
lock clearing, milestone state, and terminal re-entry rejection still
hold. Updates the affected snapshot fixtures accordingly.

Closes Goldii-locks#392
Guard the refund arithmetic so no input can cause a wrap or an unhandled
panic (issue Goldii-locks#395). Negative amount / released_amount and
released_amount > amount now return Error::InvalidAmount before any
arithmetic runs, complementing the existing checked_sub and remaining<=0
guards. The same guards are applied to the sibling
multisig_admin_override_release for consistency.

Adds a comprehensive suite to multisig_admin_override_refund_tests
asserting i128::MAX / i128::MIN operands return Error::InvalidAmount
(rather than panicking) and that valid amounts refund exactly
amount - released_amount, identical to prior behavior.

Closes Goldii-locks#395
…_approve (closes Goldii-locks#354)

Reorder multisig_approve so the signer-membership check runs before any
job/token ledger reads (job meta load, cross-contract balance call),
matching the guard ordering used elsewhere in the hardening series.
Add dedicated tests asserting unauthorized and empty-balance rejections
leave the proposal's approval bitmap unmutated.
godamongstmen897 and others added 29 commits September 1, 2026 14:26
…set-yield-rate-guards

feat: harden caller auth and precondition guards in admin_set_yield_rate
Dropping the EmergencyPauseLock dance from
emergency_pause_admin_override is correct and is the point of Goldii-locks#399: the
lock exists to close a reentrancy window around external calls, and this
path makes none -- it reads the flag, compares, writes it back, and
emits an event. EpLk is still taken by the paths that do call out.

Three names had to be corrected against the enum as it actually exists:

- DataKey::EmergencyPaused -> DataKey::Ep in lib.rs
- DataKey::EmergencyPauseLock -> DataKey::EpLk in the new test suite

One test then failed on the event tally. It calls the override twice and
expects the count to go 1 then 2, but env.events().all() reports the most
recent contract invocation rather than a running total, so the second
call reports 1. Adjusted to assert the second call emits exactly one
event, with the existing payload check confirming it is the new one.
This is the same env behaviour that Goldii-locks#418 and Goldii-locks#438 ran into.

571 tests passing / WASM release build OK
…-pause-admin-override-storage-footprint

Goldii-locks#399 - fix(emergency_pause_admin_override): remove unnecessary EmergencyPauseLock overhead
The branch's own copy of admin_pause_escrow does not parse. A line from
the previous version was left dangling after the new writes:

    .set(&DataKey::EmergencyPauseLock, &true);
  env.storage().instance().set(&DataKey::Paused, &true);
      .set(&DataKey::EpLk, &true);   <- orphaned continuation

That produced 90 errors, all downstream of "expected expression, found
`.`". DataKey::EmergencyPauseLock is also not a variant -- the enum
calls it EpLk -- in lib.rs and twice in the new test suite.

Rebuilt the function around the two guards this PR is actually for:
assert_emergency_pause_not_locked before any write, and an early return
when the escrow is already paused so a redundant call mutates nothing.
Because that early return now handles the repeat case, the inner
`if !already_paused` that main used to gate the event is redundant, and
the publish is unconditional inside the lock.

Two of the new tests then failed on event tallies. They read
pause_event_count after is_paused / is_lock_held, and those helpers go
through env.as_contract -- env.events().all() reports the most recent
invocation, not a running total. Reordered so the tally is read first.
The idempotency case now asserts the second call emits nothing at all,
which is what "no-op" means here. Same env behaviour as Goldii-locks#418, Goldii-locks#428
and Goldii-locks#438.

576 tests passing / WASM release build OK
…349-admin-pause-escrow-guards

feat: harden caller authorization and precondition guards in admin_pause_escrow (closes Goldii-locks#349)
…om pf_alloc_admin_override

Resolve the merge against main, which had already landed a narrower
version of this event under the `pf_ovr` topic.

The branch declared a second `PlatformFeeAllocationOverrideEvent` struct
(same name, plus `contract_id` and `locked`) and published it a second
time under a `pfovrride` topic after the allocation lock was released --
two identically named types will not compile, and the two publishes are
the same event emitted twice.

Unified to one struct carrying the branch's richer payload and one
publish, kept inside the lock guard where main emits it. Retained main's
already-published `pf_ovr` topic rather than renaming a topic that is
live on the default branch. Updated main's existing assertion for the
two new fields and repointed the branch's four new tests at `pf_ovr`.

580 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(Goldii-locks#396): emit PlatformFeeAllocationOverrideEvent from pf_alloc_admin_override
…pf_alloc_admin_override

Resolve the merge against main. Three fixes were needed on top of the
branch:

* The branch predates Goldii-locks#434, so taking its rewritten
  pf_alloc_admin_override body verbatim would have silently dropped the
  PlatformFeeAllocationOverrideEvent that call now emits. Kept the
  single-write optimisation and the event.

* The branch deletes `mod admin_override_cancel_tests;` -- stale-branch
  damage that would drop a whole test module from the build. Kept ours.

* Its new test referenced `DataKey::EmergencyPauseLock`, which does not
  exist; the variant is the short key `EpLk`.

Also folded the branch's `setup_pf_alloc_escrow` helper together with
Goldii-locks#434's `setup_locked_pf_alloc` instead of landing two near-identical
copies.

591 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
 perf(Goldii-locks#397): reduce ledger storage footprint of pf_alloc_admin_override
…time extensions

Main had already migrated MilestoneTimeExtension writes to temporary
storage, so the branch's remaining substance is the read fallback --
`load_time_extension` now checks temporary first and falls back to the
persistent key. Without it, any escrow whose extension was written before
the migration reads back 0 and auto-releases on the unextended deadline.

Fixes on top of the branch:

* Both new i128-extreme tests wrote storage from outside a contract
  context, which panics in soroban-sdk 22 ("not accessible outside of a
  contract"). Wrapped the seeding writes in `env.as_contract`.

* The branch reorders `mod admin_override_cancel_tests;` above `mod test;`
  and drops the `#[cfg(test)]` attributes main added; kept ours.

593 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…etic-footprint

fix-checked-arithmetic-footprint
…min paths

Three footprint reductions, all consistent with the pattern main already
applies elsewhere:

* admin_tax_withholding_deductions no longer sets and immediately removes
  TaxWithholdingExecutionLock. Main had already hoisted every guard above
  the lock, leaving a set/remove pair that protected nothing.

* admin_override_tax_release no longer writes the MilestoneReleased
  temporary flag. Nothing reads it (is_milestone_released_flag is
  dead_code) and the persistent status already carries Released -- the
  same rationale main documents for two other override paths under Goldii-locks#383.

* multisig_admin_override_refund removes MultisigLocked instead of
  writing `false`, so no stale entry is left on the ledger.

Two fixes on top of the branch: it re-added guards main had already
hoisted (kept ours), and its new test read storage through
env.as_contract before checking the event tally -- env.events().all()
only reflects the most recent invocation, so the buffer was already
cleared. Reordered the assertions.

596 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…row is paused

admin_override_refund and multisig_admin_override_refund now reject their
illegal source state (paused / not multisig-locked) before touching any
job or milestone storage, so a rejected call writes nothing.

Resolved against main: unioned the test module lists, kept main's
crate-scoped `setup_funded_escrow`, and dropped the branch's duplicate
imports. Replaced the branch's inlined Paused read with the existing
`assert_not_paused` helper rather than adding a third hand-rolled copy.

600 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rden-admin-override-refund-guards

Harden caller authorization and precondition guards in admin_override…
…AdminOverrideEvent

EmergencyPauseAdminOverrideEvent gains a `previous` field, so an indexer
can see which transition an override actually performed rather than only
its result.

The branch predates Goldii-locks#428, which removed the re-entrancy guard from
emergency_pause_admin_override, so it reinstated the lock write and
published the event a second time. Kept main's single publish and added
`previous: current` to it.

Its new tests referenced `DataKey::EmergencyPaused` and
`DataKey::EmergencyPauseLock`; neither variant exists (the short keys are
`Ep` and `EpLk`). The lock assertion also expected `Some(false)` -- after
Goldii-locks#428 the guard key is never written at all, so it now asserts `None`.

603 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(Goldii-locks#398): structured event for emergency_pause_admin_override outcome
…#400)

The same author's Goldii-locks#432 branch already carried this work -- the
ArbitrationSplitAppliedEvent struct, its publish at the end of
apply_dispute_arbitration_split, and arbitration_split_event_tests.rs --
so it landed with that merge. The only remaining difference here was a
set of stale test snapshots; taking main's regenerated copies leaves the
tree identical to main.

Merging so the commits are recorded against this PR rather than closing
it unmerged.

603 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…i-locks#351, Goldii-locks#345, Goldii-locks#336, Goldii-locks#350)

Four entrypoints now reject unauthorised callers and illegal source
states before touching the ledger:

* transfer_admin rejects a zero address (InvalidAddress) and refuses to
  rotate the admin while a multisig proposal is pending
  (AdminTransferPending).
* emergency_unpause rejects NotPaused before taking the transition lock.
* admin_resume_escrow rejects NotPaused instead of silently succeeding,
  so a mistaken call no longer reads as a completed recovery.
* multisig_admin_override_release requires MultisigLocked to be active
  before any JobMeta or milestone read.

lib.rs merged cleanly (main's DataKey renames Ep/EpLk applied to the
branch's guards). test.rs needed hand-resolution: a three-way apply
anchored the four multisig_lock insertions onto unrelated
admin_override_release / admin_override_refund tests, which would have
rewritten those tests into copies of the multisig ones. Reverted and
placed each insertion on its intended test.

613 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…_escrow locks

cancel_escrow no longer lets one party freeze the escrow on its own. The
first signature records a bit in a CancelApproval mask and emits
CancelApprovalRecordedEvent; the CancelLock fires only when the
counter-party signs. revoke_cancel_approval lets a party withdraw its
signature before the lock.

Resolved against main:

* The branch deletes the public cancel_escrow_split_refund entrypoint,
  which is unrelated to this change. Kept it.
* It reverts admin_override_tax_release to the two-equality status check
  that Goldii-locks#411 replaced with an exhaustive match rejecting Disputed. Kept
  ours.
* The textual merge spliced main's enriched CancelEscrowInitiatedEvent
  fields into the branch's new CancelApprovalRecordedEvent (which does
  not declare them) while leaving the real CancelEscrowInitiatedEvent
  publish with only two. Swapped them back.
* Dropped a duplicate CancelLock read the branch added below the one
  main already performs.

Removing the standalone `admin.require_auth()` from
admin_override_tax_release is kept: require_admin already calls
require_auth, so it was a duplicated auth requirement.

Nineteen existing tests asserted the old one-signature lock -- they were
written after the branch diverged, so it could not have adapted them.
Updated each to supply the counter-party signature, and reinterpreted the
two duplicate-call tests: a repeat from the same party is now
InvalidStatus, and EscrowLocked applies once both have signed.

revoke_cancel_approval arrived with no tests (the branch carries snapshot
files for tests that were never committed). Added six covering the event
payload, mask clearing, revoke-then-reapprove, revoking after the lock,
revoking without a prior approval, and a non-party caller.

619 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ow-multi-party-auth

feat(cancel_escrow): enforce two-party authentication before lock fir…
…nsent for yield/time/streaming

Adds TimeExtExecutionLock and PaymentStreamingExecutionLock re-entrancy
guards across the settlement entrypoints, a time_extensions_consent
dual-signature path, and an interest_yield_split_refund allocation
entrypoint.

Resolved against main:

* Error discriminant 32 is already taken by main's EmptyBalance, so the
  branch's TimeExtInProgress / PaymentStreamingInProgress moved to 33 and
  34. Leaving them at 32/33 would have renumbered a live error code.
* The branch reintroduces DataKey::EmergencyPauseLock, which main
  shortened to EpLk; dropped its copy and kept its two genuinely new lock
  keys.
* Both sides added a helper in the same place -- kept main's
  assert_nonzero_balance alongside the branch's two lock assertions.
* test.rs is purely additive on the branch, so took main's file and
  reapplied the module declaration and the Goldii-locks#255 test block rather than
  resolving the interleaved hunks.

651 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-refunds

feat: locks, split-refund, and dual consent for yield/time/streaming
…split-refund fees, shorter time-extension key

Closes Goldii-locks#306, Goldii-locks#308, Goldii-locks#315, Goldii-locks#322.

* emergency_pause now takes the client and freelancer instead of the
  admin and requires both signatures (Goldii-locks#322). The admin's unilateral path
  remains emergency_pause_admin_override.
* validate_fee_allocation enforces MAX_TREASURY_FEE_BPS (2000) and
  MAX_CLIENT_FEE_BPS (5000) with a new FeeTooHigh error (Goldii-locks#306).
* split_refund_net_distribution applies the platform fee to the
  freelancer's payout only, leaving the client refund fee-exempt (Goldii-locks#308).
* DataKey::MilestoneTimeExtension becomes TimeExt and its value narrows
  from u64 to u32, matching the EpLk/Ep shortening already in the enum
  (Goldii-locks#315).

Resolved against main:

* The branch inserts FeeTooHigh at 29 and shifts AlreadyPaused, NotPaused
  and InvalidAllocationWeights up by one. Those codes are live and main
  has since added 32-34, so FeeTooHigh took 35 and every existing
  discriminant kept its value.
* Its time-extension read/write reverted main's move to temporary storage
  and Goldii-locks#426's persistent fallback. Kept both, under the new TimeExt key.
* split_refund_net_distribution called multisig_split_refund with four
  arguments; that function takes five and additionally requires the admin
  key and an active multisig lock. Repointed at cancel_escrow_split_refund,
  the pure allocator with the same shape.
* Moved the cap check below the sum check, so a malformed ratio still
  reports InvalidRatio rather than FeeTooHigh -- structural validity
  first, policy bound second.
* Updated thirteen emergency_pause call sites the branch could not see.
  Three of them (from Goldii-locks#378) used emergency_pause as the probe for "the
  admin key is unchanged"; that no longer tests admin authority, so they
  now probe emergency_unpause, which still distinguishes the rightful
  admin (NotPaused) from anyone else (Unauthorized).
* Two fee tests configured an even-thirds split, now above the treasury
  cap; moved to 40/40/20, which still floors every share to zero.

654 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…entication

feat: enhance escrow security and optimize state storage
…rithm (Goldii-locks#308)

Replaces main's earlier cancel_escrow_split_refund with this branch's
version, which is strictly better in three ways: it rejects a zero total
(main accepted it and returned a 0/0 allocation), it derives the
freelancer leg with an explicit checked_sub, and it publishes a
CancelSplitRefundCalculatedEvent so a cancellation split can be audited
without querying storage.

Resolved against main: the branch reverts admin_override_tax_release to
the two-equality status check that Goldii-locks#411 replaced with an exhaustive match
rejecting Disputed -- kept ours. Both definitions of the function
survived the merge textually; removed main's.

The branch ships seventeen test snapshots for tests that were never
committed, so the new behaviour arrived unverified. Wrote the seventeen
tests those snapshots name: the distribution pathways (equal, 70/30, full
client, full freelancer), the rounding cases (odd total, single stroop,
sub-basis-point client share, large prime total), the rejected inputs
(zero, negative, both shares zero, shares not summing to BPS_SCALE,
overflowing shares), the echoed basis points, the event and its payload,
and that the calculator answers on an uninitialised contract.

Every distribution case asserts client_refund + freelancer_payout ==
total_amount, which is the property the round-nearest-then-subtract
arithmetic exists to guarantee.

671 tests pass; wasm32 release build is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@godamongstmen897
godamongstmen897 merged commit 773f3fe into Goldii-locks:main Sep 1, 2026
1 check 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.

Add refund allocation algorithms in cancel_escrow