Problem
keeperBot.ts's drawing workflow uses phase-agnostic time gates. Both hasMinStakingTimePassed() and hasMaxDrawingTimePassed() (contracts/scripts/keeperBot.ts:634-648) read sortition.lastPhaseChange() regardless of the court's current phase, and the outer gate at keeperBot.ts:711 applies the minStakingTime predicate to the entire drawing workflow instead of only to the staking -> generating transition.
In non-devnet deployments minStakingTime == maxDrawingTime == 1800s (contracts/deploy/00-home-chain-arbitration-mainnet.ts:64-65, 00-home-chain-arbitration.ts:69-70).
Reproduction / Trigger
- The court is in
drawing phase with disputesWithoutJurors > 0 (phase can be advanced externally via the permissionless passPhase() — e.g. the UI's PassPhaseButton, or by a previous keeper run that crashed mid-cycle; the keeper's own PM2 job is one-shot with a 10-minute restart_delay).
- A fresh keeper run starts with
elapsed < minStakingTime since lastPhaseChange (i.e. since entering drawing).
hasMinStakingTimePassed() evaluates false (it doesn't check current phase), so the entire drawing block (keeperBot.ts:716-756) is skipped — no draw() calls happen even though the court is in drawing and disputes need jurors.
- The unconditional "back to staking" cleanup (
keeperBot.ts:761-763) then either reverts (contract-side >= check not yet satisfied) leaving the court stranded in drawing, or — once elapsed >= maxDrawingTime — succeeds and pushes the phase back to staking with zero draws having happened, because the keeper's own predicate uses strict > while SortitionModule.sol:206,216 uses >=.
Expected Behavior
When the court is already in drawing phase and disputes still need jurors, the keeper should enter the drawing loop directly, without requiring minStakingTime to have elapsed since the drawing-phase entry (that predicate should only gate leaving staking).
Actual Behavior
The keeper can silently skip an entire drawing window with zero draw() calls, and its own cleanup step can even push the phase back to staking early relative to the contract's own >= semantics, leaving disputesWithoutJurors > 0 unresolved until a later cycle (or indefinitely if the pattern repeats).
Impact
Operational/off-chain (Medium): KlerosCore.draw() remains permissionless on-chain, so an independent caller can still fill the panel — this is not an on-chain DoS. But if the shipped keeper is the only drawer running, juror draws can be delayed indefinitely for a dispute, affecting arbitration liveness.
Fix
Tracked in PR (this issue is opened alongside the fix): restructure the keeper's phase-entry logic to be phase-aware — apply minStakingTime only to the staking -> generating transition, enter the drawing block unconditionally when the court is already in drawing, and align the keeper's boundary comparisons (> -> >=) with the contract's own semantics.
Problem
keeperBot.ts's drawing workflow uses phase-agnostic time gates. BothhasMinStakingTimePassed()andhasMaxDrawingTimePassed()(contracts/scripts/keeperBot.ts:634-648) readsortition.lastPhaseChange()regardless of the court's current phase, and the outer gate atkeeperBot.ts:711applies theminStakingTimepredicate to the entire drawing workflow instead of only to thestaking -> generatingtransition.In non-devnet deployments
minStakingTime == maxDrawingTime == 1800s(contracts/deploy/00-home-chain-arbitration-mainnet.ts:64-65,00-home-chain-arbitration.ts:69-70).Reproduction / Trigger
drawingphase withdisputesWithoutJurors > 0(phase can be advanced externally via the permissionlesspassPhase()— e.g. the UI'sPassPhaseButton, or by a previous keeper run that crashed mid-cycle; the keeper's own PM2 job is one-shot with a 10-minuterestart_delay).elapsed < minStakingTimesincelastPhaseChange(i.e. since enteringdrawing).hasMinStakingTimePassed()evaluatesfalse(it doesn't check current phase), so the entire drawing block (keeperBot.ts:716-756) is skipped — nodraw()calls happen even though the court is indrawingand disputes need jurors.keeperBot.ts:761-763) then either reverts (contract-side>=check not yet satisfied) leaving the court stranded indrawing, or — onceelapsed >= maxDrawingTime— succeeds and pushes the phase back tostakingwith zero draws having happened, because the keeper's own predicate uses strict>whileSortitionModule.sol:206,216uses>=.Expected Behavior
When the court is already in
drawingphase and disputes still need jurors, the keeper should enter the drawing loop directly, without requiringminStakingTimeto have elapsed since the drawing-phase entry (that predicate should only gate leavingstaking).Actual Behavior
The keeper can silently skip an entire drawing window with zero
draw()calls, and its own cleanup step can even push the phase back tostakingearly relative to the contract's own>=semantics, leavingdisputesWithoutJurors > 0unresolved until a later cycle (or indefinitely if the pattern repeats).Impact
Operational/off-chain (Medium):
KlerosCore.draw()remains permissionless on-chain, so an independent caller can still fill the panel — this is not an on-chain DoS. But if the shipped keeper is the only drawer running, juror draws can be delayed indefinitely for a dispute, affecting arbitration liveness.Fix
Tracked in PR (this issue is opened alongside the fix): restructure the keeper's phase-entry logic to be phase-aware — apply
minStakingTimeonly to thestaking -> generatingtransition, enter the drawing block unconditionally when the court is already indrawing, and align the keeper's boundary comparisons (>->>=) with the contract's own semantics.