Back to skill

Security audit

Polymarket 48h Price Curve Arb Trader

Security checks for vulnerabilities and agentic risk

Overview

This trading skill is transparent about needing a trading API key, but its live-trading safeguards are materially weaker than the documentation suggests.

Review before installing for any account with live trading enabled. Paper mode is disclosed as the default, but do not rely on the documented volume or concurrent-position limits as hard protections, and treat --live as capable of placing under-hedged or repeated positions. Use a limited API key and test in simulation first.

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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:303
Finding
Declared Minimum-Volume Safeguard Is Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:42`, `trader.py:303-321`, and `trader.py:374-388` **Vulnerability Type**: Missing enforcement of a configured trading safeguard **Risk Level**: High ### Vulnerable Code The minimum-volume setting is loaded: ```python MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "5000")) ``` However, market validation does not inspect market volume: ```python def valid_market(market) -> tuple[bool, str]: 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" ``` Market discovery likewise adds markets without enforcing the configured volume threshold: ```python def find_markets(client: SimmerClient) -> list: """Find active crypto price-threshold markets, deduplicated. Filters out non-threshold markets (e.g. 'Up or Down' coin-flips).""" seen, unique = set(), [] for kw in KEYWORDS: try: for m in client.find_markets(query=kw): market_id = getattr(m, "id", None) if market_id and market_id not in seen: q = getattr(m, "question", "").lower() if any(w in q for w in ("above", "between", "reach", "hit", "dip", "below", "exceed")): seen.add(market_id) unique.append(m) e ...[truncated 1641 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Enforce volume as a mandatory validation condition before signal calculation: ```python def valid_market(market) -> tuple[bool, str]: p = getattr(market, "current_probability", None) if not isinstance(p, (int, float)): return False, "missing probability" volume = getattr(market, "volume", None) if not isinstance(volume, (int, float)): return False, "missing market volume" if volume < MIN_VOLUME: return False, f"Volume ${volume:,.2f} < ${MIN_VOLUME:,.2f}" # Existing spread and resolution checks follow. ``` Use the exact SDK field representing executable or recent volume rather than assuming the field name. Prefer checking order-book depth at the intended order size in addition to aggregate volume. Further hardening should include: - Fail closed when volume or liquidity information is unavailable. - Validate effective execution price immediately before submission. - Set an order-specific maximum slippage. - Add tests proving that markets below `SIMMER_MIN_VOLUME` cannot reach `client.trade()`. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:431
Finding
Position Limit Resets on Every Invocation<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:431-459` **Vulnerability Type**: Ineffective concurrent-position limit **Risk Level**: High ### Vulnerable Code ```python # Execute trades on best violations placed = 0 for market_id, opp in sorted(all_opps.items(), key=lambda x: -x[1][2]): if placed >= MAX_POSITIONS: break market = opp[0] side, size, reasoning = compute_signal(market, opp) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, market_id) if not ok: 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}" print(f" [trade] {side.upper()} ${size} {tag} {status} — {reasoning[:110]}") if r.success: placed += 1 except Exception as e: print(f" [error] {market_id}: {e}") ``` ### Technical Analysis `MAX_POSITIONS` is presented as a maximum number of concurrent open positions. The implementation only compares it with `placed`, a local counter initialized to zero for every process invocation. The code does not: - Query existing open positions. - Account for positions opened by previous runs. - Detect whether the account already has exposure to the selected market. - Reserve capacity against concurrent instances. - Include orders placed by another strategy using the same account. Therefore, the limit restricts successful orders during one invocation rather than total concurrent exposure. Repeated or overlapping execution can exceed the configured limit by an arbitrary amount. ### Attack Path 1. Live mode is invoked while qualifying violations are present. 2. The program initializes `placed = ...[truncated 809 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Before submitting any order: 1. Query all current open positions and pending orders through the SDK. 2. Count existing positions using a clearly documented account-wide or strategy-specific policy. 3. Calculate remaining capacity: ```python remaining = max(0, MAX_POSITIONS - existing_open_count) ``` 4. Reject markets for which an equivalent or conflicting position already exists. 5. Recheck capacity immediately before each submission. 6. Use an account-side transactional limit or lock if the SDK supports it. For concurrent execution, use a durable distributed lock or an atomic server-side risk-control operation. A local in-memory counter is insufficient across processes. Also add tests covering: - Existing positions before startup. - Multiple sequential invocations. - Two concurrent instances. - Pending but not yet filled orders. - Existing exposure created by another strategy. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
trader.py:239
Finding
Multi-Leg Arbitrage Is Executed as Independent Non-Atomic Trades<![CDATA[ ## Vulnerability Details **File Location**: `trader.py:239-254` and `trader.py:416-459` **Vulnerability Type**: Unsafe non-atomic financial transaction handling **Risk Level**: High ### Vulnerable Code A monotonicity violation creates two related legs as separate opportunities: ```python if violation > MIN_VIOLATION: # Higher strike is overpriced OR lower strike is underpriced # Trade: sell NO on the higher-strike (overpriced), buy YES on the lower-strike (underpriced) opportunities.append(( hi_pt.market, "no", violation, f"Monotonicity break: P(>{hi_strike:,})={hi_pt.price:.1%} > P(>{lo_strike:,})={lo_pt.price:.1%} | " f"violation={violation:.1%} — {hi_pt.market.question[:55]}" )) opportunities.append(( lo_pt.market, "yes", violation, f"Monotonicity break: P(>{lo_strike:,})={lo_pt.price:.1%} < P(>{hi_strike:,})={hi_pt.price:.1%} | " f"violation={violation:.1%} — {lo_pt.market.question[:55]}" )) ``` The related legs are then flattened by market ID: ```python all_opps: dict[str, tuple] = {} for curve_key, points in curves.items(): if len(points) < 2: continue violations = find_violations(points) for market, side, mag, reason in violations: mid = getattr(market, "id", None) if not mid: continue existing = all_opps.get(mid) if existing is None or mag > existing[2]: all_opps[mid] = (market, side, mag, reason) ``` Each surviving opportunity is independently validated and submitted: ```python for market_id, opp in sorted(all_opps.items(), key=lambda x: -x[1][2]): if placed >= MAX_POSITIONS: break market = opp[0] side, size, reasoning = compute_signal(market, opp) if not side: print(f" [skip] {reasoning}") continue ok, why = context_ok(client, market_id) if not ok: print(f" [skip] {why}") continue try: r = client.trade( ...[truncated 2399 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Represent each detected violation as a structured trade plan containing all required legs rather than independent market opportunities. Before execution: 1. Validate every leg together. 2. Obtain executable quotes and depth for the complete intended size. 3. Calculate payoff-balanced quantities, including fees and slippage. 4. Ensure the net worst-case payoff satisfies the strategy's minimum-profit requirement. 5. Reserve position and capital limits for the entire plan. During execution: - Use atomic batch execution, fill-or-kill orders, or another all-or-none mechanism if supported. - If atomic execution is unavailable, define the safest order sequence and an immediate compensating hedge. - Abort before the first order if any companion leg cannot be executed. - On partial failure, cancel remaining orders and submit a bounded emergency hedge. - Record plan-level status so a partially completed plan cannot be retried as if no exposure existed. Threshold and context checks should operate at the plan level. Tests should simulate first-leg success followed by second-leg rejection, timeout, partial fill, and adverse price movement. ]]>

T08 · Insecure Dependencies

Warning
Location
clawhub.json:7
Finding
Privileged Trading SDK Dependency Is Unpinned<![CDATA[ ## Vulnerability Details **File Location**: `clawhub.json:7-9` **Vulnerability Type**: Unpinned privileged third-party dependency **Risk Level**: Medium ### Vulnerable Code ```json "pip": [ "simmer-sdk" ] ``` ### Technical Analysis The project installs `simmer-sdk` without an exact version constraint or integrity hash. Package resolution can therefore select a future release that was not reviewed with this skill. This dependency is security-sensitive because the code imports `SimmerClient`, supplies it with `SIMMER_API_KEY`, and relies on it for network communication and order execution: ```python _client = SimmerClient( api_key=os.environ["SIMMER_API_KEY"], venue=venue, ) ``` An unpinned dependency does not prove that the current package is malicious. It nevertheless creates a supply-chain exposure in which an upstream compromise, malicious release, or incompatible future change can alter effective runtime behavior after the skill itself has been audited. ### Attack Path 1. The upstream package account or release pipeline is compromised, or an unsafe future version is published. 2. The project is installed or rebuilt after that release. 3. Package resolution retrieves the new version because no exact version is specified. 4. The package is imported by `trader.py`. 5. The SDK receives `SIMMER_API_KEY` and executes with the process's network and trading authority. 6. A compromised package could exfiltrate the credential, alter market data, redirect API traffic, or submit unauthorized orders. ### Impact Assessment Potential scope is limited by the runtime identity and the permissions attached to `SIMMER_API_KEY`, but may include: - Disclosure of the trading credential. - Unauthorized simulated or live trades. - Manipulation of market search, context, and trade responses. - Access to files and environment variables available to the Python process. - Network requests under the runtime's permissions. No evidence in the reviewed file ...[truncated 71 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Pin the dependency to an exact reviewed version, for example: ```json "pip": [ "simmer-sdk==<reviewed-version>" ] ``` Additional supply-chain controls should include: - Maintain a lock file or constraints file with cryptographic hashes. - Verify package provenance and release signatures where available. - Review dependency changes before upgrading. - Run the SDK with the least-privileged trading credential possible. - Restrict outbound network access to approved API destinations. - Rotate `SIMMER_API_KEY` after any suspected dependency compromise. - Test upgrades in paper mode before allowing live execution. ]]>
Vulnerability Patterns
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (1)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill describes use of a high-value credential (`SIMMER_API_KEY`) and trading behavior, but it does not declare an explicit tool scope such as `permissions` or `allowed-tools`. That mismatch weakens least-privilege controls because an agent runtime may expose environment access more broadly than intended, increasing the chance of credential access or misuse if the skill is invoked in an unsafe context.

Static analysis

No suspicious patterns detected.