// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereAttestationGateway — unified registry for regulator attestations * @notice The compliance landscape splits the same primitive — "this entity * has been assessed by an authorised body" — into incompatible silos: * * - MiCA Title II/V: CASP licence + best-execution conformity * - EU AI Act Annex III: high-risk AI conformity * - SEC 1099-DA: broker reporting compliance * - FCA CP24/x: UK consumer-duty conformity * - FINMA / MAS: equivalents in CH / SG * * AereAttestationGateway gives those bodies (and the protocols * that need to RELY on their judgement) a single on-chain shape. * * MODEL: * * 1. The Foundation publishes a SCHEMA when a new attestation * shape is needed. Each schema has an id (e.g. keccak256 * ("mica-casp-v1") ), an immutable hash of the off-chain * human-readable spec, and a semver pin. Schemas are * append-only — fixing typos requires a new schemaId. * * 2. Per-schema, the Foundation registers AUTHORISED ATTESTORS * — addresses that this schema accepts as valid issuers. * The Foundation is one such attestor for some schemas; for * others (MiCA-Art-78 conformity, say) the only valid * attestor is the customer's own NCA-registered auditor. * * 3. An attestor issues an attestation. The payload itself * lives off-chain (ipfs:// or https://, the full PDF / JSON * spec); the chain stores ONLY the hash + validity window * + the schema/subject/attestor triple. No PII. * * 4. Revocation: the issuing attestor can revoke at any time. * Reads return `isValidNow(...) == false` immediately. A * revoked attestation cannot be un-revoked (the attestor * would issue a fresh one). The previous attestation's * revocation is preserved in event history forever. * * 5. INHERITANCE (optional): an attestation can reference a * parent attestation id. The validity check chases the * chain: if any ancestor is revoked or expired, the child * fails. Useful for "this provider is allowed to issue * best-execution receipts BECAUSE Foundation has attested * their MiCA licence." Cycle protection via depth bound. * * WHY NOT EAS? * Ethereum Attestation Service does generic on-chain * attestations beautifully. This contract is intentionally * narrower: it bakes in inheritance + per-schema attestor * gating + a Foundation-published schema registry. Consumers * that need richer features can adopt EAS in parallel. * * GAS: * attest() ~70-80k cold-path (1 SSTORE for the main record, * 1 for the per-(subject,schema) head pointer). * * IMMUTABILITY: * - FOUNDATION immutable. * - Schemas append-only; cannot be edited or removed. * - Attestor authorisation flags can be toggled OFF but never * retroactively invalidate past attestations the attestor * issued while authorised. * - MAX_INHERITANCE_DEPTH constant. */ contract AereAttestationGateway { /* ================================ config ================================ */ address public immutable FOUNDATION; uint256 public constant MAX_INHERITANCE_DEPTH = 8; /* ================================= types ================================= */ struct Schema { bytes32 specHash; // SHA256 of the off-chain human-readable spec string name; // e.g. "mica-casp-v1" string semver; // e.g. "1.0.0" string specUri; // ipfs:// or https:// — full spec doc uint64 publishedAt; bool exists; } struct Attestation { bytes32 schemaId; bytes32 subject; // keccak256 of the subject identifier address attestor; bytes32 parentId; // 0x0 if no parent bytes32 payloadHash; // SHA256 of the off-chain payload string payloadUri; uint64 validFrom; uint64 validUntil; // 0 = no expiry bool revoked; bool exists; } /* ================================ storage ================================ */ /// @notice schemaId → Schema metadata. Append-only. mapping(bytes32 => Schema) public schemas; /// @notice schemaId → attestor → authorised flag. Toggleable. mapping(bytes32 => mapping(address => bool)) public isAuthorisedAttestor; /// @notice attestor → next nonce (used to derive attestationId). mapping(address => uint256) public attestorNonce; /// @notice attestationId → Attestation. Append-only except for `revoked`. mapping(bytes32 => Attestation) internal _attestations; /// @notice (subject, schemaId) → most-recent attestationId for that pair. /// Convenience for consumers that just want "is this subject /// currently attested under this schema?" /// AUDIT FIX (HIGH #24): only updated when prior is invalid OR new /// passes _isValid at write-time. Prevents griefing where attestor B /// posts an already-expired record that wipes attestor A's valid one. mapping(bytes32 => mapping(bytes32 => bytes32)) public latestFor; /// @notice (subject, schemaId, attestor) → that attestor's most-recent /// attestation id for that subject. Consumers reading per-attestor /// data go through this map. mapping(bytes32 => mapping(bytes32 => mapping(address => bytes32))) public latestForBy; /* ================================ events ================================ */ event SchemaPublished(bytes32 indexed schemaId, bytes32 specHash, string name, string semver, string specUri); event AttestorAuthorisation(bytes32 indexed schemaId, address indexed attestor, bool authorised); event Attested( bytes32 indexed attestationId, bytes32 indexed schemaId, bytes32 indexed subject, address attestor, bytes32 parentId, bytes32 payloadHash, uint64 validFrom, uint64 validUntil, string payloadUri ); event Revoked(bytes32 indexed attestationId, address indexed attestor); /* ================================ errors ================================ */ error NotFoundation(); error SchemaExists(); error SchemaUnknown(); error EmptySpec(); error NotAuthorisedAttestor(); error AttestationExists(); error AttestationUnknown(); error AlreadyRevoked(); error NotIssuer(); error BadValidityWindow(); error InheritanceDepthExceeded(); error ParentInvalid(); error ParentSubjectMismatch(); /* =============================== modifiers ============================== */ modifier onlyFoundation() { if (msg.sender != FOUNDATION) revert NotFoundation(); _; } /* ============================== constructor ============================= */ constructor(address foundation) { require(foundation != address(0), "zero-foundation"); FOUNDATION = foundation; } /* ============================== schema ops ============================== */ function publishSchema( bytes32 schemaId, bytes32 specHash, string calldata name, string calldata semver, string calldata specUri ) external onlyFoundation { if (specHash == bytes32(0)) revert EmptySpec(); if (schemas[schemaId].exists) revert SchemaExists(); schemas[schemaId] = Schema({ specHash: specHash, name: name, semver: semver, specUri: specUri, publishedAt: uint64(block.timestamp), exists: true }); emit SchemaPublished(schemaId, specHash, name, semver, specUri); } function setAttestor(bytes32 schemaId, address attestor, bool authorised) external onlyFoundation { if (!schemas[schemaId].exists) revert SchemaUnknown(); isAuthorisedAttestor[schemaId][attestor] = authorised; emit AttestorAuthorisation(schemaId, attestor, authorised); } /* ============================== attest path ============================= */ /** * @notice Issue an attestation. msg.sender MUST be authorised for the * schema. The attestationId is keccak256(attestor, nonce++). * @return attestationId The deterministic id. */ function attest( bytes32 schemaId, bytes32 subject, bytes32 parentId, bytes32 payloadHash, uint64 validFrom, uint64 validUntil, string calldata payloadUri ) external returns (bytes32 attestationId) { if (!schemas[schemaId].exists) revert SchemaUnknown(); if (!isAuthorisedAttestor[schemaId][msg.sender]) revert NotAuthorisedAttestor(); if (validUntil != 0 && validUntil < validFrom) revert BadValidityWindow(); // If a parent is supplied, sanity-check inheritance depth + existence // BEFORE writing the new record. The chase is shallow (≤ MAX_DEPTH). if (parentId != bytes32(0)) { Attestation storage parent = _attestations[parentId]; if (!parent.exists) revert ParentInvalid(); // AUDIT FIX (HIGH #23): parent must attest the SAME subject. Without // this, low-bar attestors could chain inheritance to high-bar parents // of unrelated subjects, fabricating transitive trust. if (parent.subject != subject) revert ParentSubjectMismatch(); _checkInheritanceDepth(parentId); } uint256 nonce = attestorNonce[msg.sender]++; attestationId = keccak256(abi.encode(msg.sender, nonce)); if (_attestations[attestationId].exists) revert AttestationExists(); _attestations[attestationId] = Attestation({ schemaId: schemaId, subject: subject, attestor: msg.sender, parentId: parentId, payloadHash: payloadHash, payloadUri: payloadUri, validFrom: validFrom, validUntil: validUntil, revoked: false, exists: true }); // Per-attestor latest pointer always updates (consumer chooses // which attestor to read). latestForBy[subject][schemaId][msg.sender] = attestationId; // AUDIT FIX (HIGH #24): only overwrite the schema-wide latestFor // when the prior is invalid (revoked/expired/missing) OR the new // attestation is itself currently valid. bytes32 prior = latestFor[subject][schemaId]; bool priorValid = prior != bytes32(0) && _isValid(prior, block.timestamp, 0); bool newValidNow = (validFrom <= block.timestamp) && (validUntil == 0 || validUntil >= block.timestamp); if (!priorValid || newValidNow) { latestFor[subject][schemaId] = attestationId; } emit Attested(attestationId, schemaId, subject, msg.sender, parentId, payloadHash, validFrom, validUntil, payloadUri); } function revoke(bytes32 attestationId) external { Attestation storage a = _attestations[attestationId]; if (!a.exists) revert AttestationUnknown(); if (a.attestor != msg.sender) revert NotIssuer(); if (a.revoked) revert AlreadyRevoked(); a.revoked = true; emit Revoked(attestationId, msg.sender); } /* ================================ views ================================= */ /// @notice Full attestation record (struct memory copy). function attestationOf(bytes32 attestationId) external view returns (Attestation memory) { return _attestations[attestationId]; } /// @notice The "is this attestation good RIGHT NOW" check. /// Returns false if: /// - the attestation doesn't exist /// - it's revoked /// - block.timestamp is outside [validFrom, validUntil] /// - any ancestor (within MAX depth) fails the same check function isValidNow(bytes32 attestationId) external view returns (bool) { return _isValid(attestationId, block.timestamp, 0); } function _isValid(bytes32 id, uint256 nowTs, uint256 depth) internal view returns (bool) { if (depth >= MAX_INHERITANCE_DEPTH) return false; Attestation storage a = _attestations[id]; if (!a.exists || a.revoked) return false; if (nowTs < a.validFrom) return false; if (a.validUntil != 0 && nowTs > a.validUntil) return false; if (a.parentId == bytes32(0)) return true; return _isValid(a.parentId, nowTs, depth + 1); } /// @notice Convenience: subject is currently attested under schema by /// the LATEST issued attestation for that (subject, schema). function subjectCurrentlyAttested(bytes32 subject, bytes32 schemaId) external view returns (bool) { bytes32 id = latestFor[subject][schemaId]; if (id == bytes32(0)) return false; return _isValid(id, block.timestamp, 0); } /// @dev Validates that walking up from parent doesn't exceed MAX_DEPTH. function _checkInheritanceDepth(bytes32 parentId) internal view { uint256 depth = 1; bytes32 cur = parentId; while (cur != bytes32(0)) { if (depth >= MAX_INHERITANCE_DEPTH) revert InheritanceDepthExceeded(); Attestation storage a = _attestations[cur]; if (!a.exists) revert ParentInvalid(); cur = a.parentId; unchecked { depth++; } } } }