Back to skill

Security audit

Polymarket Twitter Weekend Drift Trader

Security checks for vulnerabilities and agentic risk

Overview

This is a disclosed paper-by-default trading skill, but its live-trading safeguards are incomplete enough that real-funds use needs review first.

Use this in paper mode unless you have reviewed and fixed the live risk controls. Before providing a live-capable key, pin and review simmer-sdk, use a limited trading credential, and verify that market volume, total open-position limits, and min/max trade sizing are enforced server-side or in code.

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

Note
Location
trader.py:42
Finding

Minimum Market Volume Safeguard Is Declared but Never Enforced

Content
View full analysis

Vulnerability Details

File Location: trader.py:42, trader.py:108-130, and trader.py:233-250
Vulnerability Type: Missing financial risk-control enforcement
Risk Level: Suspicious

The project documentation presents SIMMER_MIN_VOLUME as a minimum market-volume filter, but the trading workflow never checks a market's volume before submitting an order.

Relevant code:

python
MIN_VOLUME = float(os.environ.get("SIMMER_MIN_VOLUME", "1000"))
python
def find_markets(client: SimmerClient) -> list:
    seen, unique = set(), []
    for kw in KEYWORDS:
        try:
            for m in client.find_markets(query=kw):
                q = getattr(m, 'question', '')
                if m.id not in seen and POST_FILTER.search(q) and BIN_PATTERN.search(q):
                    seen.add(m.id)
                    unique.append(m)
        except Exception as e:
            safe_print(f"[search] {kw!r}: {e}")
    try:
        for m in client.get_markets(limit=200):
            q = getattr(m, 'question', '')
            if m.id not in seen and POST_FILTER.search(q) and BIN_PATTERN.search(q):
                seen.add(m.id)
                unique.append(m)
    except Exception as e:
        safe_print(f"[fallback] {e}")
    return unique
python
for m in markets:
    if placed >= MAX_POSITIONS:
        break
    side, size, reasoning = compute_signal(m)
    if not side:
        safe_print(f"  [skip] {reasoning}")
        continue
    ok, why = context_ok(client, m.id)
    if not ok:
        safe_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,
        )

Technical Analysis

MIN_VOLUME is initialized and refreshed after skill configuration, but it is not referenced by market di ...[truncated 1522 chars]

Remediation
View remediation

Remediation Suggestions

  • Retrieve a trusted numeric volume field for every market before signal evaluation.
  • Reject markets when market.volume < MIN_VOLUME.
  • Fail closed when volume is absent, malformed, stale, or cannot be independently verified.
  • If the SDK exposes multiple volume measurements, document and use the intended interval, currency, and venue-specific source.
  • Revalidate volume immediately before live execution to reduce time-of-check/time-of-use risk.
  • Add automated tests proving that markets below the threshold and markets with missing volume never reach client.trade().
  • Log the observed volume and applied threshold for auditability.

T09 · Insecure Skill Coding Practices

Note
Location
trader.py:233
Finding

Configured Maximum Open Positions Only Limits Orders Within the Current Process

Content
View full analysis

Vulnerability Details

File Location: trader.py:233-255
Vulnerability Type: Portfolio-limit bypass through repeated execution
Risk Level: Suspicious

The MAX_POSITIONS control counts only successful orders placed during the current invocation. Existing open positions are never queried or included.

Relevant code:

python
placed = 0
for m in markets:
    if placed >= MAX_POSITIONS:
        break
    side, size, reasoning = compute_signal(m)
    if not side:
        safe_print(f"  [skip] {reasoning}")
        continue
    ok, why = context_ok(client, m.id)
    if not ok:
        safe_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}"
        safe_print(f"  [trade] {side.upper()} ${size} {tag} {status} -- {reasoning[:70]}")
        if r.success:
            placed += 1
    except Exception as e:
        safe_print(f"  [error] {m.id}: {e}")

Technical Analysis

The variable placed is initialized to zero every time run() executes. It tracks successful orders rather than the number of currently open portfolio positions. This conflicts with the documented meaning of SIMMER_MAX_POSITIONS as a concurrent open-position limit.

The control can therefore be bypassed without modifying code or configuration: each new process receives a fresh allowance. It may also count an order that adds to an existing position as though it were a distinct position, while failing to account for positions opened by earlier runs or other clients.

Attack Path

  1. The trader already has up to MAX_POSITIONS open positions.
  2. A user, scheduler, or automation mechanism invokes trader.py --live again.

...[truncated 651 chars]

Remediation
View remediation

Remediation Suggestions

  • Query all current open positions from the trading venue before processing candidate markets.
  • Calculate the number of unique resulting positions rather than the number of successful orders.
  • Subtract the existing open-position count from MAX_POSITIONS to determine the remaining allowance.
  • Distinguish between increasing an existing position and opening a new position.
  • Recheck the position count immediately before each live trade.
  • Use an account-level atomic limit or server-side risk control where available, preventing concurrent processes from racing past the cap.
  • Add tests covering repeated runs, pre-existing positions, simultaneous processes, failed orders, and orders that add to existing positions.

T09 · Insecure Skill Coding Practices

Note
Location
trader.py:201
Finding

Minimum Trade Floor Can Override the Maximum Position Limit

Content
View full analysis

Vulnerability Details

File Location: trader.py:201-220 and clawhub.json:20-24,67-71
Vulnerability Type: Financial limit bypass caused by inconsistent configuration constraints
Risk Level: Suspicious

The position-sizing expression applies MIN_TRADE as an unconditional floor but does not clamp the result back to MAX_POSITION.

Relevant code:

python
if p <= YES_THRESHOLD:
    conviction = min(1.0, (YES_THRESHOLD - p) / YES_THRESHOLD * bias)
    size = max(MIN_TRADE, round(conviction * MAX_POSITION, 2))
    edge = YES_THRESHOLD - p
    return "yes", size, (
        f"YES {p:.0%} edge={edge:.0%} drift={drift_pct:+.0%} "
        f"bias={bias:.1f}x ${size} -- {person['key']} {bl}-{bu}"
    )

if p >= 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 {p:.0%} edge={edge:.0%} drift={drift_pct:+.0%} "
        f"bias={bias:.1f}x ${size} -- {person['key']} {bl}-{bu}"
    )
json
{
  "env": "SIMMER_MAX_POSITION",
  "default": 40,
  "range": [1, 200],
  "step": 1,
  "label": "Max position size (USD)",
  "type": "number"
},
{
  "env": "SIMMER_MIN_TRADE",
  "type": "number",
  "default": 5,
  "range": [1, 100],
  "step": 1,
  "label": "Min trade size (USD)"
}

Technical Analysis

The independently valid configuration ranges permit SIMMER_MAX_POSITION=1 and SIMMER_MIN_TRADE=100. Under those settings, both sizing branches calculate:

python
size = max(100, calculated_amount)

The submitted amount is therefore at least $100, despite the configured maximum position being $1. The same violation occurs whenever MIN_TRADE exceeds MAX_POSITION.

This is a broken security invariant: the maximum is expected to be an upper bound regardless of conviction or mini ...[truncated 1046 chars]

Remediation
View remediation

Remediation Suggestions

  • Validate configuration after loading it and reject startup unless 0 < MIN_TRADE <= MAX_POSITION.
  • Clamp the final amount defensively:
python
calculated = round(conviction * MAX_POSITION, 2)
size = min(MAX_POSITION, max(MIN_TRADE, calculated))
  • Do not place a trade when venue minimums exceed the configured maximum; report that the risk policy prevents execution.
  • Add a relational constraint to the configuration interface rather than relying only on independent numeric ranges.
  • Validate all environment-derived values for finiteness, positivity, and permitted bounds.
  • Add tests for equal limits, inverted limits, zero or negative values, non-finite floats, and both YES and NO sizing branches.

T08 · Insecure Dependencies

Note
Location
clawhub.json:6
Finding

Unpinned Third-Party Trading SDK Receives a Live-Capable API Credential

Content
View full analysis

Vulnerability Details

File Location: clawhub.json:6-9 and trader.py:16,67-78
Vulnerability Type: Unpinned privileged dependency
Risk Level: Suspicious

The project installs simmer-sdk without an exact version or integrity hash. The imported package is then given SIMMER_API_KEY and controls market discovery and trade submission.

Relevant code:

json
"requires": {
  "env": [
    "SIMMER_API_KEY"
  ],
  "pip": [
    "simmer-sdk"
  ]
}
python
from simmer_sdk import SimmerClient
python
def get_client(live: bool = False) -> SimmerClient:
    global _client, MAX_POSITION, MIN_VOLUME, MAX_SPREAD, MIN_DAYS, MAX_POSITIONS
    global 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,
        )
        if live:
            _client.live = True
        try:
            _client.apply_skill_config(SKILL_SLUG)
        except AttributeError:
            pass

Technical Analysis

An unversioned package declaration allows installation behavior to change over time without any project modification or renewed review. Python packages can execute code during installation and import. After import, this SDK is explicitly supplied with the API key and trusted to perform network and trading operations.

The audit found no evidence that the currently intended SDK is malicious. The issue is supply-chain exposure: a compromised publisher account, malicious future release, or unexpected dependency resolution could introduce code with access to the credential and live trading workflow.

Attack Path

  1. An attacker compromises the package publication channel, maintainer account, build pipeline, or a relevant transitive dependency.
  2. A modified release becomes the version selected by the unp ...[truncated 907 chars]
Remediation
View remediation

Remediation Suggestions

  • Pin simmer-sdk to an exact version that has been reviewed.
  • Install it using a lockfile or requirements file containing cryptographic hashes.
  • Lock and review transitive dependencies as well as the direct package.
  • Use a private, controlled package index or verified artifact repository where practical.
  • Separate paper and live credentials, and ensure paper-mode credentials cannot authorize real trades.
  • Grant live keys only the minimum account and venue permissions required.
  • Rotate the key after any suspected dependency compromise.
  • Run dependency installation and execution in an isolated environment with restricted filesystem and network access.
  • Monitor dependency advisories and require a new review before upgrading the pinned version.
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)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding

The skill declares that it requires the SIMMER_API_KEY environment variable but does not define any explicit tool scope such as permissions or allowed-tools. That creates an implicit capability boundary: an agent may access environment data without a clearly declared policy, increasing the risk of unintended secret exposure or broader runtime access than the skill metadata suggests.

Content

No source excerpt is available for this finding.