aere-contracts/contracts/compliance/AereAttestationGateway.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

323 lines
14 KiB
Solidity

// 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++; }
}
}
}