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.
218 lines
9.2 KiB
Solidity
218 lines
9.2 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 AereNavOracle — Merkle-rooted asset reserve attestations
|
|
* @notice Publishes proof-of-backing for every bridged stablecoin and RWA
|
|
* settlement deposit held by AERE protocol contracts. The Foundation
|
|
* publishes a snapshot Merkle root once per epoch (24h by default);
|
|
* anyone can later verify a specific (asset, reserveAmount) pair
|
|
* against the published root via a Merkle proof.
|
|
*
|
|
* Used by:
|
|
* - aere.network/reserves — public dashboard
|
|
* - USDC.e/USDT.e/USDe.e/EURC.e display pages
|
|
* - AereSettlementHub for the collateral wave 1 assets
|
|
* - third-party integrators verifying reserve before custody
|
|
*
|
|
* @dev Optimistic challenge: every newly proposed root sits for
|
|
* CHALLENGE_WINDOW (24h default) before becoming "attested".
|
|
* During that window anyone can submit `challenge(epoch, reason)`
|
|
* which flags the epoch for Foundation review. Challenged epochs
|
|
* cannot be promoted to attested until the Foundation dismisses
|
|
* the challenge with a public response (similar to AereRetroPGF
|
|
* pattern).
|
|
*
|
|
* Leaf format (single-hash to match standard Merkle libs):
|
|
* keccak256(abi.encode(asset, chainId, reserveAmount, decimals))
|
|
*
|
|
* Asset reserve sources (off-chain):
|
|
* - bridged stablecoins: AereHypERC20Collateral.lockedSupply on
|
|
* Ethereum mainnet for the corresponding token.
|
|
* - RWA tokens: on-chain balanceOf on the contract that
|
|
* custodies them on AERE chain 2800 (set in the SettlementHub
|
|
* receiver allowlist).
|
|
* - native AERE: fixed 2.8B from genesis minus burned
|
|
* balance at 0x...dEaD.
|
|
*
|
|
* The Foundation off-chain ingester (Python cron on Hostinger VPS,
|
|
* Ledger-signed) collects these reserve numbers, builds the Merkle
|
|
* tree, and posts the root via proposeSnapshot.
|
|
*
|
|
* IMMUTABILITY:
|
|
* - CHALLENGE_WINDOW constant.
|
|
* - Foundation can only propose / dismiss / attest — never
|
|
* retroactively alter an attested snapshot.
|
|
* - History is append-only; older snapshots remain queryable.
|
|
*/
|
|
contract AereNavOracle is Ownable {
|
|
|
|
/* ------------------------------- immutable ------------------------------- */
|
|
|
|
uint256 public constant CHALLENGE_WINDOW = 24 hours;
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
enum State { Proposed, Challenged, Attested }
|
|
|
|
struct Snapshot {
|
|
bytes32 root;
|
|
uint64 proposedAt;
|
|
uint64 attestedAt;
|
|
State state;
|
|
string ipfsCid; // off-chain JSON of the full reserve list, optional
|
|
}
|
|
|
|
/// @notice epoch number → snapshot
|
|
mapping(uint256 => Snapshot) public snapshots;
|
|
|
|
/// @notice monotonically-increasing epoch counter.
|
|
uint256 public latestEpoch;
|
|
|
|
/// @notice epoch → open-challenge counter (multiple may exist).
|
|
mapping(uint256 => uint256) public openChallenges;
|
|
mapping(uint256 => Challenge[]) public epochChallenges;
|
|
|
|
struct Challenge {
|
|
address challenger;
|
|
uint64 timestamp;
|
|
string reason;
|
|
bool dismissed;
|
|
}
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event SnapshotProposed(uint256 indexed epoch, bytes32 root, string ipfsCid);
|
|
event SnapshotChallenged(uint256 indexed epoch, uint256 indexed challengeId, address challenger, string reason);
|
|
event ChallengeDismissed(uint256 indexed epoch, uint256 indexed challengeId, string response);
|
|
event SnapshotAttested(uint256 indexed epoch, bytes32 root);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error WrongState(State got, State want);
|
|
error TimelockNotElapsed(uint256 nowTs, uint256 earliest);
|
|
error UnknownEpoch();
|
|
error UnknownChallenge();
|
|
error InvalidProof();
|
|
error EpochAlreadyExists();
|
|
|
|
/* -------------------------------- propose -------------------------------- */
|
|
|
|
/// @notice Foundation publishes a new snapshot root. Starts the 24h
|
|
/// challenge window. Epoch must be monotonically-greater than
|
|
/// the latest one.
|
|
function proposeSnapshot(uint256 epoch, bytes32 root, 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,
|
|
ipfsCid: ipfsCid
|
|
});
|
|
if (epoch > latestEpoch) latestEpoch = epoch;
|
|
emit SnapshotProposed(epoch, root, ipfsCid);
|
|
}
|
|
|
|
/* ------------------------------ challenge ------------------------------- */
|
|
|
|
/// @notice Anyone can challenge a proposed snapshot within the window.
|
|
/// Returns the challenge id.
|
|
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]--;
|
|
Snapshot storage s = snapshots[epoch];
|
|
if (openChallenges[epoch] == 0) s.state = State.Proposed;
|
|
emit ChallengeDismissed(epoch, challengeId, response);
|
|
}
|
|
|
|
/* ----------------------------- attestation ----------------------------- */
|
|
|
|
/// @notice Permissionless attestation after the 24h challenge window
|
|
/// with no open challenges. Locks the snapshot as attested.
|
|
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);
|
|
}
|
|
|
|
/* -------------------------------- views -------------------------------- */
|
|
|
|
/// @notice Verify a (asset, chainId, reserveAmount, decimals) tuple
|
|
/// against the attested root for `epoch`.
|
|
/// @return ok True if the proof is valid AND the snapshot is attested.
|
|
function verifyReserve(
|
|
uint256 epoch,
|
|
address asset,
|
|
uint256 chainId,
|
|
uint256 reserveAmount,
|
|
uint8 decimals,
|
|
bytes32[] calldata proof
|
|
) external view returns (bool ok) {
|
|
Snapshot storage s = snapshots[epoch];
|
|
if (s.state != State.Attested) return false;
|
|
bytes32 leaf = keccak256(abi.encode(asset, chainId, reserveAmount, decimals));
|
|
return MerkleProof.verify(proof, s.root, leaf);
|
|
}
|
|
|
|
function snapshotStatus(uint256 epoch) external view returns (
|
|
bytes32 root,
|
|
uint64 proposedAt,
|
|
uint64 attestedAt,
|
|
State state,
|
|
uint256 openChallengeCount,
|
|
string memory ipfsCid
|
|
) {
|
|
Snapshot storage s = snapshots[epoch];
|
|
return (s.root, s.proposedAt, s.attestedAt, s.state, openChallenges[epoch], s.ipfsCid);
|
|
}
|
|
|
|
/// @notice ROUND-3 FIX: bounded loop. Attacker proposing many challenged-but-
|
|
/// never-attested snapshots could DoS consumer dApps reading this view.
|
|
/// Cap at 128 epochs (~128 days at daily NAV updates).
|
|
uint256 public constant MAX_EPOCH_SCAN = 128;
|
|
|
|
function latestAttestedEpoch() external view returns (uint256 epoch, bytes32 root) {
|
|
uint256 scanned = 0;
|
|
for (uint256 e = latestEpoch; e > 0 && scanned < MAX_EPOCH_SCAN; e--) {
|
|
if (snapshots[e].state == State.Attested) {
|
|
return (e, snapshots[e].root);
|
|
}
|
|
unchecked { scanned++; }
|
|
}
|
|
return (0, bytes32(0));
|
|
}
|
|
}
|