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-0006·business-logic

Zero or dust dispute base drains the fee pool at near-zero cost

Fixedescrowarbitrationdispute-resolution
TL;DR

Juror pay is floored at the tier fee while pool income scales with deal value, and zero-amount milestones are accepted. Disputes over zero or dust amounts drain the fee pool at essentially no cost to the attacker.

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

Description

Juror pay is floored while pool income scales linearly with deal value. _sqrtFee applies if (fee < floor) fee = floor; unconditionally, so the per-juror fee never falls below tierFeeCommunity (2,000 cNGN) no matter how small the dispute, while _poolShareForDeal accrues dealValue x 1.5% x 20% and reaches zero for any deal below roughly 334 cNGN. Below a break-even deal size every dispute is unprofitable for the pool; at a zero or dust dispute base the ratio is unbounded.

Zero-amount milestones are reachable. _pushMilestone accepts amount == 0, and initialize validates only that totalAmount > 0 and that the milestone amounts sum to no more than totalAmount. disputeBaseAmount() returns the disputed milestone's amount directly, so a zero-amount milestone yields a dispute base of zero. openCase accepts whatever that call returns and imposes no minimum.

The commission that funds the pool is pulled from commissionPayer — the protocol's own treasury — not from the party creating the deal, so a deal creator bears none of the cost of the case they open.

Vulnerable Scenario: The following steps illustrate the issue:

  1. An attacker controls two addresses and creates a deal between them with totalAmount = 1 base unit and milestoneAmounts = [0]. Because the sum is below totalAmount, a "Final Payment" milestone is auto-appended, giving milestones [0, 1].
  2. The buyer address deposits. Milestone 0 is auto-commenced at funding by _activateFunding.
  3. Pool accrual for the deal is 1 x 150 / 10_000 x 2000 / 10_000 = 0, and what accrual there is would come from the treasury rather than the attacker.
  4. After the milestone delivery window, the attacker calls raiseDispute. _currentDisputableMilestoneIndex returns 0 and disputedMilestoneIndex is set to 0.
  5. disputeBaseAmount() returns 0. openCase fires VRF with dealValue = 0; caseTier(0) is Community and _sqrtFee(0, 2_000e18, ...) computes zero, which is below the floor, so the fee is floored at 2,000 cNGN per juror.
  6. Tally pays up to 3 x 2,000 = 6,000 cNGN out of feePool for a dispute worth nothing.
  7. Because the attacker controls both parties, the disputed principal returns to them under any ruling. Each iteration costs gas and removes 6,000 cNGN from the pool.
03Section · Impact

Impact

The arbitration fee pool can be drained repeatedly at essentially no attacker cost, rate-limited only by juror availability — and because each case seats three jurors for a full case cycle, that limit is itself a denial of service against legitimate disputes.

The general form is broader than the zero case. Any small dispute is unprofitable because the fee floor binds across the whole Community band while income stays linear: a 1,000 cNGN deal contributes 3 cNGN and can cost 6,000. The zero-amount milestone makes it free and deliberate rather than merely uneconomic.

04Section · Recommendation

Recommendation

Reject zero-amount milestones at initialize, including the auto-appended remainder. That closes the free variant and costs nothing else. It does not close the class: at a one-wei milestone the square-root fee still falls below tierFeeCommunity and floors to the same per-juror amount, producing an identical payout.

The general case is a property of the funding model rather than of the code, and no threshold closes it. A dispute costs the pool at least PANEL_SIZE_ROUND1 x tierFeeCommunity while pool income is proportional to deal value, so a deal sized at exactly any threshold drains the same amount, and a party controlling both sides recovers the disputed principal under any ruling. A threshold enforced at openCase would additionally strand an escrow that has already entered Disputed, which under docs/AUDIT_GATE.md DD-1 has no other exit.

Closing it requires a decision that costs something the model currently promises: charging disputants breaks free round-1 arbitration, removing the fee floor breaks juror economics on small cases, and accepting the subsidy requires a rate limit the contracts do not have. Confirm which of those is acceptable before any code change is specified.

05Section · Resolution

Resolution

Fixed. Zero-amount milestones are rejected at creation, so the dust-dispute path is unreachable.

06Section · Affected files

Affected files

  • contracts/Escrow.sol#L214-L230
  • contracts/Escrow.sol#L974-L983
  • contracts/lib/ArbitrationPoolLib.sol#L118-L134
  • contracts/lib/ArbitrationPoolLib.sol#L140-L150
  • contracts/ArbitrationPool.sol#L419-L430
  • contracts/ArbitrationPool.sol#L545-L569
Status
Fixed
F-2026-0006