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-0007·missing-validation

Tier gating is not enforced at openCase, so an unseatable dispute locks funds indefinitely

Fixedescrowarbitrationdispute-resolution
TL;DR

Neither the deal-size cap nor the tier rule for opening cases is enforced on-chain. A dispute whose value exceeds what staked jurors can serve can be opened anyway, and its principal is then locked indefinitely waiting for a panel that cannot be seated.

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

Description

Launch solvency depends on an operational rule: cap self-serve pool-eligible deals at 500,000 cNGN and seat Community jurors only, and do not open a case where the dispute base exceeds that band unless Verified or Expert jurors are staked, because selection would otherwise return no eligible panel.

Neither half of the rule is expressible in the contracts as written. openCase is external whenNotPaused with no caller restriction, so any address may open a case for any disputed escrow whose arbitrator is the pool. EscrowFactory enforces no maximum deal value, so the deal-size cap is equally unenforced at creation.

Vulnerable Scenario: The following steps illustrate the issue:

  1. At launch only Community jurors are staked.
  2. A 2,000,000 cNGN deal is created and disputed. disputeBaseAmount() returns 2,000,000 cNGN, which sits in the Verified band above MAX_DEAL_COMMUNITY.
  3. Either party calls openCase directly. Nothing blocks the call.
  4. _selectJurors filters candidates through jurorEligibleForCase(tier, 2_000_000e18, false). jurorMeetsDealTier(0, ...) is false for every Community juror, so the eligible set is empty.
  5. rawFulfillRandomWords re-queues the pending case and emits InsufficientEligibleJurors.
  6. retryStuckCase can be called indefinitely and fails identically while no Verified juror exists. Per docs/AUDIT_GATE.md DD-1 there is no other exit from Disputed, so the escrow stays locked.
03Section · Impact

Impact

The escrow's principal is locked indefinitely rather than failing visibly, and the state is indistinguishable from the empty-pool condition that docs/AUDIT_GATE.md accepts as expected behaviour, so it does not surface as an incident.

Any user reaching the contract directly, any backend defect, and any change of operational policy produces the same outcome.

04Section · Recommendation

Recommendation

Enforce the deal-size half of the rule where deals are created. Add a governance-settable maximum deal value to EscrowFactory._createEscrow, set at launch to MAX_DEAL_COMMUNITY and raised as Verified and Expert jurors are seated, so a deal the pool cannot service is never created rather than being discovered at dispute time.

Reverting in openCase when no staked juror satisfies jurorEligibleForCase is worth adding alongside it, but as a visibility and cost control rather than a remedy: the escrow reaches Disputed before the pool is involved, so a revert there does not release the principal. What it does prevent is each retryStuckCase consuming another LINK-funded VRF request against a case that cannot seat.

Neither change eliminates the condition. A deal created within the cap becomes unseatable if the jurors covering its band later unstake, and while DD-1 provides no exit from Disputed an unseatable dispute remains permanently locked. If that residual is unacceptable, DD-1 is the decision that has to be revisited.

05Section · Resolution

Resolution

Fixed. A deal above the seatable band is refused at creation, and openCase additionally refuses a case for which no eligible juror exists — so the unseatable dispute can no longer be created. Parties retain the mutual-settlement exit.

06Section · Affected files

Affected files

  • contracts/ArbitrationPool.sol#L545-L569
  • contracts/lib/ArbitrationPoolSelectLib.sol#L147-L177
  • contracts/EscrowFactory.sol#L125-L146
Status
Fixed
F-2026-0007