Identity Proofs (EVM)

How we represent KYC/AML eligibility on EVM networks using attestation-led identity proofs with EAS + PolicyEngine, separating long-term KYC verification timestamps from short-term sanctions epochs.

Background

On EVM chains, Fairway doesn’t mint identity tokens. Instead, compliance status is expressed through EAS attestations that encode:

  • When the user last passed KYC (kyc_verified_at).

  • Current sanctions/risk freshness (sanctions_epoch).

  • Optional compliance claims (jurisdiction, accreditation, risk score).

  • Commitment root linking to Midnight proofs.

These attestations are:

  • Issued off-chain by the Fairway Cloud Agent (Witness) after reading KYC data from decentralized storage.

  • Anchored on-chain in the EAS registry.

  • Referenced in dApps through the Fairway PolicyEngine:

(bool ok, uint32 reason) = policyEngine.isEligible(user, ruleBytes);

Components

  • EAS (Ethereum Attestation Service) → canonical registry of typed attestations.

  • Fairway Verifier → contract that validates attestations, issuer trust, timestamp, and sanctions epoch.

  • PolicyEngine → single-call YES/NO eligibility check for dApps.

  • IdentityRegistry → governance-controlled list of trusted issuers and rules.

  • Optional façade tokens → ERC-5484 / ERC-5192 for UX or whitelist compatibility.


Flow

sequenceDiagram
  participant User as Wallet
  participant Agent as Fairway Cloud Agent (Witness)
  participant EAS as EAS Registry
  participant Verifier as Fairway Verifier
  participant Policy as PolicyEngine
  participant dApp as Protocol

  User->>Agent: Completes off-chain KYC
  Agent->>EAS: Issue EIP-712 attestation {kyc_verified_at, sanctions_epoch, claims}
  dApp->>Policy: isEligible(user, ruleBytes)
  Policy->>Verifier: Verify attestation + signature + epoch freshness
  Verifier->>EAS: Resolve attestation UID
  Verifier->>Policy: Return (ok, reason)
  Policy->>dApp: YES / NO

Example Attestation Schema

{
  "name": "FairwayComplianceV2",
  "fields": [
    { "name": "subject", "type": "address" },
    { "name": "policyId", "type": "bytes32" },
    { "name": "kyc_verified_at", "type": "uint64" },     // unix timestamp
    { "name": "sanctions_epoch", "type": "uint64" },     // rolling freshness counter
    { "name": "jurisdictionSet", "type": "uint16" },
    { "name": "accredited", "type": "bool" },
    { "name": "risk_profile_score", "type": "uint16" },
    { "name": "commitmentRoot", "type": "bytes32" }
  ],
  "revocable": true}

Rule Encoding

Policies are defined once in the IdentityRegistry.

dApps pack rules as ruleBytes:

bytes32 policyId = keccak256("POOL-A.KYC2.EU-ACCREDITED");

bytes memory ruleBytes = abi.encode(
    policyId,
    userAttestationUID,   // EAS UID
    expectedEpochRoot,    // sanctions freshness
    maxKycAge             // e.g. 180 days
);

Validator Checks

  1. Attestation validity → issuer signature + schema match.

  2. KYC freshnessblock.timestamp - kyc_verified_at <= maxKycAge.

  3. Sanctions freshnesssanctions_epoch == latestEpochRoot.

  4. Other claims → jurisdiction, accreditation, or risk score thresholds.


Benefits

  • PII-free → no sensitive docs stored on-chain.

  • Separation of duties → KYC timestamp (long-lived) + sanctions epoch (short-lived).

  • Compliant → FATF/AMLD rules enforced per jurisdiction by dApp-defined max age.

  • Interoperable → compatible with ERC-3643 TransferManager, EAS tooling, and SBT façades.

  • Simple DX → dApps use a single isEligible() call.

  • AuditablecommitmentRoot links attestations back to Midnight ZK proofs.


Next Steps

Last updated

Was this helpful?