Skip to main content
Tokenized stocks are securities, and securities carry obligations that payment stablecoins do not: only eligible, identity-verified investors may hold them, transfers must respect restrictions, and the market they trade in must be surveilled for abuse. The ERC-3643 standard builds identity and compliance into the token itself, and every check it performs emits an onchain event. This guide covers both halves of securities compliance: transfer-level compliance (identity, eligibility, freezes) and market surveillance (wash trading, best execution). It is the securities counterpart to the payments-focused AML monitoring guide.

Part A - transfer & identity compliance

ERC-3643 tokens enforce rules through an identity registry and a compliance contract, and they can freeze holdings or force recovery. Stream those events to keep a real-time compliance picture.

Stream identity and freeze events

erc3643-compliance.yaml
name: erc3643-compliance
resource_size: s

sources:
  security_logs:
    type: dataset
    dataset_name: ethereum.raw_logs
    version: 1.2.0
    start_at: latest
    filter: address = lower('0xTOKENIZED_SECURITY')

transforms:
  events:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        address AS token,
        _gs_log_decode(
          _gs_fetch_abi('https://raw.githubusercontent.com/your-org/abis/main/erc3643.json', 'raw'),
          topics,
          data
        ) AS decoded,
        block_timestamp,
        transaction_hash,
        _gs_op
      FROM security_logs

  compliance_events:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        token,
        decoded.event_signature AS event,
        decoded.event_params[1] AS subject,
        to_timestamp(block_timestamp) AS at,
        transaction_hash,
        _gs_op
      FROM events
      WHERE decoded IS NOT NULL
        AND decoded.event_signature IN (
          'IdentityRegistered', 'IdentityRemoved',
          'AddressFrozen', 'TokensFrozen', 'TokensUnfrozen',
          'RecoverySuccess', 'Paused', 'Unpaused'
        )

sinks:
  compliance_ledger:
    type: postgres
    from: compliance_events
    schema: compliance
    table: security_events
    secret_name: MY_POSTGRES
    primary_key: id
This gives compliance a live feed of every registration, freeze, and recovery - the events an examiner will ask you to produce.

Attest eligibility decisions with Compose

When a new holder must be onboarded or a transfer needs an eligibility check, route it to a Compose task that verifies against your identity provider and records the outcome in a TEE. The pattern is identical to the AML screening task: fetch the verification, write an attestation on-chain, and keep the full trace. That attestation is what makes an eligibility decision defensible.

Part B - market surveillance

Tokenized equities trade 24/7 on open venues, which means the market-integrity checks a broker-dealer runs - wash trading, spoofing, best execution - now run against public onchain data in real time.

Detect wash trades

A wash trade is a sale with no change in beneficial ownership - the same owner on both sides, usually across two different addresses. The operative word is same: it is not enough for both parties to be known investors. You have to resolve each address to its beneficial owner and check that the two owners match.
A membership test like dynamic_table_check('watchlist', sender) AND dynamic_table_check('watchlist', recipient) is wrong here - it fires on any trade between two listed investors, not just self-trades, flooding surveillance with false positives. Dynamic tables answer “is this address known?”, not “do these two addresses share an owner?”
Resolve both counterparties to a beneficial-owner ID with an HTTP handler that calls your identity/KYC system, then flag trades where the two owners are equal.
wash-trade-surveillance.yaml
name: wash-trade-surveillance
resource_size: s

sources:
  trades:
    type: dataset
    dataset_name: robinhood_testnet.erc20_transfers
    version: 1.0.0
    start_at: latest

transforms:
  # Resolve each side to its beneficial owner via your identity service.
  # The endpoint echoes each row and adds sender_owner + recipient_owner.
  with_owners:
    type: handler
    from: trades
    url: https://identity.example.com/resolve-beneficial-owners
    primary_key: id
    secret_name: IDENTITY_API_SECRET
    schema_override:
      sender_owner: Utf8
      recipient_owner: Utf8

  # Self-trade: both addresses resolve to the SAME (non-null) owner
  suspected_wash:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        address AS token,
        sender,
        recipient,
        sender_owner AS beneficial_owner,
        amount,
        block_timestamp,
        transaction_hash,
        _gs_op
      FROM with_owners
      WHERE sender_owner IS NOT NULL
        AND sender_owner = recipient_owner

sinks:
  surveillance_alerts:
    type: webhook
    from: suspected_wash
    url: https://surveillance.example.com/alerts/wash-trade
    one_row_per_request: true
    secret_name: SURVEILLANCE_WEBHOOK_AUTH
Your resolver’s ownership graph is the source of truth and is queried live, so linking new addresses to an owner takes effect on the next trade with no redeploy.
To cut volume, add a SQL pre-filter before the handler that drops trades where neither side is a known investor - a dynamic table membership check is fine for that. Just don’t mistake membership for detection; the same-owner comparison is what identifies a wash trade. If you maintain the address→owner map as a table, you can equivalently detect downstream with a Postgres self-join: join trades to the mapping on both sender and recipient and keep rows where the owner IDs match.

Best execution

Compare each fill to the prevailing market price at the moment of the trade. Join your trade stream to the VWAP price feed from the tokenized-equities data layer and flag fills that executed materially outside the spread - the raw material for a best-execution report.

Attest and report with Compose

A surveillance alert becomes a case, and a case becomes a filing. A Compose task enriches each alert, opens a case in your surveillance system, and - where required - files a regulatory report, with every step traced and TEE-attested. Durable execution means a required report is never dropped because a downstream API blipped.

Business outcomes

  • Real-time, not T+1, surveillance - abuse is flagged as it settles.
  • Provable eligibility and freezes - a live compliance ledger plus TEE-attested decisions answer “who was allowed to hold this, and why?”
  • One widening net - the identity graph updates a running pipeline in seconds.
  • Data stays in your boundary - compliance events and identity data live in your own database.

Resources

Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.