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.
323 lines
14 KiB
Solidity
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++; }
|
|
}
|
|
}
|
|
}
|