{
  "schema_version": "1.0.0",
  "bot_id": "5.8",
  "bot_name": "ContractAddressGuard",
  "slug": "contractaddressguard",
  "layer": "Security",
  "layer_key": "sec",
  "bot_class": "Guardrail",
  "authority": [
    "Reject",
    "Pause"
  ],
  "status": "planned",
  "readiness": "Ready to build",
  "flagship": true,
  "is_reference": true,
  "public_export": false,
  "identity": {
    "layer": "Security",
    "bot_class": "Guardrail",
    "authority": "Reject, Pause",
    "runs_before": "Any order signing or submission",
    "runs_after": "Strategy OrderIntent and all Risk guardrails",
    "applies_to": "Every pending order before signature or on-chain submission",
    "default_mode": "shadow_only",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Security pod"
  },
  "purpose": "ContractAddressGuard refuses to allow a signature or on-chain submission against any contract address that is not present on the committed CLOB V2 allow-list. It enforces the EIP-712 domain separator match against the expected V2 domain and rejects any order that targets a V1 Exchange address. This is a hard security control for the V1-to-V2 migration and must not be loosened without an explicit signed-off admin change. It cannot modify orders \u2014 it only approves or rejects.",
  "why_it_matters": [
    {
      "failure": "Order signed against V1 contract after migration",
      "consequence": "Funds are sent to a deprecated contract that may not be monitored, potentially locking or losing assets that cannot be recovered through normal settlement."
    },
    {
      "failure": "Unknown contract address accepted",
      "consequence": "Signing an order against an unrecognised contract is the primary vector for phishing and malicious contract substitution attacks in decentralised trading environments."
    },
    {
      "failure": "Domain separator mismatch not detected",
      "consequence": "A forged or misconfigured EIP-712 domain separator could cause a valid-looking signature to be replayed on a different contract or chain, leading to unintended asset transfers."
    },
    {
      "failure": "No alert on block",
      "consequence": "Without an alert every time a suspicious address is blocked, security incidents may not be noticed until significant damage has occurred."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB V2 Exchange contract addresses by chain",
      "source": "on-chain",
      "required": true,
      "use": "Build and maintain the allow-list of valid contract addresses that orders may target."
    },
    {
      "input": "EIP-712 domain separator for the V2 CLOB exchange",
      "source": "on-chain",
      "required": true,
      "use": "Verify that the domain separator in the pending order matches the expected V2 domain before allowing signature."
    },
    {
      "input": "Order-type schema of the pending intent",
      "source": "CLOB",
      "required": true,
      "use": "Confirm the order conforms to the V2 order schema specification; V1 schema orders are rejected regardless of address."
    }
  ],
  "internal_inputs": [
    {
      "input": "Committed V2 address allow-list",
      "source": "Admin UI",
      "required": true,
      "use": "Authoritative list of permitted contract addresses and chain IDs; must be signed off before any address is added or removed."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Reject all orders immediately if KillSwitch is active, before address checks run."
    }
  ],
  "raw_params": [
    "v2_addresses \u00b7 list (locked)",
    "block_v1_signing \u00b7 bool (locked true)",
    "require_domain_match \u00b7 bool (locked true)",
    "alert_on_block \u00b7 bool (locked true)"
  ],
  "parameters": [
    {
      "name": "v2_addresses",
      "default": "[]",
      "warning": "\u2014",
      "hard": "\u2014",
      "controls": "The committed list of CLOB V2 Exchange contract addresses (address + chainId pairs) that orders are permitted to target. This list is locked and can only be changed via a signed admin action.",
      "why_default_matters": "The default is an empty list, which means no orders are permitted until the list is explicitly populated and locked by an admin. This fail-closed default prevents accidental signing on an unconfigured deployment.",
      "threshold_logic": [
        {
          "condition": "order.contract_address in v2_addresses AND chainId matches",
          "action": "APPROVE \u2014 proceed to signing"
        },
        {
          "condition": "order.contract_address NOT in v2_addresses",
          "action": "REJECT \u2014 CONTRACT_ADDRESS_NOT_ALLOWED"
        },
        {
          "condition": "v2_addresses is empty",
          "action": "REJECT \u2014 CONTRACT_ADDRESS_NOT_ALLOWED (not configured)"
        }
      ],
      "dev_check": "if (!p.v2_addresses.some(a => a.address === order.contract_address && a.chainId === order.chainId)) return reject('CONTRACT_ADDRESS_NOT_ALLOWED');",
      "user_facing": "This order targeted a contract address that is not on the approved list. It was blocked for your security."
    },
    {
      "name": "block_v1_signing",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true (locked), any order carrying a detected V1 Exchange address is rejected outright, even if somehow present on the allow-list.",
      "why_default_matters": "V1 contracts are deprecated. No legitimate order should target them after the migration. This parameter is locked to prevent accidental downgrade.",
      "threshold_logic": [
        {
          "condition": "block_v1_signing=true AND V1 address detected",
          "action": "REJECT \u2014 CONTRACT_ADDRESS_NOT_ALLOWED"
        },
        {
          "condition": "V1 address not detected",
          "action": "Proceed to other checks"
        }
      ],
      "dev_check": "if (p.block_v1_signing && V1_ADDRESSES.has(order.contract_address)) return reject('CONTRACT_ADDRESS_NOT_ALLOWED');",
      "user_facing": "This order used an old contract version. It was blocked as part of the exchange upgrade process."
    },
    {
      "name": "require_domain_match",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true (locked), the EIP-712 domain separator in the order must exactly match the expected V2 domain separator for the given chain. Any mismatch causes an immediate reject.",
      "why_default_matters": "Domain separator mismatches are a primary indicator of a replay attack or a misconfigured client. Accepting orders with wrong domain separators undermines the security of EIP-712 signing.",
      "threshold_logic": [
        {
          "condition": "require_domain_match=true AND domain separator matches",
          "action": "APPROVE \u2014 proceed"
        },
        {
          "condition": "require_domain_match=true AND domain separator mismatch",
          "action": "REJECT \u2014 CONTRACT_ADDRESS_NOT_ALLOWED (domain mismatch)"
        }
      ],
      "dev_check": "if (p.require_domain_match && order.domainSeparator !== EXPECTED_V2_DOMAIN) return reject('CONTRACT_ADDRESS_NOT_ALLOWED');",
      "user_facing": "This order contained an unexpected security signature. It was blocked to protect your wallet."
    },
    {
      "name": "alert_on_block",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "When true (locked), every blocked order triggers an immediate security alert to the monitoring stack with the full order metadata and blocked address.",
      "why_default_matters": "Security rejections are high-signal events. Every block may indicate an attempted attack or a client misconfiguration. Alerting on every block ensures none are missed.",
      "threshold_logic": [
        {
          "condition": "alert_on_block=true AND order rejected",
          "action": "Emit security alert with order metadata to monitoring"
        },
        {
          "condition": "alert_on_block=false",
          "action": "Silent reject \u2014 not recommended"
        }
      ],
      "dev_check": "if (p.alert_on_block && decision === 'REJECT') alerting.emit('SECURITY_BLOCK', { order, reason });",
      "user_facing": "This order was blocked and flagged for review by the security system."
    }
  ],
  "default_config": {
    "bot_id": "sec.contract_address_guard",
    "version": "1.0.0",
    "mode": "hard_guard",
    "defaults": {
      "v2_addresses": [],
      "block_v1_signing": true,
      "require_domain_match": true,
      "alert_on_block": true
    },
    "locked": {
      "block_v1_signing": {
        "immutable": true
      },
      "require_domain_match": {
        "immutable": true
      },
      "alert_on_block": {
        "immutable": true
      },
      "v2_addresses": {
        "change_requires_signed_admin_action": true
      }
    }
  },
  "implementation_flow": [
    "Receive a pending order or OrderIntent before it reaches the signing step.",
    "Check KillSwitch active flag; if active, return REJECT immediately with KILL_SWITCH_ACTIVE.",
    "Check whether v2_addresses is non-empty; if empty, return REJECT with CONTRACT_ADDRESS_NOT_ALLOWED and log a configuration alert.",
    "If block_v1_signing=true, check whether order.contract_address matches any known V1 Exchange address; if matched, return REJECT with CONTRACT_ADDRESS_NOT_ALLOWED.",
    "Check order.contract_address and order.chainId against the v2_addresses allow-list; if not found, return REJECT with CONTRACT_ADDRESS_NOT_ALLOWED.",
    "If require_domain_match=true, compute the expected EIP-712 domain separator for the V2 contract on the target chain and compare with order.domainSeparator; if mismatch, return REJECT with CONTRACT_ADDRESS_NOT_ALLOWED.",
    "Validate order schema against the V2 order specification; if the order uses V1 schema fields, return REJECT with CONTRACT_ADDRESS_NOT_ALLOWED.",
    "If alert_on_block=true and any reject was issued, emit a security alert to the monitoring stack with full order metadata.",
    "Log the decision (approve or reject), the checked address, chain ID, and timestamp to the governance audit trail via BuilderAttribution.",
    "Return APPROVE with inputs_used and checked_at timestamp if all checks pass."
  ],
  "decision_logic": {
    "approve": "order.contract_address is present in v2_addresses with matching chainId, EIP-712 domain separator matches the expected V2 domain, order schema is V2-compliant, and no V1 address is detected.",
    "reshape_required": "Not applicable \u2014 ContractAddressGuard does not reshape orders. An order either targets a permitted contract or it does not.",
    "reject": "Contract address not in allow-list (CONTRACT_ADDRESS_NOT_ALLOWED), V1 address detected (CONTRACT_ADDRESS_NOT_ALLOWED), domain separator mismatch (CONTRACT_ADDRESS_NOT_ALLOWED), allow-list is empty (CONTRACT_ADDRESS_NOT_ALLOWED), or KillSwitch active (KILL_SWITCH_ACTIVE).",
    "warning_only": "Not used \u2014 ContractAddressGuard has reject authority. All anomalies result in a hard reject and an alert."
  },
  "decision_output_schema": "SecurityCheck",
  "decision_output_example": {
    "check_id": "sec.contract_address_guard.20260509T100000Z",
    "scope": "contract",
    "decision": "DENY",
    "reason_code": "CONTRACT_ADDRESS_NOT_ALLOWED",
    "evidence": {
      "submitted_address": "0xDEAD1234",
      "chain_id": 137,
      "allow_list_version": "v2.2026-05-08",
      "allow_list_match": false,
      "alert_raised": true
    },
    "checked_at": "2026-05-09T10:00:00Z"
  },
  "developer_log": {
    "bot_id": "sec.contract_address_guard",
    "decision": "DENY",
    "reason_code": "CONTRACT_ADDRESS_NOT_ALLOWED",
    "inputs_used": [
      "clob.order_schema",
      "admin_ui.v2_allow_list"
    ],
    "metrics": {
      "submitted_address": "0xDEAD1234",
      "allow_list_size": 4,
      "match": false
    },
    "checked_at": "2026-05-09T10:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Order blocked \u2014 unknown contract address",
      "message": "This order was directed at a contract address that is not on the approved list. It was blocked to protect your funds from being sent to an unverified contract."
    },
    {
      "situation": "Order blocked \u2014 old exchange contract",
      "message": "This order was targeting an older version of the exchange contract. All trading now goes through the current version. The order was blocked automatically."
    },
    {
      "situation": "Order blocked \u2014 security signature mismatch",
      "message": "The security parameters in this order did not match what is expected for the current exchange. The order was blocked to prevent a potential signing issue."
    },
    {
      "situation": "Order blocked \u2014 allow-list not configured",
      "message": "The approved contract list has not been set up yet. No orders can be placed until the list is configured and verified."
    },
    {
      "situation": "Order blocked \u2014 security alert raised",
      "message": "This order was blocked and flagged for security review. No funds were moved. The security team has been notified."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "An order being signed against a non-V2 contract because the allow-list check is bypassed or the list is misconfigured, enabling unintended asset transfers.",
    "false_positive_risk": "Rejecting a legitimate order because the V2 address list has not yet been populated after a contract upgrade, requiring an admin action to unblock trading.",
    "false_negative_risk": "Approving an order against a stale allow-list that does not yet reflect a contract deprecation if the list cache is not refreshed promptly after an admin change.",
    "safe_fallback": "If the v2_addresses list is empty or cannot be loaded, reject all orders with CONTRACT_ADDRESS_NOT_ALLOWED. An unconfigured allow-list is fail-closed. If admin UI is unreachable, continue using the last cached list and alert.",
    "required_dependencies": [
      "Admin UI v2 allow-list with signed admin approval",
      "On-chain V2 contract address registry",
      "KillSwitch active flag",
      "BuilderAttribution governance audit log"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when address is in allow-list and domain matches",
        "setup": "order.contract_address=0xVALID, chainId=137, domainSeparator=EXPECTED_V2",
        "expected": "APPROVE"
      },
      {
        "test": "Reject when address is not in allow-list",
        "setup": "order.contract_address=0xUNKNOWN, allow_list=[0xVALID]",
        "expected": "REJECT with reason_code=CONTRACT_ADDRESS_NOT_ALLOWED"
      },
      {
        "test": "Reject when V1 address detected and block_v1_signing=true",
        "setup": "order.contract_address=0xV1_EXCHANGE, block_v1_signing=true",
        "expected": "REJECT with reason_code=CONTRACT_ADDRESS_NOT_ALLOWED and alert_emitted=true"
      },
      {
        "test": "Reject when domain separator does not match",
        "setup": "order.contract_address=0xVALID, domainSeparator=WRONG_DOMAIN",
        "expected": "REJECT with reason_code=CONTRACT_ADDRESS_NOT_ALLOWED"
      },
      {
        "test": "Reject when v2_addresses list is empty",
        "setup": "v2_addresses=[]",
        "expected": "REJECT with reason_code=CONTRACT_ADDRESS_NOT_ALLOWED and configuration alert"
      },
      {
        "test": "Reject when chainId does not match even if address matches",
        "setup": "order.contract_address=0xVALID on chainId=1 but allow_list has chainId=137 only",
        "expected": "REJECT with reason_code=CONTRACT_ADDRESS_NOT_ALLOWED"
      },
      {
        "test": "Alert is emitted on every reject when alert_on_block=true",
        "setup": "any reject scenario, alert_on_block=true",
        "expected": "Security alert emitted with full order metadata"
      }
    ],
    "integration": [
      {
        "test": "Order signing step is never reached when address is not in allow-list",
        "expected": "REJECT before signing; no signature produced; alert emitted to monitoring"
      },
      {
        "test": "Allow-list update via Admin UI immediately reflected in subsequent checks",
        "expected": "Order previously rejected is approved after valid address is added to allow-list via Admin UI"
      },
      {
        "test": "KillSwitch active bypasses allow-list check and rejects immediately",
        "expected": "REJECT with KILL_SWITCH_ACTIVE without reading the allow-list"
      }
    ],
    "property": [
      {
        "property": "An empty allow-list always produces REJECT \u2014 never APPROVE",
        "required": "Always true \u2014 fail-closed on unconfigured deployment"
      },
      {
        "property": "A V1 address with block_v1_signing=true always results in REJECT even if somehow present in the allow-list",
        "required": "Always true \u2014 V1 block takes precedence over the allow-list"
      },
      {
        "property": "Every REJECT emits a security alert when alert_on_block=true",
        "required": "Always true \u2014 no silent security rejects"
      }
    ]
  },
  "checklist_overrides": {
    "warning_thresholds": {
      "na": true,
      "reason": "Binary check \u2014 no warning band; allow-list is fail-closed"
    },
    "hard_thresholds": {
      "na": true,
      "reason": "Binary check \u2014 disallowed contracts are immediate HARD_REJECT"
    }
  },
  "legacy_goal": "Refuse to sign anything against a contract address not on the committed CLOB V2 allow-list. Critical for the V1 \u2192 V2 migration.",
  "legacy_pm_signals": [
    "Hard-coded CLOB V2 Exchange addresses by chain",
    "EIP-712 domain separator vs. expected V2 domain",
    "Order-type schema match against V2 spec",
    "Detection of any V1 Exchange address in pending intents"
  ],
  "legacy_external_feeds": [],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "onchain"
  ],
  "reference_implementation": {
    "summary": "Validates every pending order against a hardcoded V2 allow-list (CTFExchangeV2, NegRiskAdapter, pUSD ERC-20 on Polygon), verifies the EIP-712 domain separator version is '2', and rejects any order targeting a V1 address.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "// V2 allow-list \u2014 chain ID 137 (Polygon)\nCONST V2_ALLOW_LIST = [\n  { address: '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E', label: 'CTFExchangeV2' },\n  { address: '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296', label: 'NegRiskAdapter' },\n  { address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', label: 'pUSD ERC-20' }\n]\nCONST V1_DENY_LIST = [ '0xC5d563A36AE78145C45a50134d48A1A61A3A4Dc7' ]  // old CTFExchange\nCONST EXPECTED_EIP712_DOMAIN_VERSION = '2'\n\nFUNCTION checkContractAddress(pendingOrder):\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    EMIT SecurityCheck(decision=DENY, reason=KILL_SWITCH_ACTIVE)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: KILL_SWITCH_ACTIVE })\n    RETURN\n\n  // --- 1. V2 allow-list non-empty check ---\n  IF V2_ALLOW_LIST.isEmpty():\n    EMIT SecurityCheck(decision=DENY, reason=CONTRACT_ADDRESS_NOT_ALLOWED)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: 'allow_list_empty' })\n    RETURN\n\n  // --- 2. V1 deny-list check ---\n  IF V1_DENY_LIST.includes(pendingOrder.contract_address):\n    EMIT SecurityCheck(decision=DENY, reason=CONTRACT_ADDRESS_NOT_ALLOWED)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: 'v1_address_detected' })\n    RETURN\n\n  // --- 3. V2 allow-list membership check ---\n  matched = V2_ALLOW_LIST.find(\n    a => a.address == pendingOrder.contract_address\n         AND pendingOrder.chain_id == 137\n  )\n  IF NOT matched:\n    EMIT SecurityCheck(decision=DENY, reason=CONTRACT_ADDRESS_NOT_ALLOWED)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: 'not_in_allow_list' })\n    RETURN\n\n  // --- 4. EIP-712 domain separator check ---\n  // Domain version must be '2'; ClobAuth domain stays '1'\n  IF pendingOrder.eip712_domain_version != EXPECTED_EIP712_DOMAIN_VERSION:\n    EMIT SecurityCheck(decision=DENY, reason=CONTRACT_ADDRESS_NOT_ALLOWED)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: 'domain_version_mismatch' })\n    RETURN\n\n  // --- 5. V2 order schema check ---\n  // V2 schema: timestamp + metadata(bytes32) + builder(bytes32)\n  // V1 fields nonce/feeRateBps/taker must be absent\n  IF pendingOrder.nonce IS NOT NULL OR pendingOrder.feeRateBps IS NOT NULL:\n    EMIT SecurityCheck(decision=DENY, reason=CONTRACT_ADDRESS_NOT_ALLOWED)\n    alerting.emit('SECURITY_BLOCK', { pendingOrder, reason: 'v1_schema_fields_present' })\n    RETURN\n\n  // --- 6. Log to governance audit trail ---\n  EMIT GovernanceLog(event=SECURITY_CHECK_PASSED, order=pendingOrder, allow_list_label=matched.label)\n\n  // --- 7. Happy path ---\n  EMIT SecurityCheck(decision=ALLOW, checked_at=now_iso())\n",
    "helpers": [
      {
        "name": "buildOrderTypedData",
        "signature": "buildOrderTypedData(intent, domain) -> TypedData",
        "purpose": "Constructs the EIP-712 typed data structure for an order; ContractAddressGuard validates the domain.verifyingContract field against the allow-list."
      },
      {
        "name": "fetchClobPublic",
        "signature": "fetchClobPublic(path: str) -> JSON",
        "purpose": "Reads market metadata from CLOB for V2 schema validation context."
      },
      {
        "name": "isStale",
        "signature": "isStale(snapshot: any, maxAgeS: int) -> bool",
        "purpose": "Used to detect stale allow-list cache during cold starts."
      },
      {
        "name": "toUsdcUnits",
        "signature": "toUsdcUnits(rawUsd: float) -> int",
        "purpose": "Not called directly; imported for consistency with Security pod SDK setup."
      }
    ],
    "sdk_calls": [
      "buildOrderTypedData(pendingOrder, { name: 'CTFExchange', version: '2', chainId: 137, verifyingContract: '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E' })",
      "fetchClobPublic('/markets/' + pendingOrder.market_id)",
      "internal.killswitch.status()",
      "alerting.emit('SECURITY_BLOCK', metadata)"
    ],
    "complexity": "O(1) per order \u2014 allow-list lookup is a constant-size set"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Pending order targeting CTFExchangeV2 (valid)",
        "source": "internal",
        "payload": {
          "intent_id": "int_5e6f7a8b9c0d1e2f",
          "contract_address": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
          "chain_id": 137,
          "eip712_domain_version": "2",
          "market_id": "0x5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e",
          "side": "BUY",
          "size_usd": 400,
          "timestamp_ms": 1746768672000,
          "metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "builder": "0x706f6c7974726164657273000000000000000000000000000000000000000000"
        }
      },
      {
        "label": "Pending order targeting V1 address (should DENY)",
        "source": "internal",
        "payload": {
          "intent_id": "int_3c4d5e6f7a8b9c0d",
          "contract_address": "0xC5d563A36AE78145C45a50134d48A1A61A3A4Dc7",
          "chain_id": 137,
          "eip712_domain_version": "1",
          "nonce": "12345"
        }
      }
    ],
    "output": [
      {
        "label": "SecurityCheck \u2014 ALLOW (V2 address, valid schema)",
        "payload": {
          "check_id": "sec.contract_address_guard.20260509T100000Z",
          "scope": "contract",
          "decision": "ALLOW",
          "reason_code": null,
          "evidence": {
            "submitted_address": "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E",
            "chain_id": 137,
            "allow_list_version": "v2.2026-04-28",
            "allow_list_label": "CTFExchangeV2",
            "allow_list_match": true,
            "domain_version_ok": true,
            "v2_schema_ok": true,
            "alert_raised": false
          },
          "checked_at": "2026-05-09T10:00:00Z"
        }
      },
      {
        "label": "SecurityCheck \u2014 DENY (V1 address detected)",
        "payload": {
          "check_id": "sec.contract_address_guard.20260509T100500Z",
          "scope": "contract",
          "decision": "DENY",
          "reason_code": "CONTRACT_ADDRESS_NOT_ALLOWED",
          "evidence": {
            "submitted_address": "0xC5d563A36AE78145C45a50134d48A1A61A3A4Dc7",
            "chain_id": 137,
            "allow_list_version": "v2.2026-04-28",
            "allow_list_match": false,
            "v1_address_detected": true,
            "alert_raised": true
          },
          "checked_at": "2026-05-09T10:05:00Z"
        }
      }
    ],
    "curl": "curl 'https://clob.polymarket.com/markets/0x5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e'"
  },
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active.",
      "action": "Immediately return DENY with security alert.",
      "user_message": "Trading is currently paused."
    },
    {
      "code": "CONTRACT_ADDRESS_NOT_ALLOWED",
      "severity": "HARD_REJECT",
      "meaning": "Order targets a contract address not in the V2 allow-list, a V1 address, an empty allow-list, or has a domain separator mismatch.",
      "action": "Return DENY and emit security alert with full metadata.",
      "user_message": "This order was blocked because it targeted an unrecognised contract address."
    },
    {
      "code": "WALLET_PERMISSION_DENIED",
      "severity": "HARD_REJECT",
      "meaning": "The signing wallet does not have permission to submit to the target contract (chain_id or permissions mismatch).",
      "action": "Return DENY without proceeding to signing.",
      "user_message": "Your wallet does not have permission to interact with this contract."
    },
    {
      "code": "CONTRACT_GUARD_V1_DETECTED",
      "severity": "HARD_REJECT",
      "meaning": "Order carries a known V1 CTFExchange address; block_v1_signing is locked true.",
      "action": "Return DENY; emit security alert with submitted_address.",
      "user_message": "This order used an older contract version. It was blocked automatically."
    },
    {
      "code": "CONTRACT_GUARD_DOMAIN_MISMATCH",
      "severity": "HARD_REJECT",
      "meaning": "EIP-712 domain separator version is not '2' (e.g., still '1' from V1 SDK).",
      "action": "Return DENY; emit security alert with domain details.",
      "user_message": "The security parameters in this order did not match the current exchange. The order was blocked."
    },
    {
      "code": "CONTRACT_GUARD_V1_SCHEMA",
      "severity": "HARD_REJECT",
      "meaning": "Order contains V1-only fields (nonce, feeRateBps, taker) that must be absent in V2.",
      "action": "Return DENY; emit security alert.",
      "user_message": "This order contained outdated fields. Please update the SDK to V2."
    },
    {
      "code": "CONTRACT_GUARD_ALLOW_LIST_EMPTY",
      "severity": "HARD_REJECT",
      "meaning": "The V2 allow-list has not been configured; fail-closed default.",
      "action": "Return DENY; emit configuration alert.",
      "user_message": "The approved contract list has not been set up yet. No orders can be placed until it is configured."
    },
    {
      "code": "PARAMETER_CHANGE_REQUIRES_APPROVAL",
      "severity": "HARD_REJECT",
      "meaning": "An attempt was made to modify a locked parameter (block_v1_signing, require_domain_match, alert_on_block, or v2_addresses) without a signed admin action.",
      "action": "Reject the configuration change and emit an alert.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_sec_contractaddressguard_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code"
        ],
        "meaning": "Total SecurityCheck decisions by type."
      },
      {
        "name": "polytraders_sec_contractaddressguard_alerts_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "reason"
        ],
        "meaning": "Total security alerts emitted, broken down by reason code."
      },
      {
        "name": "polytraders_sec_contractaddressguard_v1_blocks_total",
        "type": "counter",
        "unit": "count",
        "labels": [],
        "meaning": "Count of orders blocked specifically because a V1 address was detected. Should be zero in normal V2 operation."
      },
      {
        "name": "polytraders_sec_contractaddressguard_allow_list_size",
        "type": "gauge",
        "unit": "count",
        "labels": [],
        "meaning": "Current number of addresses in the V2 allow-list; should be 3 (CTFExchangeV2, NegRiskAdapter, pUSD)."
      },
      {
        "name": "polytraders_sec_contractaddressguard_eval_latency_ms",
        "type": "histogram",
        "unit": "seconds",
        "labels": [],
        "meaning": "Wall-clock latency of the full address check."
      }
    ],
    "alerts": [
      {
        "name": "ContractAddressGuardBlock",
        "condition": "rate(polytraders_sec_contractaddressguard_alerts_total[5m]) > 0",
        "severity": "P0",
        "runbook": "#runbook-contractguard-block"
      },
      {
        "name": "ContractAddressGuardV1Detected",
        "condition": "rate(polytraders_sec_contractaddressguard_v1_blocks_total[5m]) > 0",
        "severity": "P0",
        "runbook": "#runbook-contractguard-v1"
      },
      {
        "name": "ContractAddressGuardAllowListEmpty",
        "condition": "polytraders_sec_contractaddressguard_allow_list_size == 0",
        "severity": "P0",
        "runbook": "#runbook-contractguard-allow-list"
      },
      {
        "name": "ContractAddressGuardHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_sec_contractaddressguard_eval_latency_ms_bucket[5m])) > 50",
        "severity": "P2",
        "runbook": "#runbook-contractguard-latency"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Security / ContractAddressGuard",
      "Grafana \u2014 V1 to V2 migration / address block history"
    ],
    "log_levels": {
      "DEBUG": "Each address check result including matched allow-list label.",
      "INFO": "ALLOW decisions with contract label. DENY decisions always at WARN or higher.",
      "WARN": "DENY decision emitted; security alert raised.",
      "ERROR": "Allow-list empty on startup; Admin UI unreachable; V1 address detected in live traffic."
    }
  },
  "state": {
    "summary": "Stateless per evaluation. The V2 allow-list is loaded from Admin UI configuration at startup and is immutable until a signed admin change.",
    "stores": [],
    "recovery": "On cold start, allow-list is loaded from Admin UI config. If config is unreachable, fail-closed: treat allow-list as empty and return DENY on all orders.",
    "on_restart": "Allow-list is re-loaded from Admin UI on startup. If the last known config is cached locally, that cache is used until Admin UI is reachable."
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 500,
    "idempotency_key": "intent_id",
    "replay_safe": true,
    "deduplication": "by intent_id within a 24h window",
    "ordering_guarantees": "no ordering \u2014 check is fully stateless",
    "timeout_ms": 20,
    "backpressure": "drop newest",
    "locking": "none"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "KillSwitch gate is checked before any address validation.",
        "contract": "DENY(KILL_SWITCH_ACTIVE) short-circuits the allow-list check."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.builder_attribution",
        "why": "Every SecurityCheck result is logged to the governance audit trail.",
        "contract": "GovernanceLog entry emitted on both ALLOW and DENY."
      },
      {
        "bot_id": "exec.smart_router",
        "why": "Only orders that pass ALLOW proceed to signing and SmartRouter execution.",
        "contract": "DENY prevents any ExecutionPlan from being constructed."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "On-chain Polygon RPC (read)",
        "endpoint": "Polygon RPC",
        "sla": "best-effort",
        "failure_mode": "Falls back to cached allow-list; if cache is empty, DENY."
      },
      {
        "service": "CLOB API (read)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.95% / 200ms p99",
        "failure_mode": "Order schema validation uses cached V2 spec if CLOB is unreachable."
      }
    ]
  },
  "security_surfaces": {
    "summary": "ContractAddressGuard is the primary defence against phishing, V1 replay, and malicious contract substitution. Every block emits a security alert. The allow-list is change-controlled.",
    "signing": "This bot does NOT sign anything. It runs before signing to prevent signing against unapproved contracts.",
    "secrets": [],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "method": "matchOrders(...)",
        "network": "polygon",
        "effect": "ContractAddressGuard validates that the target contract address matches CTFExchangeV2 before signing is permitted. It does not call matchOrders itself."
      },
      {
        "contract": "NegRiskAdapter",
        "method": "convertPosition(...)",
        "network": "polygon",
        "effect": "For neg-risk convert-arb routes, ContractAddressGuard validates the NegRiskAdapter address is in the allow-list."
      }
    ],
    "abuse_vectors": [
      "Injecting an unknown contract address into an order payload to redirect funds",
      "Replaying a V1 signed order by substituting the V1 CTFExchange address",
      "EIP-712 domain separator forgery to redirect signing to a different chain or contract",
      "Modifying the allow-list by bypassing the signed admin action requirement"
    ],
    "mitigations": [
      "V2 allow-list is hardcoded for Polygon chain_id=137; any other chain_id is rejected",
      "V1 deny-list check runs before the allow-list check",
      "EIP-712 domain version must exactly equal '2' \u2014 mismatches are blocked and alerted",
      "V1 schema fields (nonce, feeRateBps, taker) trigger immediate DENY",
      "Every DENY emits a security alert regardless of alert_on_block setting (it is locked true)"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "@polymarket/clob-client-v2 ^2.x",
    "settlement_contract": "CTFExchangeV2 on Polygon",
    "notes": "This bot is the hardest V1\u2192V2 migration control point. Allow-list now contains CTFExchangeV2, NegRiskAdapter, and pUSD ERC-20. The old V1 CTFExchange address is on the deny-list. EIP-712 Exchange domain version must be '2'; ClobAuth domain version remains '1' and is not checked here. V2 order fields: timestamp(ms) + metadata(bytes32) + builder(bytes32); nonce/feeRateBps/taker must be absent."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.3",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e + HMAC builder)",
      "to": "v2 (pUSD + builderCode field)",
      "reason": "Polymarket V2 cutover",
      "action_taken": "Added CTFExchangeV2, NegRiskAdapter, and pUSD ERC-20 to the allow-list. Added old V1 CTFExchange to the deny-list. EIP-712 domain version check updated from '1' to '2'. Added V1 schema field detection (nonce, feeRateBps, taker). HMAC builder replaced with on-order builderCode bytes32 field."
    }
  ],
  "failure_injection": [
    {
      "scenario": "V1_ADDRESS_IN_ORDER",
      "how_to_inject": "Submit a pendingOrder with contract_address = V1 CTFExchange address",
      "expected_behavior": "Immediate DENY(CONTRACT_ADDRESS_NOT_ALLOWED) + security alert; no signing occurs",
      "recovery": "Automatic on next valid order."
    },
    {
      "scenario": "DOMAIN_VERSION_MISMATCH",
      "how_to_inject": "Set pendingOrder.eip712_domain_version = '1'",
      "expected_behavior": "DENY(CONTRACT_ADDRESS_NOT_ALLOWED) + security alert",
      "recovery": "Automatic on next order with correct domain version."
    },
    {
      "scenario": "ALLOW_LIST_EMPTY",
      "how_to_inject": "Start bot with empty v2_addresses config",
      "expected_behavior": "DENY on all orders + configuration alert; no orders proceed to signing",
      "recovery": "Admin must populate allow-list via signed admin action."
    },
    {
      "scenario": "V1_SCHEMA_FIELDS",
      "how_to_inject": "Include nonce or feeRateBps field in pendingOrder",
      "expected_behavior": "DENY(CONTRACT_ADDRESS_NOT_ALLOWED) + security alert",
      "recovery": "Automatic on next order without V1 fields."
    },
    {
      "scenario": "WRONG_CHAIN_ID",
      "how_to_inject": "Set pendingOrder.chain_id = 1 (Ethereum mainnet)",
      "expected_behavior": "DENY(CONTRACT_ADDRESS_NOT_ALLOWED) \u2014 no Polygon allow-list entry matches chain_id=1",
      "recovery": "Automatic on next order with chain_id=137."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behavior": "DENY(KILL_SWITCH_ACTIVE) + security alert without allow-list check",
      "recovery": "Manual KillSwitch reset."
    }
  ],
  "runbook": {
    "summary": "Every ContractAddressGuard block is a P0 security event. On-call must treat every block as a potential attack until proven otherwise.",
    "oncall_actions": [
      {
        "alert": "ContractAddressGuardBlock",
        "first_step": "Examine the security alert metadata: submitted_address, reason, and intent_id.",
        "diagnosis": "If submitted_address matches a known V1 address, the client SDK has not been updated to V2. If it is unknown, treat as potential phishing.",
        "mitigation": "Block the offending client or strategy. Do not modify the allow-list without a security review.",
        "escalation": "Security pod lead immediately on any DENY alert."
      },
      {
        "alert": "ContractAddressGuardV1Detected",
        "first_step": "Identify which strategy submitted the V1-addressed order.",
        "diagnosis": "Strategy is using an outdated SDK or configuration.",
        "mitigation": "Pause the strategy and require SDK upgrade to @polymarket/clob-client-v2.",
        "escalation": "Security pod lead + Risk pod lead."
      },
      {
        "alert": "ContractAddressGuardAllowListEmpty",
        "first_step": "Check Admin UI configuration for v2_addresses.",
        "diagnosis": "Allow-list was not populated after deployment or a config reset.",
        "mitigation": "Populate the allow-list with the three V2 addresses via signed admin action. All trading is blocked until this is done.",
        "escalation": "Security pod lead immediately."
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders admin add-address sec.contract_address_guard --address <addr> --chain-id 137",
        "effect": "Adds an address to the V2 allow-list. Requires signed admin action and is audit-logged."
      },
      {
        "command": "polytraders bot status sec.contract_address_guard",
        "effect": "Prints current allow-list, deny-list, and last block event."
      }
    ],
    "healthcheck": "GET /health \u2192 200 if allow-list contains at least 3 addresses and no DENY alert has fired in the last 60s."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for all address check paths including V1 deny-list",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Allow-list populated with correct V2 addresses in staging",
        "how_measured": "Manual config check",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Zero security alerts in shadow mode over 48h of synthetic traffic",
        "how_measured": "Grafana ContractAddressGuardBlock alert history",
        "threshold": "0 alerts"
      },
      {
        "gate": "V1 address injection test fires DENY + alert correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ],
    "to_general_live": [
      {
        "gate": "Domain version mismatch test fires DENY + alert correctly",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      },
      {
        "gate": "Allow-list-empty fail-closed test verified: all orders blocked with no signing",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ]
  },
  "reporting_groups": [
    "risk_compliance",
    "governance_audit"
  ],
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 5,
    "phase_name": "Execution rails",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}