Zero or dust dispute base drains the fee pool at near-zero cost
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.
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:
- An attacker controls two addresses and creates a deal between them with
totalAmount= 1 base unit andmilestoneAmounts = [0]. Because the sum is belowtotalAmount, a "Final Payment" milestone is auto-appended, giving milestones[0, 1]. - The buyer address deposits. Milestone 0 is auto-commenced at funding by
_activateFunding. - 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. - After the milestone delivery window, the attacker calls
raiseDispute._currentDisputableMilestoneIndexreturns 0 anddisputedMilestoneIndexis set to 0. disputeBaseAmount()returns 0.openCasefires VRF withdealValue = 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.- Tally pays up to 3 x 2,000 = 6,000 cNGN out of
feePoolfor a dispute worth nothing. - 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.
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.
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.
Resolution
Fixed. Zero-amount milestones are rejected at creation, so the dust-dispute path is unreachable.
Affected files
contracts/Escrow.sol#L214-L230contracts/Escrow.sol#L974-L983contracts/lib/ArbitrationPoolLib.sol#L118-L134contracts/lib/ArbitrationPoolLib.sol#L140-L150contracts/ArbitrationPool.sol#L419-L430contracts/ArbitrationPool.sol#L545-L569