Offline pre-signing intent analysis for Ethereum, and a self-hosted co-signer callback handler built on it. Python >= 3.11, MIT licence.
This repository is extracted from a larger private toolkit (contractscan). It contains the pre-signing layer and the co-signer plugin only.
- Intent analysis (
contractscan_guard.drainers,contractscan_guard.guard): decodes what a transaction or a signature request actually authorises --approve,permit, Permit2,setApprovalForAll, transfers, multicall, Seaport orders, EIP-7702 authorisations, EIP-712 (Permit, Permit2, Seaport, SafeTx), SIWE andeth_sign-- and returns findings together with explicitchecked/not_checkedlists. It never says "safe". Address-poisoning checks (lookalike addresses against an address book) and an offline threat-feed loader are included. - Co-signer plugin (
contractscan_guard.cosigner): a policy engine for institutional signing hooks. It implements the Fireblocks API Co-Signer callback (/v2/tx_sign_request, JWT RS256 both ways) and answersAPPROVE,REJECTorRETRY(escalation to a human) with reasons. It unfolds SafeexecTransaction(including delegatecall), MultiSend, Multicall3, Safe administration (owners, threshold, modules, guards, fallback handler, singleton changes), ownership transfers, proxy upgrades, role grants and EIP-7702, and cross-checks the signed digest,rawTx,contractCallDataanddestAddressagainst each other. It hasshadowandenforcemodes, a JSONL decision log and an override file for answering escalations. Utila and Cordial adapters are sketches only: their request schemas are not public.
Offline by design. The decision path makes no network call: everything the engine knows about
the world (address book, code snapshot, block lists, feed snapshots, delegatecall and 7702
allowlists) is loaded from files at start-up. tests/cosigner/test_offline.py replaces every
socket entry point with an exception and runs all request shapes through the engine.
It is not a simulator (nothing is executed) and not a reputation service (it reads only what the operator put on disk). Details, the Fireblocks callback contract as documented publicly, the default policy and known limits: docs/cosigner-plugin.md.
python -m pip install -e ".[dev]"
contractscan-guard cosigner keygen --out keys/ # handler key pair (RSA 2048)
contractscan-guard cosigner serve -c examples/cosigner.example.toml # mode = "shadow"
contractscan-guard cosigner check request.json --json # one offline decision
contractscan-guard cosigner approve <txId> -o cosigner-overrides.json # answer an escalation
contractscan-guard feeds sync # optional: threat feeds into a local cacheAs a library:
from contractscan_guard.cosigner import Engine, Knowledge
engine = Engine(knowledge=Knowledge())
record = engine.decide_fireblocks(tx_sign_request_body)
print(record.decision, record.reasons)python -m pytest225 tests, no network (local mock servers only).
The benchmark datasets are not committed. The collectors rebuild them from public sources and cache every HTTP response, so reruns are deterministic and offline:
# Real Safe treasury flow and incidents: Safe Transaction Service, public RPC, post-mortems
python analysis/cosigner_real_collect.py # network; writes analysis/cosigner-real/
python analysis/cosigner_real_benchmark.py # offline replay
# Retail approvals corpus: Etherscan phish-hack labels (forta-network/labelled-datasets),
# on-chain Approval/Permit events, polkadot-js phishing list, DefiLlama
python analysis/guard_corpus_collect.py # network; writes analysis/guard-benchmark/collected.json
python analysis/guard_benchmark.py build # hand-built cases -> corpus.json
python analysis/guard_benchmark.py run
python analysis/cosigner_benchmark.py # co-signer over both corporaThe sources of every dataset, their licences and the selection rules are documented in the
collectors' docstrings. Two small reference tables are committed because the tests check the
built-in constants against them: analysis/cosigner-real/safe-deployments.json (the
safe-global/safe-deployments registry) and analysis/cosigner-real/fireblocks-assets.json
(Fireblocks' public "Supported Networks" table matched to chainid.network).
Enforce mode, default policy, nothing tuned on these numbers, 95% Wilson intervals.
- Retail approvals corpus, frozen held-out half (262 legitimate, 306 malicious): with a code snapshot, legitimate REJECT 0% (0-2%), malicious REJECT 83% (78-87%); with no knowledge at all, legitimate RETRY 36% (31-42%) and no REJECT.
- Real treasury flow: 25 public DAO treasury Safes of 6 organisations, 4,393 executed
transactions replayed as both owner confirmations and executions. With a code snapshot and an
address book seeded from each Safe's earlier history: REJECT 0.5% (0.3-0.7%), RETRY 5.3%
(4.6-6.0%). All 21 REJECTs were reviewed by hand and are policy hits (
enableModuleof a new module,removeOwnerdown to threshold 1, delegatecall to unverified code). - Real incidents (malicious SafeTxs identified by hash,
safeTxHashreproduced): Bybit REJECT, WazirX REJECT, Radiant RETRY. - Latency: engine decision p50 about 0.1 ms; full HTTP round trip with JWT RS256 both ways p50 about 6-10 ms, p99 about 30-55 ms on a desktop CPU.
Caveats: DAO treasury flow is not a custodian's flow; the code snapshot is the current one, not historical; transactions are clustered by Safe, so the intervals are optimistic; EIP-7702 and claimed-vs-raw mismatches are covered only by constructed scenarios. A specific operator's escalation rate can only be measured in a shadow pilot.
MIT, see LICENSE. The bundled OFAC SDN address list is US government public data.