Back to skill

Security audit

Polymarket Market Maker

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed live trading skill, but its safety checks can fail open and cause unintended financial orders or exposure.

Install only if you understand this is an advanced live trading scaffold, not a production-safe bot. Use a restricted API key and small balances, avoid cron/live mode until fail-closed handling is added for inventory, open-order, and market-metadata failures, and pin/review the SDK version before exposing credentials.

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
market_maker.py:202
Finding

Open-order API failures can cause duplicate live orders

Content
View full analysis
set: """Return set of order IDs Simmer considers open (GTC/GTD on CLOB).""" try: resp = get_client().get_open_orders() return {o["order_id"] for o in resp.get("orders", []) if o.get("order_id")} except Exception as e: print(f" [warn] Could not fetch open orders: {e}") return set() ``` The returned empty set is subsequently used as proof that recorded orders are no longer open: ```python bid_needs_place = post_bid and ( not market_state.get("bid_order_id") or market_state.get("bid_order_id") not in open_order_ids ) ask_needs_place = post_ask and ( not market_state.get("ask_order_id") or market_state.get("ask_order_id") not in open_order_ids ) ``` ### Technical Analysis The implementation does not distinguish between: 1. A successful API response confirming that there are no open orders. 2. A failed API request for which the state of existing orders is unknown. On any SDK, network, authentication, rate-limit, parsing, or remote-service error, `get_open_order_ids()` returns an empty set. Existing locally recorded order IDs then appear absent from the open-order set, causing `bid_needs_place` and `ask_needs_place` to become true. This is a fail-open design in a safety-critical financial operation. When the remote state cannot be verified, the safe behavior is to suspend new order placement rather than assume that all previous orders have disappeared. ### Attack Path 1. The Skill has one or more GTC orders already live on the exchange. 2. The open-order API becomes unavailable, is rate-limited, returns malformed data, or is disrupted by an attacker capable of interfering with that service path. 3. `get_open_order_ids()` catches the exc ...[truncated 914 chars]
Remediation
View remediation
tuple[set[str], bool]: try: resp = get_client().get_open_orders() ids = { order["order_id"] for order in resp.get("orders", []) if order.get("order_id") } return ids, True except Exception as exc: print(f"[error] Could not verify open orders: {exc}") return set(), False ``` - Require the success flag before calling `run_market()` in live mode. - Reconcile local order IDs against authoritative exchange state before posting replacements. - Add idempotency or client-generated order identifiers if supported by the SDK. - Add bounded retries with exponential backoff for transient failures. - Emit a failure result to the automaton rather than reporting a successful run when reconciliation fails. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
market_maker.py:159
Finding

Inventory retrieval failures disable exposure controls

Content
View full analysis
tuple[float, float]: """Return (shares_yes, shares_no) from current positions for a market.""" try: positions = get_client().get_positions(venue="polymarket") for pos in positions: if pos.market_id == market_id: return float(pos.shares_yes or 0), float(pos.shares_no or 0) except Exception as e: print(f" [warn] Could not fetch inventory for {market_id[:8]}: {e}") return 0.0, 0.0 ``` The synthetic zero balance is then treated as authoritative: ```python shares_yes, shares_no = get_inventory(market_id) skew = net_skew(shares_yes, shares_no) max_skew_shares = (quote_usdc / mid) * max_skew_pct post_bid = True post_ask = True if skew > max_skew_shares: post_bid = False print(f" → Skip BID: net_yes_skew={skew:.1f} > max={max_skew_shares:.1f}") if skew < -max_skew_shares: post_ask = False print(f" → Skip ASK: net_no_skew={-skew:.1f} > max={max_skew_shares:.1f}") ``` ### Technical Analysis The Skill's primary directional-exposure control depends on accurate inventory data. However, any inventory retrieval exception is converted into `(0.0, 0.0)`, which represents a verified neutral position. This causes `net_skew()` to return zero and leaves both `post_bid` and `post_ask` enabled. A communication or parsing error therefore silently bypasses the configured `MM_MAX_SKEW_PCT` control. An unknown position must not be treated as a zero position. In live trading, inability to establish current exposure should prevent new risk from being added. ### Attack Path 1. The account accumulates a significant YES or NO position in a configured market. 2. The position API becomes unavailable, returns incompatible data, or is d ...[truncated 1032 chars]
Remediation
View remediation
Optional[tuple[float, float]]: try: positions = get_client().get_positions(venue="polymarket") for pos in positions: if pos.market_id == market_id: return float(pos.shares_yes or 0), float(pos.shares_no or 0) return 0.0, 0.0 except Exception as exc: print(f"[error] Could not verify inventory for {market_id}: {exc}") return None ``` - In `run_market()`, set a clear `inventory_unavailable` skip reason when the result is `None`. - Add bounded retries for transient service failures. - Consider calculating exposure from both positions and outstanding orders. - Apply an account-wide maximum exposure in addition to per-market skew limits. - Ensure automaton output reports the skipped market and failed safety check. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
market_maker.py:191
Finding

Market metadata failures bypass the unsupported-market safety guard

Content
View full analysis
dict: """Fetch full market dict for a market ID.""" try: resp = get_client()._request("GET", f"/api/sdk/markets/{market_id}") return resp.get("market", resp) except Exception as e: print(f" [warn] Could not fetch market info for {market_id[:8]}: {e}") return {} ``` An empty dictionary is treated as a market for which no negative-risk flag is set: ```python market_info = get_market_info(market_id) is_neg_risk = ( market_info.get("polymarket_neg_risk") or market_info.get("negativeRisk") or market_info.get("neg_risk") or market_info.get("is_neg_risk") ) if is_neg_risk: stats["skip_reason"] = "neg_risk_unsupported (multi-outcome market — synthetic-ask math invalid)" return stats ``` ### Technical Analysis The Skill documents that its synthetic ask formula is valid only for binary markets where YES and NO prices are complementary. Multi-outcome or negative-risk markets require different pricing behavior. The validation is implemented as a blacklist: the Skill proceeds unless one of several fields is truthy. If metadata retrieval fails, `get_market_info()` returns `{}`, all checks evaluate as false, and the Skill assumes the market is supported. This conflates “confirmed not negative-risk” with “market type unknown.” Because the formula can generate invalid prices for unsupported markets, live trading should require affirmative confirmation that the market is binary. ### Attack Path 1. A configured market is a negative-risk or otherwise unsupported multi-outcome market. 2. The separate mid-price request succeeds. 3. The full market-information request fails, returns malformed content, or becomes incompatible with the expected API schema. 4. ...[truncated 831 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding

Unpinned third-party SDK receives the account API credential

Content
View full analysis
=0.13.0 ``` The installation instructions explicitly request an upgrade to the newest matching release: ```bash pip install -U 'simmer-sdk>=0.13.0' ``` The installed package is imported into the Skill process and receives the API key: ```python def get_client(): global _client if _client is None: try: from simmer_sdk import SimmerClient except ImportError: print("Error: simmer-sdk not installed. Run: pip install simmer-sdk") sys.exit(1) api_key = os.environ.get("SIMMER_API_KEY") if not api_key: print("Error: SIMMER_API_KEY not set — get from simmer.markets/dashboard") sys.exit(1) _client = SimmerClient(api_key=api_key, venue="polymarket") return _client ``` ### Technical Analysis The dependency constraint does not identify a reviewed, immutable artifact. The `-U` installation instruction increases the likelihood that users will install a future release that was not part of this audit. The SDK executes with the same process privileges as the Skill and directly receives `SIMMER_API_KEY`. It also mediates balance queries, position retrieval, order lookup, trade placement, and cancellation. Therefore, a compromised, malicious, or unexpectedly incompatible future SDK release would have access to both sensitive credentials and financial operations. The audited project code does not directly exfiltrate the API key, and use of an authenticated SDK is necessary for the declared functionality. The risk arises from insufficient supply-chain reproducibility and not from confirmed malic ...[truncated 1247 chars]
Remediation
View remediation
Vulnerability Patterns
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • 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 (3)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill documents --live and --cancel-all commands for a wallet-enabled trading bot without a prominent warning that these actions place real orders or cancel active ones. In this context, omission of an explicit safety warning increases the chance of operator error, accidental live execution, and unintended order cancellation affecting real funds.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The documentation explicitly says the skill is binary-only, then admits it will still quote unsupported neg_risk markets where the core pricing formula is incorrect. In a live trading skill, knowingly allowing execution in a market type the strategy cannot price correctly can cause systematically mispriced orders, unintended fills, and direct financial loss.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
95% confidence
Finding

The dependency is specified with a lower-bound only constraint (>=0.13.0), which allows future unreviewed versions to be installed. This creates supply-chain risk because a breaking, vulnerable, or malicious upstream release could be pulled into the skill without notice, reducing build reproducibility and making security review harder.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
simmer-sdk>=0.13.0

Static analysis

No suspicious patterns detected.