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.
146 lines
6.2 KiB
Solidity
146 lines
6.2 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
/**
|
|
* @title AereFoundationAggregator — Chainlink-compatible single-asset feed
|
|
* @notice One deployment per (asset, denomination) pair. The Foundation
|
|
* pushes price updates (in 8-decimal Chainlink convention) backed
|
|
* by an off-chain attestation pipeline (e.g. fund-administrator
|
|
* signed NAV reports for BUIDL/USDY/OUSG, exchange median for
|
|
* BTC LST de-peg detection, etc.).
|
|
*
|
|
* Exposes the EXACT Chainlink AggregatorV3 surface that
|
|
* AereLendingOracle expects:
|
|
* function latestRoundData() external view returns
|
|
* (uint80 roundId, int256 answer, uint256 startedAt,
|
|
* uint256 updatedAt, uint80 answeredInRound);
|
|
*
|
|
* SAFETY:
|
|
* - `description()` is set at deploy and immutable (e.g. "BUIDL.e/USD").
|
|
* - `decimals()` is fixed at 8 to match the Chainlink ecosystem.
|
|
* - `maxJumpBps`: cap on per-update price move. Set at deploy.
|
|
* Foundation cannot push a single update that exceeds this cap.
|
|
* Forces multi-update walks for any large legitimate move
|
|
* (gives external observers a chance to challenge / freeze).
|
|
* - `minHeartbeat`: minimum seconds between updates. Prevents
|
|
* Foundation from spamming small updates to walk the price
|
|
* rapidly within a single block.
|
|
* - PAUSE: Foundation can pause; while paused, `latestRoundData`
|
|
* returns the LAST GOOD price + stale timestamp so the
|
|
* AereLendingOracle staleness check trips and lending falls
|
|
* back to "no fresh price" → safest behaviour (blocks new
|
|
* borrows, allows liquidations on the pre-pause price).
|
|
*
|
|
* WORKFLOW:
|
|
* 1. Foundation gets off-chain NAV (BUIDL daily, USDY hourly, etc.).
|
|
* 2. Multisig signs `updateAnswer(price)` via Ledger.
|
|
* 3. Contract validates: not paused, ≥ minHeartbeat since last,
|
|
* within maxJumpBps of current price.
|
|
* 4. Round is incremented, answer + updatedAt stored.
|
|
* 5. AereLendingOracle reads via configured feed.
|
|
*/
|
|
contract AereFoundationAggregator is Ownable {
|
|
|
|
/* ------------------------------- immutable ------------------------------- */
|
|
|
|
string public description;
|
|
uint8 public constant decimals = 8;
|
|
uint16 public immutable maxJumpBps; // e.g. 500 = 5%
|
|
uint32 public immutable minHeartbeat; // e.g. 300 = 5 min
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
uint80 public latestRoundId;
|
|
int256 public latestAnswer;
|
|
uint256 public latestUpdatedAt;
|
|
bool public paused;
|
|
|
|
/// @notice roundId → {answer, updatedAt}
|
|
mapping(uint80 => RoundData) internal _rounds;
|
|
struct RoundData { int256 answer; uint256 updatedAt; }
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event AnswerUpdated(int256 indexed answer, uint256 indexed roundId, uint256 updatedAt);
|
|
event PausedSet(bool paused);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error InvalidPrice();
|
|
error HeartbeatTooSoon(uint256 next);
|
|
error JumpExceeded(uint256 deviationBps, uint256 cap);
|
|
error AggregatorPaused();
|
|
error UnknownRound();
|
|
|
|
/* ----------------------------- constructor ------------------------------ */
|
|
|
|
constructor(
|
|
string memory description_,
|
|
uint16 maxJumpBps_,
|
|
uint32 minHeartbeat_,
|
|
int256 initialAnswer
|
|
) {
|
|
require(initialAnswer > 0, "initial price zero");
|
|
require(maxJumpBps_ > 0 && maxJumpBps_ <= 5_000, "jump cap");
|
|
description = description_;
|
|
maxJumpBps = maxJumpBps_;
|
|
minHeartbeat = minHeartbeat_;
|
|
latestRoundId = 1;
|
|
latestAnswer = initialAnswer;
|
|
latestUpdatedAt = block.timestamp;
|
|
_rounds[1] = RoundData(initialAnswer, block.timestamp);
|
|
emit AnswerUpdated(initialAnswer, 1, block.timestamp);
|
|
}
|
|
|
|
/* ----------------------------- price update ----------------------------- */
|
|
|
|
function updateAnswer(int256 newAnswer) external onlyOwner {
|
|
if (paused) revert AggregatorPaused();
|
|
if (newAnswer <= 0) revert InvalidPrice();
|
|
if (block.timestamp < latestUpdatedAt + minHeartbeat) {
|
|
revert HeartbeatTooSoon(latestUpdatedAt + minHeartbeat);
|
|
}
|
|
// Jump check.
|
|
int256 prev = latestAnswer;
|
|
uint256 absDiff;
|
|
if (newAnswer > prev) absDiff = uint256(newAnswer - prev);
|
|
else absDiff = uint256(prev - newAnswer);
|
|
uint256 cap = (uint256(prev) * maxJumpBps) / 10_000;
|
|
if (absDiff > cap) revert JumpExceeded((absDiff * 10_000) / uint256(prev), maxJumpBps);
|
|
|
|
unchecked { latestRoundId += 1; }
|
|
latestAnswer = newAnswer;
|
|
latestUpdatedAt = block.timestamp;
|
|
_rounds[latestRoundId] = RoundData(newAnswer, block.timestamp);
|
|
emit AnswerUpdated(newAnswer, latestRoundId, block.timestamp);
|
|
}
|
|
|
|
function setPaused(bool _paused) external onlyOwner {
|
|
paused = _paused;
|
|
emit PausedSet(_paused);
|
|
}
|
|
|
|
/* ------------------------------ Chainlink view -------------------------- */
|
|
|
|
function latestRoundData() external view returns (
|
|
uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
|
|
) {
|
|
// When paused: still return last good values; the AereLendingOracle's
|
|
// staleness window will eventually expire and tip downstream into
|
|
// safe-mode automatically. We do NOT lie about updatedAt — paused
|
|
// means the upstream attestor stopped vouching, so the on-chain
|
|
// timestamp legitimately freezes.
|
|
return (latestRoundId, latestAnswer, latestUpdatedAt, latestUpdatedAt, latestRoundId);
|
|
}
|
|
|
|
function getRoundData(uint80 roundId) external view returns (
|
|
uint80 _roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
|
|
) {
|
|
RoundData memory r = _rounds[roundId];
|
|
if (r.updatedAt == 0) revert UnknownRound();
|
|
return (roundId, r.answer, r.updatedAt, r.updatedAt, roundId);
|
|
}
|
|
}
|