// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereBestExReceipt — MiCA Article 78 best-execution receipts on-chain * @notice EU MiCA Title V (CASP conduct) Article 78 requires every Crypto * Asset Service Provider to demonstrate "best execution" — that the * price, costs, speed, likelihood, size, nature and other relevant * considerations led to the most favourable result for the client. * Until now, this proof has lived in CASPs' private back-office logs. * AERE turns it into an open, append-only on-chain record. * * For every trade executed on behalf of an EU client, a registered * CASP MUST mint a receipt here. The receipt commits to: * * - which venue was used (executedVenue) * - the price and quantity actually filled * - the alternative venues considered, with their best quotes * at the time of decision (alternativesRoot — Merkle root) * - the speed (millisecond timestamp at which the order was * accepted vs the execution timestamp) * - the protocol/SOR algorithm version that made the routing call * * Anyone — clients, supervisors, journalists — can later verify by * (a) reading the on-chain receipt and (b) reproducing the * alternative-quote Merkle tree from the CASP's published snapshot. * * @dev DESIGN CHOICES: * * 1. NOT an NFT. ERC-721 carries semantics (transfer, marketplace) * that don't fit a regulatory artefact. Receipts are bound to * the CASP that issued them; transfer/ownership is irrelevant. * Each receipt is a struct in a sealed mapping. * * 2. CASP registry is owned by the Foundation but registration is * by-reference: Foundation publishes the CASP's MiCA licence * number + national competent authority + LEI. The Foundation * CANNOT amend a receipt after issuance, only register new * CASPs and pause-register a bad actor. * * 3. No self-policing of receipt content. The contract does NOT * attempt to validate that the venue chosen actually was the * best — only that the CASP COMMITTED to a specific decision * with a specific information set. Off-chain auditors / clients * / supervisors detect non-compliance by reading the open * record. This is the same model as Chainlink: the oracle * commits, the market detects mispricing. * * 4. Per-CASP rolling counter (receiptNonce[casp]) avoids global * counter contention. Each CASP can issue receipts in parallel * without coordinating. * * 5. Pause is at the *issuance* level. A paused CASP cannot mint * new receipts but historical receipts remain queryable and * verifiable forever. Compliance discipline, not data deletion. * * @dev GAS BUDGET (target): * - issueReceipt: ~95-105k gas (mostly cold storage SSTOREs). * - At 0.5 gwei base × 100k gas = 50_000 gwei = 0.00005 AERE * (~ tiny fraction of a cent at any plausible AERE price). * - A CASP doing 10M trades/yr pays << €500/yr in receipt gas. * - That cost is the chain's competitive moat versus Polygon / * Arbitrum / Base — at MiCA scale it stays a rounding error. */ contract AereBestExReceipt { /* ================================ types ================================ */ /// @notice Permitted classification for the receipt (matches MiCA Art 78). enum OrderType { Market, // 0 Limit, // 1 StopLoss, // 2 StopLimit, // 3 TWAP, // 4 VWAP, // 5 RFQ, // 6 Other // 7 } /// @notice Permitted client classification under MiCA / MiFID II. enum ClientClass { Retail, // 0 — MiCA Title V conduct rules fully apply Professional, // 1 — opt-out clients Eligible // 2 — eligible counterparties } struct CaspRegistration { bytes32 licenceRef; // hash of (MiCA-licence-number || NCA-code || LEI) uint64 registeredAt; // unix seconds bool paused; // emergency stop on issuance bool exists; // set true on registration; never unset } struct Receipt { // identity address casp; // issuing CASP (== msg.sender at mint) bytes32 clientHash; // keccak256(client-id || casp-internal-salt) — pseudonymous, MiCA Art 78(2) ClientClass clientClass; // order bytes32 orderId; // CASP's internal order id (opaque); used by client to find this record OrderType orderType; bytes32 instrumentSymbol; // e.g. keccak256("BTC/EUR") — short stable identifier uint128 requestedQty; // base asset, scaled to 18 dec uint128 executedQty; // base asset, scaled to 18 dec uint128 executedPrice; // quote asset per base, scaled to 18 dec // routing decision bytes32 executedVenue; // keccak256(venue identifier — e.g. "Binance.com", "Coinbase Exchange") bytes32 alternativesRoot; // Merkle root of (venue, best-bid, best-ask, depth, timestamp) tuples // considered at routing time; CASP publishes the leaves off-chain. bytes32 algorithmRef; // hash of (SOR algo name || version || commit-sha) — pins which logic chose uint64 decisionTsMs; // ms timestamp at which routing decision was made uint64 executionTsMs; // ms timestamp at which fill confirmed // metadata pointer string evidenceUri; // ipfs://… or https://… — full alternatives JSON, fee schedule, etc. // referenced read-only; chain stores only the root. } /* =============================== storage =============================== */ address public immutable FOUNDATION; /// @notice CASP address → registration metadata. Once `exists`, never unset. mapping(address => CaspRegistration) public caspOf; /// @notice CASP → next nonce. receiptId = keccak256(casp, nonce). mapping(address => uint256) public receiptNonce; /// @notice global receiptId → Receipt. Append-only; never edited or deleted. mapping(bytes32 => Receipt) internal _receipts; /// @notice CASP → cumulative receipts issued. mapping(address => uint256) public issuedCount; /* ================================ events ================================ */ event CaspRegistered(address indexed casp, bytes32 licenceRef); event CaspPaused(address indexed casp, bool paused); event ReceiptIssued( address indexed casp, bytes32 indexed receiptId, bytes32 indexed clientHash, bytes32 instrumentSymbol, bytes32 executedVenue, bytes32 alternativesRoot, uint128 executedQty, uint128 executedPrice, uint64 decisionTsMs, uint64 executionTsMs, bytes32 algorithmRef, string evidenceUri ); /* ================================ errors ================================ */ error NotFoundation(); error CaspExists(); error CaspUnknown(); error CaspIsPaused(); error ReceiptExists(); error ZeroLicence(); error BadTimestamps(); error BadQuantities(); error BadAlternatives(); /* =============================== modifiers ============================== */ modifier onlyFoundation() { if (msg.sender != FOUNDATION) revert NotFoundation(); _; } /* ============================== constructor ============================= */ constructor(address foundation) { require(foundation != address(0), "zero-foundation"); FOUNDATION = foundation; } /* ============================= registry ops ============================= */ /// @notice Foundation registers a new CASP. licenceRef MUST hash an /// actual MiCA licence + NCA + LEI off-chain (no on-chain PII). function registerCasp(address casp, bytes32 licenceRef) external onlyFoundation { if (casp == address(0)) revert CaspUnknown(); if (licenceRef == bytes32(0)) revert ZeroLicence(); if (caspOf[casp].exists) revert CaspExists(); caspOf[casp] = CaspRegistration({ licenceRef: licenceRef, registeredAt: uint64(block.timestamp), paused: false, exists: true }); emit CaspRegistered(casp, licenceRef); } /// @notice Pause / unpause issuance for a CASP. Historical receipts /// remain queryable and verifiable. function setCaspPaused(address casp, bool paused) external onlyFoundation { CaspRegistration storage r = caspOf[casp]; if (!r.exists) revert CaspUnknown(); r.paused = paused; emit CaspPaused(casp, paused); } /* ============================ issuance path ============================ */ /** * @notice Mint a new best-execution receipt. msg.sender MUST be a * registered, non-paused CASP. The receipt is bound to this * CASP forever and cannot be amended. * @return receiptId = keccak256(abi.encode(msg.sender, nonce)). */ function issueReceipt(Receipt calldata r) external returns (bytes32 receiptId) { CaspRegistration storage reg = caspOf[msg.sender]; if (!reg.exists) revert CaspUnknown(); if (reg.paused) revert CaspIsPaused(); if (r.casp != msg.sender) revert CaspUnknown(); if (r.executionTsMs < r.decisionTsMs) revert BadTimestamps(); if (r.executedQty == 0 || r.executedPrice == 0) revert BadQuantities(); if (r.executedQty > r.requestedQty) revert BadQuantities(); if (r.alternativesRoot == bytes32(0)) revert BadAlternatives(); if (r.executedVenue == bytes32(0)) revert BadAlternatives(); uint256 nonce = receiptNonce[msg.sender]++; receiptId = keccak256(abi.encode(msg.sender, nonce)); if (_receipts[receiptId].casp != address(0)) revert ReceiptExists(); _receipts[receiptId] = r; issuedCount[msg.sender] = nonce + 1; emit ReceiptIssued( msg.sender, receiptId, r.clientHash, r.instrumentSymbol, r.executedVenue, r.alternativesRoot, r.executedQty, r.executedPrice, r.decisionTsMs, r.executionTsMs, r.algorithmRef, r.evidenceUri ); } /* ================================ views ================================ */ /// @notice Read a receipt by its id. Reverts not applied — caller checks /// `r.casp == address(0)` for not-found. function getReceipt(bytes32 receiptId) external view returns (Receipt memory r) { return _receipts[receiptId]; } /// @notice Deterministic receipt id for the next mint by `casp`. /// Useful for relayer dry-runs. function nextReceiptId(address casp) external view returns (bytes32) { return keccak256(abi.encode(casp, receiptNonce[casp])); } /// @notice The decision-to-execution latency in milliseconds. One of the /// "speed" inputs MiCA Art 78 names. Stored derivable for indexers. function executionLatencyMs(bytes32 receiptId) external view returns (uint64) { Receipt storage r = _receipts[receiptId]; return r.executionTsMs - r.decisionTsMs; } /// @notice CASP is registered, non-paused, and has issued ≥ 1 receipt. /// The minimum filter a client should apply when choosing a CASP. function caspIsActive(address casp) external view returns (bool) { CaspRegistration storage reg = caspOf[casp]; return reg.exists && !reg.paused && issuedCount[casp] > 0; } }