aere-contracts/contracts/bestex/AereBestExReceipt.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

281 lines
12 KiB
Solidity
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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