// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; /** * @title AereIdentity * @notice Lightweight on-chain DID registry. Each address controls its own identity: * off-chain signed credentials, profile metadata URI, and revocable claims * issued by trusted attestors (KYC providers, employers, communities). * @dev Compatible-in-spirit with W3C DID + ERC-1056. Profile metadata stored as a * content-addressed URI (e.g. ipfs://...) so the chain holds only the pointer. */ contract AereIdentity { // ── Profiles ──────────────────────────────────────────────── mapping(address => string) public profileURI; mapping(address => uint64) public profileUpdatedAt; event ProfileUpdated(address indexed identity, string uri, uint64 timestamp); function setProfile(string calldata uri) external { profileURI[msg.sender] = uri; profileUpdatedAt[msg.sender] = uint64(block.timestamp); emit ProfileUpdated(msg.sender, uri, uint64(block.timestamp)); } // ── Attestors ─────────────────────────────────────────────── mapping(address => bool) public isAttestor; address public owner; event AttestorAdded(address indexed attestor); event AttestorRemoved(address indexed attestor); constructor() { owner = msg.sender; isAttestor[msg.sender] = true; } modifier onlyOwner() { require(msg.sender == owner, "owner"); _; } function addAttestor(address a) external onlyOwner { isAttestor[a] = true; emit AttestorAdded(a); } function removeAttestor(address a) external onlyOwner { isAttestor[a] = false; emit AttestorRemoved(a); } function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0)); owner = newOwner; } // ── Claims ────────────────────────────────────────────────── /// @dev claimType examples: keccak256("KYC_TIER_1"), keccak256("EMAIL_VERIFIED"), /// keccak256("DAO_MEMBER:ENS"), keccak256("AGE_OVER_18"), etc. struct Claim { address attestor; uint64 issuedAt; uint64 expiresAt; // 0 = never bytes32 evidence; // optional hash of off-chain document bool revoked; } // subject => claimType => attestor => Claim mapping(address => mapping(bytes32 => mapping(address => Claim))) public claims; event ClaimIssued(address indexed subject, bytes32 indexed claimType, address indexed attestor, uint64 expiresAt, bytes32 evidence); event ClaimRevoked(address indexed subject, bytes32 indexed claimType, address indexed attestor); function issueClaim(address subject, bytes32 claimType, uint64 expiresAt, bytes32 evidence) external { require(isAttestor[msg.sender], "not attestor"); require(subject != address(0), "subject"); claims[subject][claimType][msg.sender] = Claim({ attestor: msg.sender, issuedAt: uint64(block.timestamp), expiresAt: expiresAt, evidence: evidence, revoked: false }); emit ClaimIssued(subject, claimType, msg.sender, expiresAt, evidence); } function revokeClaim(address subject, bytes32 claimType) external { Claim storage c = claims[subject][claimType][msg.sender]; require(c.attestor == msg.sender, "not issuer"); require(!c.revoked, "already"); c.revoked = true; emit ClaimRevoked(subject, claimType, msg.sender); } /// @notice Check if a subject has a non-expired, non-revoked claim of a given type /// from a specific attestor. function hasValidClaim(address subject, bytes32 claimType, address attestor) external view returns (bool) { Claim memory c = claims[subject][claimType][attestor]; if (c.attestor == address(0) || c.revoked) return false; if (c.expiresAt != 0 && block.timestamp > c.expiresAt) return false; return true; } }