aere-contracts/contracts/compliance/AereSanctionsRegistry.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

211 lines
8.6 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
/**
* @title AereSanctionsRegistry — read-only OFAC SDN sanctions registry
* @notice Foundation publishes a Merkle root of OFAC Specially Designated
* Nationals (SDN) addresses each epoch (24h cron). Any consumer
* contract can verify whether a specific address is sanctioned at
* a given epoch by submitting a Merkle inclusion proof.
*
* Source of truth: the official OFAC SDN list at
* https://www.treasury.gov/ofac/downloads/sdnlist.txt
* An off-chain ingester running on a Hostinger VPS (#2) reads the
* official list nightly, extracts crypto addresses where present,
* builds the Merkle tree, and publishes the root via the
* Foundation 2-of-3 multisig (Ledger-signed).
*
* Designed as a PUBLIC GOOD. Anyone — institutional integrator,
* dApp, EOA — calls `isSanctioned(...)` to gate or screen a
* counterparty. There is no fee. No discretionary interception.
*
* @dev Lifecycle: Proposed → (Challenged?) → Attested (mirror of
* AereNavOracle pattern). Optimistic 24h window.
*
* Leaf format: keccak256(abi.encode(address evmAddress, uint16 listId))
* where listId distinguishes the source list (1 = OFAC SDN,
* 2 = OFAC SSI, 3 = EU consolidated, etc.). Phase 1 ships with
* OFAC SDN only.
*
* IMMUTABILITY:
* - CHALLENGE_WINDOW constant
* - History append-only; attested epochs cannot be rewritten
* - Owner can only propose / dismiss / attest
* - There is NO "deSanction" path on chain — that would suggest
* AERE Foundation is making a sanctions decision, which it is
* NOT. The Foundation only mirrors the canonical OFAC list.
*/
contract AereSanctionsRegistry is Ownable {
uint256 public constant CHALLENGE_WINDOW = 24 hours;
/// @notice OFAC SDN list id, used as the prefix in the leaf hash.
uint16 public constant LIST_OFAC_SDN = 1;
uint16 public constant LIST_OFAC_SSI = 2;
uint16 public constant LIST_EU_CONSOLIDATED = 3;
uint16 public constant LIST_UK_HMT = 4;
uint16 public constant LIST_UN_CONSOLIDATED = 5;
enum State { Proposed, Challenged, Attested }
struct Snapshot {
bytes32 root;
uint64 proposedAt;
uint64 attestedAt;
State state;
string sourceUrl; // canonical OFAC URL the root was built from
string ipfsCid; // optional: pin the full list
}
/// @notice epoch → Snapshot
mapping(uint256 => Snapshot) public snapshots;
/// @notice epoch → open-challenge count
mapping(uint256 => uint256) public openChallenges;
mapping(uint256 => Challenge[]) public epochChallenges;
struct Challenge {
address challenger;
uint64 timestamp;
string reason;
bool dismissed;
}
/// @notice monotonic latest epoch counter.
uint256 public latestEpoch;
event SnapshotProposed(uint256 indexed epoch, bytes32 root, string sourceUrl, string ipfsCid);
event SnapshotChallenged(uint256 indexed epoch, uint256 challengeId, address challenger, string reason);
event ChallengeDismissed(uint256 indexed epoch, uint256 challengeId, string response);
event SnapshotAttested(uint256 indexed epoch, bytes32 root);
error WrongState(State got, State want);
error UnknownEpoch();
error UnknownChallenge();
error EpochAlreadyExists();
error TimelockNotElapsed(uint256 nowTs, uint256 earliest);
/* -------------------------------- propose -------------------------------- */
function proposeSnapshot(
uint256 epoch,
bytes32 root,
string calldata sourceUrl,
string calldata ipfsCid
) external onlyOwner {
if (snapshots[epoch].proposedAt != 0) revert EpochAlreadyExists();
snapshots[epoch] = Snapshot({
root: root,
proposedAt: uint64(block.timestamp),
attestedAt: 0,
state: State.Proposed,
sourceUrl: sourceUrl,
ipfsCid: ipfsCid
});
if (epoch > latestEpoch) latestEpoch = epoch;
emit SnapshotProposed(epoch, root, sourceUrl, ipfsCid);
}
/* ------------------------------- challenge ------------------------------ */
function challenge(uint256 epoch, string calldata reason) external returns (uint256 challengeId) {
Snapshot storage s = snapshots[epoch];
if (s.proposedAt == 0) revert UnknownEpoch();
if (s.state != State.Proposed && s.state != State.Challenged) revert WrongState(s.state, State.Proposed);
if (block.timestamp >= uint256(s.proposedAt) + CHALLENGE_WINDOW) {
revert TimelockNotElapsed(block.timestamp, uint256(s.proposedAt) + CHALLENGE_WINDOW);
}
challengeId = epochChallenges[epoch].length;
epochChallenges[epoch].push(Challenge({
challenger: msg.sender,
timestamp: uint64(block.timestamp),
reason: reason,
dismissed: false
}));
openChallenges[epoch]++;
s.state = State.Challenged;
emit SnapshotChallenged(epoch, challengeId, msg.sender, reason);
}
function dismissChallenge(uint256 epoch, uint256 challengeId, string calldata response) external onlyOwner {
Challenge[] storage list = epochChallenges[epoch];
if (challengeId >= list.length) revert UnknownChallenge();
Challenge storage c = list[challengeId];
if (c.dismissed) revert UnknownChallenge();
c.dismissed = true;
openChallenges[epoch]--;
if (openChallenges[epoch] == 0) snapshots[epoch].state = State.Proposed;
emit ChallengeDismissed(epoch, challengeId, response);
}
/* ------------------------------- attestation ---------------------------- */
function attest(uint256 epoch) external {
Snapshot storage s = snapshots[epoch];
if (s.proposedAt == 0) revert UnknownEpoch();
if (s.state != State.Proposed) revert WrongState(s.state, State.Proposed);
uint256 earliest = uint256(s.proposedAt) + CHALLENGE_WINDOW;
if (block.timestamp < earliest) revert TimelockNotElapsed(block.timestamp, earliest);
if (openChallenges[epoch] != 0) revert WrongState(State.Challenged, State.Proposed);
s.state = State.Attested;
s.attestedAt = uint64(block.timestamp);
emit SnapshotAttested(epoch, s.root);
}
/* ------------------------------- verify -------------------------------- */
/// @notice Verify that `evmAddress` was on `listId` at `epoch`.
/// @return sanctioned True if the proof is valid AND the snapshot is attested.
function isSanctioned(
uint256 epoch,
address evmAddress,
uint16 listId,
bytes32[] calldata proof
) external view returns (bool sanctioned) {
Snapshot storage s = snapshots[epoch];
if (s.state != State.Attested) return false;
bytes32 leaf = keccak256(abi.encode(evmAddress, listId));
return MerkleProof.verify(proof, s.root, leaf);
}
/// @notice ROUND-3 FIX: cap iteration depth to prevent griefable DoS.
/// If Foundation proposes 1000 challenged-but-never-attested
/// snapshots, dApps calling this in-tx would have been bricked.
/// Now capped at 64 — practically: the latest 64 epochs.
uint256 public constant MAX_EPOCH_SCAN = 64;
/// @notice Same shape as isSanctioned but takes only the latest attested epoch.
function isSanctionedLatest(
address evmAddress,
uint16 listId,
bytes32[] calldata proof
) external view returns (bool sanctioned, uint256 attestedEpoch) {
uint256 scanned = 0;
for (uint256 e = latestEpoch; e > 0 && scanned < MAX_EPOCH_SCAN; e--) {
Snapshot storage s = snapshots[e];
if (s.state == State.Attested) {
bytes32 leaf = keccak256(abi.encode(evmAddress, listId));
return (MerkleProof.verify(proof, s.root, leaf), e);
}
unchecked { scanned++; }
}
return (false, 0);
}
function snapshotStatus(uint256 epoch) external view returns (
bytes32 root,
uint64 proposedAt,
uint64 attestedAt,
State state,
uint256 openChallengeCount,
string memory sourceUrl,
string memory ipfsCid
) {
Snapshot storage s = snapshots[epoch];
return (s.root, s.proposedAt, s.attestedAt, s.state, openChallenges[epoch], s.sourceUrl, s.ipfsCid);
}
}