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.
211 lines
8.6 KiB
Solidity
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);
|
|
}
|
|
}
|