Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0030·logic-error

Terminal status (Completed/Refunded) reflects only the last dispute episode on milestone deals, not the deal's actual outcome

Fixedescrowarbitrationdispute-resolution
TL;DR

On milestone deals the terminal Completed/Refunded status is derived from whichever milestone settled last, not from what each party actually received across the deal, so the final label can misrepresent the outcome.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
LOW
02Section · Description

Description

When a milestone-scoped dispute resolves and happens to be the last unsettled milestone, the terminal status is derived from only that milestone's buyerShareBps:

solidity
if (_allMilestonesSettled()) {
status = buyerShareBps == 10_000 ? Status.Refunded : Status.Completed;
}

buyerShareBps is decided fresh on every dispute and can differ per milestone, and milestones released without a dispute never reach this branch. The final label is therefore whichever outcome settled last, not an aggregate of what each party actually received.

Example: M0 releases normally and pays the seller in full; M1 is disputed and the buyer wins outright; the deal is permanently marked Refunded, though the seller was paid for M0. The reverse holds for a seller-favourable last ruling. docs/PROTOCOL_SPEC.md §6.2.2 documents these values as whole-deal claims — "Seller paid", "Buyer refunded" — which the milestone-scoped path cannot honour.

No arithmetic is wrong and no on-chain gate branches on the distinction; every check tests != Active, != Pending or == Disputed. The cost falls on explorers, indexers and any integration reading status as the deal's outcome, which is the meaning the specification promises it carries.

03Section · Recommendation

Recommendation

Do not collapse a multi-episode milestone outcome into a binary label from the last episode's split. Either track cumulative buyer and seller payouts and set Refunded only when the seller's cumulative share is zero (Completed when the buyer's is), introducing a Settled state for mixed outcomes — or, at minimum, correct §6.2.2 to state that these values reflect only the terms of the milestone that closed the deal.

04Section · Resolution

Resolution

Fixed. Terminal status now derives from cumulative payouts across the whole deal, with a new Settled state for mixed outcomes.

05Section · Affected files

Affected files

  • contracts/Escrow.sol#L859-L892 — _resolveDisputeFunds, milestone-scoped branch (line 887)
  • contracts/Escrow.sol#L927-L965 — resolveDisputeByMutualAgreement, resolveDisputeByArbitrator, resolveDisputeByArbitratorWithSplit
Status
Fixed
F-2026-0030