Back to skill

Security audit

Polymarket Social Trends Trader

Security checks for vulnerabilities and agentic risk

Overview

This skill openly performs trading, but several advertised financial safety limits are unreliable and should be reviewed before use.

Install only if you understand that this can place live financial trades when run with live mode and that several documented safeguards are not reliably enforced. Use a limited, revocable credential, keep live trading disabled until the risk-control bugs are fixed, and require a pinned or otherwise verified simmer-sdk dependency before trusting it with real funds.

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:6
Finding

Unpinned privileged third-party trading dependency

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:316
Finding

Configured minimum market volume is not enforced

Content
View full analysis
None: mode = "LIVE" if live else "PAPER (sim)" year = datetime.now(timezone.utc).year month = datetime.now(timezone.utc).month is_election_gridlock = (year % 2 == 0 and month >= 8) print(f"[polymarket-social-trends-trader] mode={mode} max_pos=${MAX_POSITION} min_vol=${MIN_VOLUME} max_spread={MAX_SPREAD:.0%} election_gridlock={is_election_gridlock}") client = get_client(live=live) markets = find_markets(client) print(f"[polymarket-social-trends-trader] {len(markets)} candidate markets") placed = 0 for m in markets: if placed >= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: print(f" [skip] {why}") continue try: r = client.trade( market_id=m.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}" print(f" [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:70]}") if r.success: placed += 1 except Exception as e: print(f" [error] {m.id}: {e}") print(f"[polymarket-social-trends-trader] done. ...[truncated 1225 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:257
Finding

Minimum-trade floor can exceed the configured maximum position

Content
View full analysis
= NO_THRESHOLD: conviction = min(1.0, (p - NO_THRESHOLD) / (1 - NO_THRESHOLD) * bias) size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2)) edge = p - NO_THRESHOLD return "no", size, f"NO YES={p:.0%} edge={edge:.0%} bias={bias:.2f}x size=${size} — {q[:65]}" ``` The maximum-position configuration explicitly permits values below the default minimum trade: ```json { "env": "SIMMER_MAX_POSITION", "type": "number", "default": 25, "range": [ 1, 200 ], "step": 1, "label": "Max position size (USD)" } ``` ### Technical Analysis Both order-size branches use: ```python max(MIN_TRADE, calculated_size) ``` No subsequent operation caps the result at `MAX_POSITION`. Therefore, if `MIN_TRADE` is greater than `MAX_POSITION`, the minimum floor overrides the supposed maximum. With the permitted `MAX_POSITION=1` configuration and default `MIN_TRADE=5`, every qualifying signal produces at least a $5 order, exceeding the explicit $1 limit. This contradicts the documentation that order size is capped at `MAX_POSITION`. ### Attack Path 1. A user or managed configuration sets `SIMMER_MAX_POSITION` to a value below `SIMMER_MIN_TRADE`; for example, `$1` versus the default `$5`. 2. A market meets either the YES or NO threshold. 3. Conviction-based sizing calculates an amount no greater than the configured maximum. 4. `max(MIN_TRADE, ...[truncated 374 chars]
Remediation
View remediation
MAX_POSITION`. - Apply an explicit final cap: ```python calculated = round(conviction * MAX_POSITION, 2) size = min(MAX_POSITION, max(MIN_TRADE, calculated)) ``` - If the venue minimum exceeds the configured maximum, skip the trade rather than silently overriding the maximum. - Revalidate the final amount immediately before calling `client.trade()`. - Declare `SIMMER_MIN_TRADE` in managed metadata if users are expected to configure it. - Add unit tests for equal limits, inverted limits, boundary probabilities, and extreme environment-variable values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:324
Finding

Maximum concurrent-position limit ignores existing positions

Content
View full analysis
= MAX_POSITIONS: break side, size, reasoning = compute_signal(m) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, m.id) if not ok: print(f" [skip] {why}") continue try: r = client.trade( market_id=m.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}" print(f" [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:70]}") if r.success: placed += 1 except Exception as e: print(f" [error] {m.id}: {e}") ``` ### Technical Analysis `MAX_POSITIONS` is described as the maximum number of concurrent open positions, but the implementation only counts successful orders submitted during the current invocation. The counter starts at zero on every run. The code does not query existing open positions, pending orders, or positions opened by previous executions. It also does not reserve capacity atomically before order placement. Repeated manual or automated executions can therefore accumulate substantially more than `MAX_POSITIONS` concurrent positions. ### Attack Path 1. The Skill executes and successfully opens up to `MAX_POSITIONS` positions. 2. Those positions remain open. 3. The Skill executes again manually or through an automaton. 4. `placed` resets to zero, and no existing positions are queried. 5. A second set of up to `MAX_POSITIONS` orders is submitted. 6. Repeated runs continue increasing aggregate exposure beyond the configured limit. ### Impact Assessm ...[truncated 337 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:48
Finding

Cached client can retain live-trading mode during a later paper-mode invocation

Content
View full analysis
SimmerClient: """ live=False → venue="sim" (paper trades — safe default). live=True → venue="polymarket" (real trades, only with --live flag). """ global _client, MAX_POSITION, MIN_VOLUME, MAX_SPREAD, MIN_DAYS, MAX_POSITIONS, YES_THRESHOLD, NO_THRESHOLD, MIN_TRADE if _client is None: venue = "polymarket" if live else "sim" _client = SimmerClient( api_key=os.environ["SIMMER_API_KEY"], venue=venue, ) # Load tunable overrides set via the Simmer UI (SIMMER_* vars only). if live: _client.live = True try: _client.apply_skill_config(SKILL_SLUG) except AttributeError: pass # apply_skill_config only available in Simmer runtime # Re-read params in case apply_skill_config updated os.environ. MAX_POSITION = float(os.environ.get("SIMMER_MAX_POSITION", str(MAX_POSITION))) MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", str(MIN_VOLUME))) MAX_SPREAD = float(os.environ.get("SIMMER_MAX_SPREAD", str(MAX_SPREAD))) MIN_DAYS = int(os.environ.get( "SIMMER_MIN_DAYS", str(MIN_DAYS))) MAX_POSITIONS = int(os.environ.get( "SIMMER_MAX_POSITIONS", str(MAX_POSITIONS))) YES_THRESHOLD = float(os.environ.get("SIMMER_YES_THRESHOLD", str(YES_THRESHOLD))) NO_THRESHOLD = float(os.environ.get("SIMMER_NO_THRESHOLD", str(NO_THRESHOLD))) MIN_TRADE = float(os.environ.get("SIMMER_MIN_TRADE", str(MIN_TRADE))) return _client ``` ### Technical Analysis The module stores one global client in `_client`. The requested `live` mode is only evaluated when that global is `None`. Once initialized, later calls return the same object ...[truncated 1281 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (2)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding

The skill references environment-based credentials (SIMMER_API_KEY) and trading execution behavior, but it does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, that mismatch can lead to broader-than-expected access to environment secrets or execution features, making accidental credential exposure or unauthorized live trading more likely if the runtime grants default capabilities.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The manifest declares a required API credential (SIMMER_API_KEY) but provides no user-facing explanation of what service it authenticates to, what actions it enables, or how the secret will be used by the skill. In an agent skill that can place trades, undisclosed credential use increases the risk of users granting sensitive access without informed consent and can mask downstream API calls with financial consequences.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.