oak_techniques: [OAK-T16.004]
spec_id: oak-detection-T16.004
version: 0.1.0
maturity: draft
maintainer: "@iZonex"
license: Apache-2.0

scope: |
  Detect off-chain Snapshot-class voting attacks where the load-bearing
  surface is signature aggregation rather than on-chain economic
  finality. Four sub-shapes: Sybil-cluster voting; signature replay
  across votes (where signing scheme lacks proposalId / nonce
  binding); operator-side commitment-vs-execution divergence (multisig
  refuses or executes a disputed off-chain result); voting-weight-not-
  anchored-to-economic-stake. Excludes: T16.001/.002/.003 (on-chain
  governance, contractual finality); T11 (custody compromise — adjacent
  when operator multisig is the load-bearing surface); legitimate
  non-binding signal voting where the protocol does not commit to
  honour the result.

data_sources: [snapshot_vote_log, signature_payload_log,
               operator_multisig_actions, voter_eligibility_set,
               funder_graph]

detection_logic:
  description: |
    Four orthogonal paths matching the four sub-shapes. PATH A (Sybil
    cluster): the off-chain vote signer-set overlaps a single funder-
    graph cluster above sybil_share. PATH B (signature replay): the
    platform's signing scheme lacks proposalId / per-vote nonce
    binding (design-review path). PATH C (operator-binding mismatch):
    operator multisig executes (or refuses to execute) the off-chain
    result without disclosed commitment policy or with publicly-
    disputed outcome. PATH D (snapshot-block manipulation): voting-
    weight snapshot block is single-block / non-randomised / pre-
    announced and the cohort accumulates inside the snapshot window.
  pseudocode: |
    # PATH A — Sybil-cluster overlap with off-chain voter set
    for each off-chain vote V:
      voters ← V.signers
      cluster ← funder_graph_cluster(voters, hops = trace_hops)
      cluster_share ← Σ weight_of(v) for v in voters if v ∈ cluster
                       / total_voted_weight(V)
      eligibility_growth ← voter_eligibility_set_growth(
                             V.proposal, window = pre_close_window)
      if cluster_share > sybil_share or eligibility_growth > eligibility_growth_threshold:
        emit(PATH_A, vote=V, cluster_share, eligibility_growth,
             severity=critical)

    # PATH B — signature scheme audit (design-review)
    for each off-chain platform P:
      scheme ← signature_scheme(P)
      if not scheme.binds_proposal_id or not scheme.has_per_vote_nonce:
        emit(PATH_B, platform=P, missing=scheme.missing_fields,
             severity=high)

    # PATH C — operator-binding mismatch
    for each off-chain vote V with operator-side commitment to honour:
      multisig_action ← operator_multisig_action_for(V,
                                                      window = post_close_window)
      if multisig_action == None and V.protocol.commitment_to_honour:
        emit(PATH_C, vote=V, mode="operator-refused",
             severity=high)
      elif multisig_action ≠ None
           and disputed_publicly(V, sources = ["forum", "x", "discord"]):
        emit(PATH_C, vote=V, mode="executed-disputed",
             multisig_action, severity=critical)

    # PATH D — snapshot-block manipulation
    for each off-chain vote V:
      snap_kind ← snapshot_block_method(V.protocol)
      if snap_kind ∈ {single_block, pre_announced}:
        accum ← voter_accumulation_in(snapshot_block_window(V),
                                        cohort = funder_graph_cluster(V.signers,
                                                                       hops = trace_hops))
        if accum > accumulation_threshold:
          emit(PATH_D, vote=V, snapshot_method=snap_kind,
               accumulation_usd=accum, severity=high)

parameters:
  trace_hops:                     { type: integer, default: 3 }
  sybil_share:                    { type: number,  default: 0.30 }     # 30% of voted weight
  pre_close_window:               { type: duration, default: 24h }
  eligibility_growth_threshold:   { type: number,  default: 0.5 }       # 50% growth in window
  post_close_window:              { type: duration, default: 14d }
  accumulation_threshold:         { type: number,  default: 100000 }    # $100k

output_alert: [oak_technique, detection_path, severity, platform,
               protocol, vote_id, cluster_share, missing_fields,
               multisig_action, snapshot_method, evidence]

test_fixtures:
  positive: []                      # no canonical extraction-scale anchor at v0.1; class is draft
  negative:
    - "Snapshot vote with EIP-712 binding + multi-block snapshot + post-close eligibility lock"
    - "Non-binding signal vote with no operator commitment to honour"
    - "Voter set with no funder-cluster overlap above sybil_share"

false_positive_modes:
  - legitimate large-holder participation that happens to share an exchange-cluster funder (allowlist-suppress shared CEX clusters)
  - eligibility-set growth from a marketing campaign or airdrop without coordinated voting (require funder-cluster overlap)
  - operator-multisig refusal for legitimate compliance reasons (annotate compliance-overrides as a distinct outcome)
  - PATH D false positives where snapshot block is single but volume around it is organic — combine with funder-cluster check

mitigations: [OAK-M02, OAK-M16]

reference_implementations:
  - { target: snapshot-platform,     chain: chain-agnostic, url: "" }
  - { target: tally-off-chain,       chain: evm,             url: "" }
  - { target: boardroom-off-chain,   chain: evm,             url: "" }
  - { target: dune,                  chain: evm,             url: "" }
