Skip to content

fix(bots): keeper skips drawing when phase already advanced to drawing before minStakingTime elapses #2574

Description

@salgozino

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

  1. 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).
  2. A fresh keeper run starts with elapsed < minStakingTime since lastPhaseChange (i.e. since entering drawing).
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions