Protocol constants assume 18-decimal units but the settlement token is 6-decimal, locking every disputed deal
Every stake minimum, deal band and fee constant is written for an 18-decimal token, but the production settlement token cNGN has 6 decimals. No juror can meet a stake minimum, so every disputed deal's principal is locked with no revert or event to signal it.
Description
The production settlement token is cNGN on Polygon (0x52828daa48C1a9A06F37500882b42daf0bE04C3B), which exposes 6 decimals. Every stake minimum, deal-value band, and fee constant in the protocol is written as an 18-decimal literal:
MIN_STAKE_COMMUNITY = 25_000e18; MAX_DEAL_COMMUNITY = 500_000e18;MIN_STAKE_VERIFIED = 200_000e18; MAX_DEAL_VERIFIED = 5_000_000e18;MIN_STAKE_EXPERT = 500_000e18; COMMERCIAL_EXPERT_JUROR_GATE = 20_000_000e18;tierFeeCommunity = 2_000e18; tierFeeExpertCommercial = 35_000e18;
docs/PROTOCOL_SPEC.md §6.3.2 records the assumption these were written under: "deal values use token base units (18 decimals)." Against a 6-decimal token each is overstated by a factor of 10^12, and nothing in the deployment path detects it — EscrowFactory.addStablecoin accepts any token address without reading decimals().
Vulnerable Scenario: The following steps illustrate the issue:
ArbitrationPoolis deployed with the 6-decimal cNGN as_cNGN.- A prospective juror calls
stake(0, amount).minStakeForTier(0)requires25_000e18base units — 25 quadrillion actual cNGN. Every realistic amount revertsBelowMinStake, sojurorRegistrystays permanently empty. - A buyer funds a deal, delivery fails, and the buyer calls
raiseDispute. The escrow entersDisputed. openCaserequests VRF._selectJurorsfinds no eligible juror and returns an empty panel, sorawFulfillRandomWordsre-queues the pending case and emitsInsufficientEligibleJurors.retryStuckCaserepeats step 4 indefinitely. Perdocs/AUDIT_GATE.mdDD-1 there is no time-based or unilateral exit fromDisputed, so the buyer's principal stays locked.
Tier routing and fee sizing break independently of the empty registry: caseTier classifies every deal as Community because MAX_DEAL_COMMUNITY is effectively 500 quadrillion cNGN, COMMERCIAL_EXPERT_JUROR_GATE becomes unreachable, _sqrtFee always returns its floor, and the withdrawFeePool solvency floor at ArbitrationPool.sol#L444 is overstated by the same factor.
Impact
Disputed principal is permanently locked for every deal that reaches arbitration, and the failure is silent — no revert or event distinguishes it from the empty-pool state that docs/AUDIT_GATE.md accepts as normal operation.
The test suite cannot surface it. contracts/MockUSDT.sol#L9 declares 18 decimals, and test/helpers.js already mixes units — deal amounts at parseUnits("1000", 6) against stakes and fees at parseUnits("25000", 18) — so the mismatched assumption is built into the fixtures.
Recommendation
decimals() on the deployed token was read as 6 against three independent Polygon RPC endpoints on 2026-08-06.
Express every threshold, minimum, cap and fee in the settlement token's own unit rather than as hardcoded e18 literals, and assert IERC20Metadata(_cNGN).decimals() against the value they were compiled for in the ArbitrationPool constructor, so a mismatched deployment reverts instead of succeeding silently. Deriving the unit at runtime is not available here: the constants live in ArbitrationPoolLib and are consumed by ArbitrationPoolSelectLib and ArbitrationPoolTallyLib, which are deployed and linked as external libraries and cannot read a pool immutable.
Apply the same decimals check in EscrowFactory.addStablecoin and updateStablecoin. Without it a second whitelisted currency can be denominated in a unit the pool's bands were never compiled for, and every deal in that currency routes against the wrong tier.
The √d coefficients at ArbitrationPoolLib.sol#L67-L69 must be re-derived against the new base unit rather than rescaled alongside the thresholds, because _sqrtFee scales with sqrt(unit) and not with unit. Change contracts/MockUSDT.sol to 6 decimals and make test/helpers.js use one consistent unit so that a recurrence fails the suite.
Resolution
Fixed. The pool constructor and the factory both reject a settlement token whose decimals() is not 6. Re-running the original test against the fixed code, a juror onboards normally with the 6-decimal token and a full panel seats on a ₦200,000 deal; the 18-decimal control can no longer be deployed at all.
Affected files
contracts/lib/ArbitrationPoolLib.sol#L16-L25contracts/lib/ArbitrationPoolLib.sol#L67-L69contracts/lib/ArbitrationPoolLib.sol#L140-L150contracts/ArbitrationPool.sol#L184-L188contracts/ArbitrationPool.sol#L442-L450contracts/EscrowFactory.sol#L320-L357