Evidence
Attribution & evidence
Each dimension is its own claim with its own evidence. Knowing one never settles another.
An EntryPoint transaction proves authorisation and execution on this chain very precisely. Identity, intent, off-chain billing and other chains sit outside it. So every attribution is a separate claim, in one dimension, with its own evidence.
Attribution dimensions and their traps
The central rule from the earlier research: each dimension is a separate claim with its own evidence. A strong answer on one does not lift the others. "Unknown" is a valid, honest value.
| Dimension | What answers it | What it does not prove |
|---|---|---|
| Operation (requested) | Decoded op.callData → target + selector + args, exact ABI re-encode | That it happened. A selector can collide (fakeApprove_6350575210(address,address) also fits 0x095ea7b3). A request is not a result. |
| Outcome (realized) | UserOperationEvent.success and the target's own logs (e.g. a Transfer matching the requested amount and recipient) |
Outer status 1 says nothing about ops. success = true can hide a caught sub-call failure. A matching Transfer is not "economic purpose". |
| Application | Exact address in a project-owned registry/docs (e.g. LI.FI 0x1231deb6f5749ef6ce6943a275a1d3e7486f4eae evidence level: primary source in LI.FI's official deployments; Relay Depository in Relay's chains API) |
Token names, decompiler names and signature-DB hits are clues. "EtherArcade" from a name() getter does not make a game, a project or an issuer. |
| Assets | Exact token address with an identity source (official USDC/USDt) | Asset identity says nothing about who or why. Bridged USDt 0xc7198437…5118 evidence level: primary source is a different asset from native USDt. |
| Routes | Decoded swap/bridge request fields (e.g. LI.FI destination chain IDs 1, 10, 56); source-chain events | A requested destination is not an arrival. Arrival needs destination-chain evidence. |
| Wallet implementation | Factory/implementation/module address bound to vendor docs (Kernel factory, Safe + Candide module 0x22939e83…aead evidence level: primary source) |
The wallet brand is not the app the user is using, and not the user. |
| Control / signers | Signature recovery under the correct, version-specific schema; owner getters at a block | Recovered key ≠ a person. A getter at block X ≠ continuous control. Same signer across accounts = one key, not necessarily one human. |
| Bundlers | tx.from |
Bundler ≠ user ≠ app. A 0x4337… vanity prefix suggests bundler software but doesn't identify the operator. |
| Beneficiaries | handleOps last argument |
Where compensation went; not the end user, not profit. |
| Sponsorship | Nonzero paymaster in op + topic3; paymaster's own events (mode, token amount) | Not "free to user", not "the app paid". Off-chain billing is invisible. |
| Deployment | AccountDeployed |
A new account isn't a new person (one script can deploy 75). In v0.9, non-empty initCode can be ignored, so count events, not initCode. |
| Automation | Precise repeated patterns + recovered signer + nonce scheme, ideally plus an exact operator/script binding | Regular timing, sponsorship or repetition alone don't prove a bot. A burst supports a coordination hypothesis. |
| Fees | Receipt fee per outer (once); actualGasCost per op (separate layer) | Fees by target/leg overlap and aren't additive. |
The habit to build: for any label, ask "which of these columns does my evidence actually sit in?"
Evidence grades: how a value was obtained
Strongest first. Each fact on an example page carries one.
- grade: observed read directly from the receipt, logs or transaction
- grade: decoded decoded from calldata and re-encoded byte-for-byte
- grade: recomputed independently recomputed (e.g. the userOpHash)
- grade: recovered recovered cryptographically (signature recovery)
- grade: contract read read from a contract getter at a stated block
- grade: registry taken from the address registry at the stated level
- grade: inferred derived from other facts (e.g. log order); weaker than an observation
- grade: hint a clue only
- grade: unknown not established
Evidence levels: how strong an address label is
| level | meaning | example |
|---|---|---|
| evidence level: primary source | The protocol/vendor itself publishes this exact address (official docs, official GitHub deployment file / constants). URL is in source. A same-address deterministic (CREATE2) deployment listed for other chains qualifies only when the address matches exactly; the builder then also compares runtime code hashes with Ethereum mainnet and records the result in notes. |
EntryPoints, Safe singletons, Pimlico singleton paymasters |
| evidence level: self-description | What the contract says about itself through a getter at a stated block (name(), eip712Domain(), VERSION()). Contracts can claim any name. |
Signed result registry eip712Domain().name == "Avax-dle" |
| evidence level: cluster | Shared key / getter / bytecode with other labelled addresses: same recovered signer, same owner(), identical runtime code hash, consistent co-occurrence. Shows "same machinery", not the same operator or a named organization. |
Safe self-call cluster B and its sole owner key |
| evidence level: hint | Ranking position, deployer lineage, explorer name tags, metadata names. Never used as a heading or as proof. | "Unattributed bundler #7 (EOA)" |
A name is never a real-world identity. EOAs (bundlers, signers, owners) are keys; we never infer who controls them.
Carry-forward rules, in plain language
- Keep every dimension separate: what was requested, what actually happened, which app, which asset, which route, which wallet software, which key, who bundled, who sponsored. A strong answer on one never settles another.
- Outer success does not prove inner success; a successful UserOperation can still hide a caught failure in one of its calls.
- Count the outer fee exactly once per transaction. The per-operation charge (actualGasCost) is a different layer and is never added to it, and fees are never split across legs and summed.
- Sponsored is not free: a paymaster paying the gas says nothing about who paid the paymaster.
- A signer is a key. Signature recovery proves control of a key, never a person’s identity.
- A third-party label (explorer tag, dashboard, another vendor’s list) is a hint and can never raise an evidence level.
- A claim is only as strong as its weakest link: a log that is observed but tied to an operation by log order is “inferred”.
- Capability is not usage: a version supporting a feature (EIP-7702, block-number validity) does not mean anyone used it.
- “Unknown” is a valid, honest answer. Leave it unknown until the evidence exists.
The ontology
The rules above are also written down as a small formal vocabulary (OWL + SKOS + PROV-O in Turtle) with SHACL shapes that reject data breaking them. Download: epi-ontology.ttl · epi-shapes.ttl.
1. Four ideas
1. Chain artifacts are the skeleton. An OuterTransaction (in a Block) carries one or more Envelopes (a handleOps call; DirectEnvelope when tx.to is the EntryPoint, EmbeddedEnvelope when found inside a wrapper's calldata). An envelope holds UserOperations. A UserOperation has Legs: the calls the account was asked to make. A ContainerLeg (e.g. Safe MultiSend) has sub-legs. The transaction has Logs, each placed in a lifecycle phase (validation, execution, compensation ...). These are facts read or decoded from the chain.
2. Addresses play roles; they are not roles. An address is a ChainAddress: an EOA or a Contract. Some contract kinds follow from protocol semantics (EntryPoint, SmartAccount, Paymaster, Factory, AccountImplementation, Module/Validator, Token, WrapperContract). What an address did in a given transaction is a RoleAssignment: player + role + context. The roles are bundler, beneficiary, entrypoint, sender, factory, paymaster, signer, aggregator, account implementation, validator module, leg target, wrapper and token-payment recipient. The same address can play several roles at once. A bundler is usually also the beneficiary, and in one v0.8 bundle the beneficiary is the paymaster. That is a coincidence of addresses, not an identity. epio:Bundler and epio:Beneficiary are defined classes: "any address that played that role somewhere".
Signers are different: the signer role is played by a SignerKey, never by an address or a person. An EOAKey (secp256k1) controlsAddress exactly one EOA. A PasskeyP256 (WebAuthn) has a public key (x, y) and no address.
3. Every attribution is a claim, in one independent dimension. An epio:Claim has a claimSubject, a dimension, a claimPredicate and claimValue, an evidenceGrade, an evidenceLevel for labels, sourceLocator (URL, file or field), method, qualification (caveats), observedAtBlock and prov:wasDerivedFrom / prov:generatedAtTime. The dimensions (epio:DimensionScheme) are the ones in the research notes: operation, outcome (outer status / UserOp success / leg evidence as three separate sub-dimensions), application, asset, route, wallet implementation, control, bundling, compensation, sponsorship, deployment, automation, fees (receipt fee / actualGasCost as two separate sub-dimensions) and labelling. Knowing one dimension never settles another. The paymaster being Pimlico tells you nothing about who runs the bundler, and "unknown application" does not erase a well-evidenced asset transfer.
4. Evidence has two orthogonal scales.
- Grade: how a value was obtained (
epio:EvidenceGradeScheme, fromepi/attribution/claims.py):observed>decoded_canonical>recomputed>recovered>endpoint_getter>registry>inferred>hint>unknown. - Level: how strong an address label is (
epio:EvidenceLevelScheme, fromepi/registry.py):primary_source>self_description>cluster>hint, withepio:rank1–4 and the transitiveepio:strongerThan.
The registry's protocol field becomes one of three distinct relations so that a weak claim can't pass for a strong one:
| relation | meaning | level |
|---|---|---|
epio:publishedBy |
the organisation itself publishes this exact address | primary_source only (enforced) |
epio:sharesMachineryWith |
same owner key / implementation / bytecode as that organisation's contracts: same machinery, not same operator | cluster (or weaker) |
epio:thirdPartyAttributedTo |
Arkham, BundleBear, 4337-stats, Dune or another vendor says so | always hint (enforced) |
Third-party sources (epio:Arkham, epio:Snowtrace, epio:BundleBear, epio:Stats4337, epio:DuneSpellbook, epio:OtherVendorPublication) are typed epio:ThirdPartySource. Any claim prov:wasDerivedFrom one of them must be level hint, so a third party can never raise a level. Each one becomes an epio:ThirdPartyClaim, carrying the explorer URL, the verbatim tag and the fetch date (2026-10-19).
2. Core class diagram
This diagram is wider than the screen: scroll sideways inside its frame.
classDiagram
direction TB
class OuterTransaction { txHash; gasUsed }
class Envelope { handleFunction; canonical }
class DirectEnvelope
class EmbeddedEnvelope
class UserOperation { userOpHash; versionLabel; nonceKey }
class Leg { legForm; selector; valueWei }
class Log { logKind; index }
class ChainAddress { address }
class EOA
class Contract
class EntryPoint
class SmartAccount
class Paymaster
class Factory
class AccountImplementation
class SignerKey
class EOAKey
class PasskeyP256 { publicKeyX; publicKeyY }
class Organization
class Claim { sourceLocator; method; qualification; observedAtBlock }
class RoleAssignment
class LabelClaim
class ThirdPartyClaim
class FeeObservation { amountWei }
class ReceiptFee
class ActualGasCharge
class Source
class ThirdPartySource
OuterTransaction "1" --> "1..*" Envelope : hasEnvelope
Envelope <|-- DirectEnvelope
Envelope <|-- EmbeddedEnvelope
Envelope --> EntryPoint : envelopeEntryPoint
Envelope "1" --> "1..*" UserOperation : hasUserOperation
UserOperation "1" --> "*" Leg : hasLeg
OuterTransaction --> Log : hasLog
ChainAddress <|-- EOA
ChainAddress <|-- Contract
Contract <|-- EntryPoint
Contract <|-- SmartAccount
Contract <|-- Paymaster
Contract <|-- Factory
Contract <|-- AccountImplementation
SignerKey <|-- EOAKey
SignerKey <|-- PasskeyP256
EOAKey --> EOA : controlsAddress
Claim <|-- RoleAssignment
Claim <|-- LabelClaim
Claim <|-- ThirdPartyClaim
Claim <|-- FeeObservation
FeeObservation <|-- ReceiptFee
FeeObservation <|-- ActualGasCharge
RoleAssignment --> ChainAddress : player (or SignerKey)
RoleAssignment --> UserOperation : inContext (or Leg)
ReceiptFee --> OuterTransaction : feeOf (exactly one)
ActualGasCharge --> UserOperation : feeOf
Claim --> Source : prov wasDerivedFrom
Source <|-- ThirdPartySource
LabelClaim --> Organization : publishedBy / sharesMachineryWith
ThirdPartyClaim --> Organization : thirdPartyAttributedTo (hint)
Every Claim also points to a dimension, a grade and (for labels) a level, which are SKOS concepts. They are left out of the diagram for space.
4. What SHACL enforces (epi-shapes.ttl)
| rule (research notes / registry contract) | shape |
|---|---|
| Every claim has one subject, one dimension, one grade (from the schemes), provenance | ClaimShape, LevelInSchemeShape |
| A label is never shown without its level | RegistryGradeNeedsLevel |
primary_source needs an http(s) URL |
PrimarySourceNeedsURL |
| Third parties never raise a level | ThirdPartyIsHint, ThirdPartyClaimShape |
| "Published by" only at primary_source | PublishedByIsPrimary |
| A key/EOA claim may not assert a Person | KeyIsNotPerson |
| Passkeys have no address; an EOA key has exactly one | PasskeyShape, EOAKeyShape |
| The signer role is played by a key | SignerRoleShape |
A role has one type and one context, and a player unless the grade is unknown |
RoleAssignmentShape |
| A paymaster role or sponsorship mode does not imply "free to the user" | PaymasterNotFree |
| One receipt fee per outer tx; actualGasCost belongs to an op; a fee is in exactly one layer | OuterFeeOnce, ReceiptFeeShape, ActualGasChargeShape, NoSummedFees |
| Outer status / UserOp success / leg outcome stay on their own artifact | OutcomeLayers |
| Addresses are lowercase 40-hex; EOA and Contract are disjoint; skeleton cardinalities | AddressShape, OuterTransactionShape, UserOperationShape, EnvelopeShape, LegShape |
Result on real data: registry + 7 showcases = 20,570 triples (with the ontology), 1,497 claims, 0 violations. With --all, every explain record (17 transactions, 1,974 claims) also conforms. tests/test_ontology.py adds 14 deliberately bad fixtures, one per rule. Each one fails with the expected message.
Competency questions, answered over the example data
CQ1: Which vendors' infrastructure touched this UserOperation (tx 0x0c9f5c6d..., v0.6 Pimlico ERC20 paymaster + Safe), through which role, and at what evidence level?
ontology/queries/cq1-vendors-touching-userop.rq - 10 row(s)
| role | address | organization | relation | level | source |
|---|---|---|---|---|---|
| account_impl | 0x29fcb43b46531bca003ddc8fcb67ffe91900c762 evidence level: primary source | Safe | publishedBy | primary_source | https://github.com/safe-global/safe-deployments/blob/main/src/assets/v1.4.1/safe_l2.json |
| entrypoint | 0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789 evidence level: primary source | eth-infinitism | publishedBy | primary_source | https://github.com/eth-infinitism/account-abstraction/releases/tag/v0.6.0 |
| module | 0xa581c4a4db7175302464ff3c06380bc3270b4037 evidence level: primary source | Safe | publishedBy | primary_source | https://github.com/safe-global/safe-modules-deployments/blob/main/src/assets/safe-4337-module/v0.2.0/safe-4337-module.json |
| paymaster | 0x6666666666667849c56f2850848ce1c4da65c68b evidence level: primary source | Pimlico | publishedBy | primary_source | https://raw.githubusercontent.com/pimlicolabs/docs/main/docs/pages/references/paymaster/erc20-paymaster/contract-addresses.mdx |
| target | 0x9641d764fc13c8b624c04430c7356c1c7c8102e2 evidence level: primary source | Safe | publishedBy | primary_source | https://github.com/safe-global/safe-deployments/blob/main/src/assets/v1.4.1/multi_send_call_only.json |
| target | 0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7 evidence level: primary source | Tether | publishedBy | primary_source | https://tether.to/en/supported-protocols/ |
| beneficiary | 0x4337001fff419768e088ce247456c1b892888084 evidence level: hint | Pimlico | thirdPartyAttributedTo | hint | BundleBear |
| beneficiary | 0x4337001fff419768e088ce247456c1b892888084 evidence level: hint | Pimlico | thirdPartyAttributedTo | hint | Arkham Intelligence |
| bundler | 0x4337001fff419768e088ce247456c1b892888084 evidence level: hint | Pimlico | thirdPartyAttributedTo | hint | BundleBear |
| bundler | 0x4337001fff419768e088ce247456c1b892888084 evidence level: hint | Pimlico | thirdPartyAttributedTo | hint | Arkham Intelligence |
CQ4: Which organisation attributions rest ONLY on third-party sources (no primary_source or cluster claim links the address to that organisation)? Restricted to addresses that played a role in the example transactions.
ontology/queries/cq4-third-party-only.rq - 7 row(s)
| address | roles | organization | thirdPartySources | level |
|---|---|---|---|---|
| 0x4337001fff419768e088ce247456c1b892888084 evidence level: hint | bundler+beneficiary | Pimlico | Arkham Intelligence, BundleBear | hint |
| 0x4337003fcd2f56de3977ccb806383e9161628d0e evidence level: hint | bundler+beneficiary | Pimlico | Arkham Intelligence | hint |
| 0x4337005db25dbad41da5692ba1188751ee5d98b6 evidence level: hint | beneficiary+bundler | Pimlico | Arkham Intelligence | hint |
| 0x91e60e0613810449d098b0b5ec8b51a0fe8c8985 evidence level: hint | factory | Pimlico | Arkham Intelligence | hint |
| 0xf70da97812cb96acdf810712aa562db8dfa3dbef evidence level: hint | bundler | Relay.link | Arkham Intelligence | hint |
| 0x91e60e0613810449d098b0b5ec8b51a0fe8c8985 evidence level: hint | factory | eth-infinitism | Another vendor's publication | hint |
| 0x5fa66dfe8a3983e55071e8c4631ab43b5f33a4ab evidence level: hint | paymaster | thirdweb | BundleBear, pimlicolabs/4337-stats | hint |
CQ9: For the addresses that played each role in the examples, how strong are their labels? (Distinct addresses per role and evidence level; 'none' = unlabelled.)
ontology/queries/cq9-label-levels-by-role.rq - 25 row(s)
| role | level | addresses |
|---|---|---|
| account_impl | primary_source | 2 |
| account_impl | cluster | 1 |
| account_impl | none | 2 |
| beneficiary | hint | 4 |
| beneficiary | none | 2 |
| beneficiary | cluster | 1 |
| bundler | hint | 5 |
| bundler | none | 1 |
| bundler | cluster | 1 |
| entrypoint | primary_source | 4 |
| factory | primary_source | 1 |
| factory | hint | 1 |
| module | primary_source | 2 |
| paymaster | primary_source | 3 |
| paymaster | hint | 1 |
| paymaster | none | 1 |
| sender | none | 7 |
| sender | cluster | 1 |
| signer | none | 7 |
| signer | cluster | 1 |
| target | primary_source | 3 |
| target | cluster | 3 |
| target | none | 2 |
| token_payment_recipient | none | 1 |
| wrapper | none | 1 |