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
Attestation validity → issuer signature + schema match.
KYC freshness →
block.timestamp - kyc_verified_at <= maxKycAge
.Sanctions freshness →
sanctions_epoch == latestEpochRoot
.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.Auditable →
commitmentRoot
links attestations back to Midnight ZK proofs.
Next Steps
See the ERC-3643 Adapter for RWA integration. (coming soon)
Last updated
Was this helpful?