Skip to content

Rating System Hardening Audit — full-target red team (KR + international)

Status: Active · Last reviewed: 2026-08-12

Owner-commissioned adversarial audit (2026-08-12) of the rating system's TARGET STATE (current engine + S7 interop + rd-inflation/set-scoring/governance/decay additions), attacked by three independent red teams: algorithm/statistics, manipulation/abuse, sociotechnical/compliance/product. All findings code-verified against live migrations (00458/00461/00463/00432/00431/00130/00085) and components. Companion to rating-interop-model.md.

Fixed immediately (migration 00467, live)

  • get_home_club broken by 00461's PK widening — ranked per (club, discipline) row instead of per club; a 24-match home club split across disciplines lost to a 15-match single-row club → matches.is_cross_pool stamped wrong → wrong confidence increments. Now aggregates SUM(elo_matches_played) per club.
  • Confidence bonus bypassed the peak floor — the ±10% bonus applied as a bare rating + delta, able to push a loser below round(peak*0.80), breaching the engine's one universal invariant. Now clamps the applied delta per row and records the clamped value in elo_history.

Launch-blocking for KR (owner sign-off then implement)

  1. Host-only outcome stamping is a permanent loss-suppression channel (abuse C1). A playing host can stamp any losing match abandoned/walkover/non_competitive → zero rating impact, real score still displayed, and NO write path can ever restore the outcome (score corrections preserve outcome by design). Fix: non-host co-sign for no-rating outcomes (mirrors the two-sided score pattern), plus a signal on every no-rating outcome for auditability.
  2. Unrated accounts skip every tier gate (abuse C2). rsvps_enforce_tier skips unrated/self_assessed — a fresh (or throwaway) account's first RSVP into any tier-restricted session is never checked. Fix: gate unrated accounts on their self-declared seed instead of skipping; phone-verify friction before first tier-gated RSVP until 본인인증 ships.
  3. Loss-recording avoidance + host-as-referee (socio F2/F21). Face-sensitive losses can quietly go unrecorded or be reclassified friendly post-hoc; the score-correction proposer is the host who may be a participant. Fix: friendly is a session-level declaration BEFORE play, not post-hoc; disputed + cross-pool matches need multi-party confirmation; per-member played-vs-scored ratio surfaced to club admin.
  4. Singles/mixed bands shown without projection labels (socio F3 — live invariant violation). TierInfoSheet prints singles bands for players with zero singles matches, violating the config's own "projections must be labeled" rule. One-file fix + 참고용 styling.
  5. Demotion shield (socio F13). No grace period: one bad loss after promotion = instant visible demotion — worst-case interaction with 체면 dynamics. Fix: hold display_tier for 3–5 matches after promotion (LoL-precedented, display-layer only).

High-value engine corrections (S7 rides)

  • Doubles credit assignment (algo C1): both partners get the same delta magnitude (K-scaled only) — the weaker partner of a carry gains MORE per win than the carrier, compounding forever. Fix: weight per-partner delta by contribution to team-average.
  • Sandbag window (algo C3 + abuse M4): STRIKE_K only fires matches 5–19 vs the player's OWN seed — a disciplined day-one sandbagger never triggers it. Fix: re-arm strikes past match 20 on large delta streaks; base detection on opponent-pool-implied skill, never self-report.
  • Margin gaming is live today (algo M1 + abuse M3): score_factor (1.5×/1.2×) already imports UTR's run-up-the-score incentive and softens deliberate narrow losses. Fix: apply the multiplier only when team-effective gap is small (blowouts vs much-weaker opponents are expected, uninformative).
  • Mixed cold-start (algo M4): every first mixed row starts at hard 1000 regardless of doubles/singles skill. Fix: seed new discipline rows from a doubles-weighted cross-discipline prior, labeled as seed.
  • rd is decorative + name collision (algo M3): rd never grows, never enters K/expected-score; rating_confidence is an unrelated trust scalar sharing the "confidence" name. Fix: rename rating_confidenceverification_trust BEFORE any rd-inflation work; wire rd properly only via the held Glicko/OpenSkill port (don't half-wire).
  • Display fallback lies (algo C2): established singles-only player shows a default-1000 doubles "Silver" badge. Fix: fall back to the singles tier (labeled projection) or a distinct no-doubles-data state.
  • Calibration layer (algo M5/M6): blend prior→posterior by min(n/30,1) instead of a hard cutover; enforce monotonicity on ordinal fits; anchors for fitting must be api_verified only (abuse M2 — Sybil screenshot rings clear a flat count threshold).
  • Pump-club detection (abuse C3): global ratings + self-owned clubs + self-service verification = collusion pump with zero cross-club signal. Fix: weekly offline job flagging closed-clique match graphs / outlier velocity → moderation queue (detection, not gates).
  • Decay policy recommendation: uncertainty-inflation only (Glicko-style), never LK-style rating decay — KR outdoor-season winter breaks must not cost rating points.

Compliance deltas (pre-expansion, some pre-KR)

  • GDPR Art. 22 adjacency (F7): tier gates are fully automated denial of session entry — EU launch needs a visible human override path (host authority extension) + block-reason transparency.
  • Age gate is hardcoded 14 (F9): PIPA-correct, GDPR-wrong for DE/FR/NL (16/15) — must become a per-market config value; cheap now (clean-break window), expensive later.
  • Erasure vs evidence (F8): S7's evidence JSONB must never store raw screenshots (third-party PII) — numeric extraction only; anonymization cron column list must explicitly cover elite_background*.
  • Consent memo (F10): name the "contract necessity" basis for rating computation explicitly before EU.
  • Leaderboard as market feature flag (F18): public skill display is a per-market product decision (JP's soft-label culture), not a formatting swap.

Cheap high-leverage product wins

  1. Demotion shield (above). 2. Surface eloToNextTier() — the function exists, unused; turns band-edge anxiety into progress motivation. 3. Leaderboard "you + nearby" default view (small-club bottom-of-list shame). 4. ReliabilityPill on every leaderboard row (women's-pool volatility exposure, cold-start honesty). 5. Instrument demotion→attendance correlation (F17) before further tier polish — session attendance is the metric that matters.

What survived the attack unscathed

Three-track independence (consequences need handling, principle held) · hidden-precise/public-coarse display structure · hub-and-spoke interop with never-fuse · the deletion/anonymization pipeline architecture (soft-delete + scrub preserves pool integrity through erasure) · demotion copy ("조정됐어요") · US onboarding fit of seed self-rate.

Markdown remains the source of truth. Run yarn docs:check before handoff.