Back to skill

Security audit

Polymarket Ladder Esports Kills Trader

Security checks for vulnerabilities and agentic risk

Overview

This skill is openly a Polymarket trading bot, but several documented financial safeguards are not reliably enforced before live trades.

Review this carefully before installing for live trading. Use paper mode first, pin and review the SDK version, use a limited Simmer/Polymarket credential, and do not run with --live unless you accept that the current safeguards may allow trades in low-liquidity, misgrouped, or stale-context markets.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (5)

T08 · Insecure Dependencies

Error
Location
clawhub.json:3
Finding

Unpinned Trading SDK Creates a Credential and Supply-Chain Risk

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
trader.py:290
Finding

Configured Minimum Market Volume Safeguard Is Not Enforced

Content
View full analysis
tuple[bool, str]: """Standard spread and time-to-resolution checks.""" p = getattr(market, "current_probability", None) if not isinstance(p, (int, float)): return False, "missing probability" spread_cents = getattr(market, "spread_cents", None) if isinstance(spread_cents, (int, float)) and spread_cents / 100 > MAX_SPREAD: return False, f"Spread {spread_cents/100:.1%} > {MAX_SPREAD:.1%}" resolves_at = getattr(market, "resolves_at", None) if resolves_at: try: resolves = datetime.fromisoformat(resolves_at.replace("Z", "+00:00")) days = (resolves - datetime.now(timezone.utc)).days if days < MIN_DAYS: return False, f"Only {days} days to resolve" except Exception: pass return True, "ok" ``` `MIN_VOLUME` is loaded at `trader.py:32`: ```python MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "5000")) ``` However, `valid_market()` never retrieves or compares market volume against this value. ### Technical Analysis The Skill declares `SIMMER_MIN_VOLUME` as a risk parameter and documents it as a minimum market-volume filter. The execution path does not enforce that control. A market is considered valid based only on probability availability, spread when present, and resolution time when parseable. Low-volume markets are easier to manipulate and may have inadequate depth even when a quoted spread appears acceptable. A transient or attacker-induced price discrepancy can therefore be interpreted as a ladder violation and used to trigger a live order. The issue does not grant operating-system privileges. It bypasses an intended financial safety boundary and exposes the trading authority associated wit ...[truncated 982 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:426
Finding

Maximum Concurrent Position Limit Resets on Every Invocation

Content
View full analysis
= MAX_POSITIONS: break market = opp[0] side, size, reasoning = compute_signal(market, opp) if not side: safe_print(f" [skip] {reasoning}") continue ok, why = context_ok(client, market_id) if not ok: safe_print(f" [skip] {why}") continue try: r = client.trade( market_id=market_id, side=side, amount=size, source=TRADE_SOURCE, skill_slug=SKILL_SLUG, reasoning=reasoning, ) tag = "(sim)" if r.simulated else "(live)" status = "OK" if r.success else f"FAIL:{r.error}" safe_print(f" [trade] {side.upper()} ${size} {tag} {status} -- {reasoning[:110]}") if r.success: placed += 1 except Exception as e: safe_print(f" [error] {market_id}: {e}") safe_print(f"[ladder-esports-kills] done. {placed} orders placed.") ``` ### Technical Analysis `MAX_POSITIONS` is documented as the maximum number of concurrent open positions, but the code only counts orders successfully submitted during the current process invocation. It never queries existing open positions or outstanding orders. The local `placed` counter starts at zero every time `run()` executes. As a result, repeated manual invocations, overlapping executions, or future scheduling can each submit up to `MAX_POSITIONS` additional orders. This violates the intended aggregate exposure limit. The code also does not reserve capacity atomically. Even if an initial pos ...[truncated 1063 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:331
Finding

Slippage and Flip-Flop Safety Checks Fail Open on Errors

Content
View full analysis
tuple[bool, str]: """Check flip-flop and slippage safeguards.""" try: ctx = client.get_market_context(market_id) if not ctx: return True, "no context" if ctx.get("discipline", {}).get("is_flip_flop"): reason = ctx["discipline"].get("flip_flop_reason", "recent reversal") return False, f"Flip-flop: {reason}" slip = ctx.get("slippage", {}) if isinstance(slip, dict) and slip.get("slippage_pct", 0) > 0.15: return False, f"Slippage {slip['slippage_pct']:.1%}" for w in ctx.get("warnings", []): safe_print(f" [warn] {w}") except Exception as e: safe_print(f" [ctx] {market_id}: {e}") return True, "ok" ``` ### Technical Analysis The function is intended to reject recent directional reversals and excessive slippage. It instead approves trading in two unsafe states: - `get_market_context()` returns no context. - Context retrieval or parsing raises any exception. The broad exception handler logs the error and then returns `(True, "ok")`. This converts network failures, SDK failures, malformed responses, and unexpected data types into authorization to continue trading. Because `context_ok()` is called immediately before `client.trade()`, failure of the safety-data service removes the intended control precisely when its result is unavailable. This is a fail-open design inappropriate for real-money execution. ### Attack Path 1. A candidate market passes signal and basic validation. 2. The context API becomes unavailable, times out, or returns malformed data. An attacker capable of interfering with that response could induce the same condition. 3. `get_market_context()` or subsequent pars ...[truncated 743 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:164
Finding

Unidentified Markets Can Be Combined into False Trading Ladders

Content
View full analysis
str: """Extract a normalized match identifier from the question. Falls back to a generic key when the question contains no team/event prefix (e.g. "Total Kills Over/Under 52.5 in Game 1?" has no teams). In that case we try the market's import_source or id to distinguish different matches, or default to 'unknown_match'. """ m = _MATCH_PREFIX.search(question) if m: raw = m.group(1).strip() raw = _GAME_NUM.sub("", raw) key = re.sub(r"\s+", " ", raw.lower()).strip(" -|:,") if len(key) >= 3: return key # Fallback: use market metadata to group by parent event if market is not None: for attr in ("event_slug", "group_slug", "import_source", "event_id"): val = getattr(market, attr, None) if val and isinstance(val, str) and len(val) >= 3: return val.lower().strip() # Last resort: group all kills markets without prefix together return "unknown_match" ``` The returned value is used for grouping at `trader.py:242-258`: ```python threshold = parse_kill_threshold(q) match_key = parse_match_key(q, market=m) if threshold is None: continue game_number = parse_game_number(q) key = f"{match_key}|game{game_number}" point = LadderPoint(m, match_key, game_number, threshold, float(p)) ladders.setdefault(key, []).append(point) ``` ### Technical Analysis A probability ladder is valid only when all points refer to the same event and game. The final fallback returns the same identifier, `unknown_match`, for every market that lacks a recognized question prefix and usable event metadata. `build_ladders()` then combi ...[truncated 1584 chars]
Remediation
View remediation
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill references environment-based credentials (SIMMER_API_KEY) and describes live trading capability, but it does not declare an explicit tool/permission scope such as permissions or allowed-tools. In an agent platform, missing scope boundaries can let the runtime expose broader environment access than necessary, increasing the chance of credential leakage or misuse if the skill is invoked in an unsafe context.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · trader.py (reported line 167)May include surrounding context.

python
# Fallback: use market metadata to group by parent event
    if market is not None:
        for attr in ("event_slug", "group_slug", "import_source", "event_id"):
            val = getattr(market, attr, None)
            if val and isinstance(val, str) and len(val) >= 3:
                return val.lower().strip()

Static analysis

No suspicious patterns detected.