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.
149 lines
6.4 KiB
Solidity
149 lines
6.4 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
/**
|
|
* @title AereOracleV2
|
|
* @notice Bug-fix redeploy of the legacy AereOracle (V1 at
|
|
* 0xf0A13823A4bFa86358Fe30aaf1f44A36AcbCf399).
|
|
*
|
|
* Multi-feed price oracle with a reporter quorum. Reporters submit price
|
|
* updates per symbol (e.g. "AERE/USD", "BTC/USD"). The on-chain price is the
|
|
* median of the latest FRESH submission from each authorized reporter.
|
|
*
|
|
* ──────────────────────── WHY V2 (finding #11) ────────────────────────
|
|
* V1.getPrice returned the median even when only ONE reporter was fresh
|
|
* (contributors == 1). With the others stale, a single fresh reporter fully
|
|
* controlled the feed — and the live V1 ran at reporterCount == 1, so the
|
|
* median was always a single key's number. There was no on-chain minimum
|
|
* quorum and no setter to raise one.
|
|
*
|
|
* V2 fix — a QUORUM-STALENESS guard:
|
|
* getPrice reverts InsufficientQuorum unless at least `minContributors`
|
|
* reporters are BOTH authorized AND fresh (within maxStaleness). Stale
|
|
* submissions are still discarded exactly as in V1, so the quorum can only
|
|
* be met by that many independently-fresh reporters — a single fresh
|
|
* reporter (or any number below the quorum) can no longer set the price.
|
|
* `minContributors` is configurable by the owner (Foundation), floored at 1,
|
|
* and defaults to 3 for a real median.
|
|
*
|
|
* The reporter model, submit(), the getPrice return signature
|
|
* (uint128 price, uint64 timestamp, uint256 contributors), DECIMALS,
|
|
* maxStaleness and the median math are otherwise identical to V1, so V2 is a
|
|
* drop-in replacement for any consumer once the quorum is operationally met.
|
|
*/
|
|
contract AereOracleV2 is Ownable {
|
|
uint8 public constant DECIMALS = 8;
|
|
uint64 public maxStaleness = 5 minutes;
|
|
|
|
/// Minimum number of FRESH, authorized reporters required for getPrice to
|
|
/// return a price. Invariant: minContributors >= 1. Default 3.
|
|
uint256 public minContributors;
|
|
|
|
address[] public reporters;
|
|
mapping(address => bool) public isReporter;
|
|
|
|
struct Submission {
|
|
uint128 price; // scaled to 8 decimals
|
|
uint64 timestamp;
|
|
}
|
|
|
|
// symbol => reporter => submission
|
|
mapping(bytes32 => mapping(address => Submission)) public submissions;
|
|
bytes32[] public symbols;
|
|
mapping(bytes32 => bool) private _knownSymbol;
|
|
|
|
event ReporterAdded(address indexed reporter);
|
|
event ReporterRemoved(address indexed reporter);
|
|
event PriceSubmitted(bytes32 indexed symbol, address indexed reporter, uint128 price, uint64 timestamp);
|
|
event SymbolAdded(bytes32 indexed symbol);
|
|
event MaxStalenessUpdated(uint64 newValue);
|
|
event MinContributorsUpdated(uint256 newValue);
|
|
|
|
error InsufficientQuorum(uint256 fresh, uint256 required);
|
|
|
|
constructor(uint256 _minContributors) {
|
|
require(_minContributors >= 1, "minContributors");
|
|
minContributors = _minContributors;
|
|
emit MinContributorsUpdated(_minContributors);
|
|
}
|
|
|
|
function addReporter(address r) external onlyOwner {
|
|
require(r != address(0) && !isReporter[r], "bad");
|
|
isReporter[r] = true;
|
|
reporters.push(r);
|
|
emit ReporterAdded(r);
|
|
}
|
|
|
|
function removeReporter(address r) external onlyOwner {
|
|
require(isReporter[r], "not reporter");
|
|
isReporter[r] = false;
|
|
for (uint256 i = 0; i < reporters.length; i++) {
|
|
if (reporters[i] == r) {
|
|
reporters[i] = reporters[reporters.length - 1];
|
|
reporters.pop();
|
|
break;
|
|
}
|
|
}
|
|
emit ReporterRemoved(r);
|
|
}
|
|
|
|
function setMaxStaleness(uint64 v) external onlyOwner {
|
|
require(v >= 30 seconds && v <= 1 hours, "range");
|
|
maxStaleness = v;
|
|
emit MaxStalenessUpdated(v);
|
|
}
|
|
|
|
/// @notice Raise/lower the required fresh-reporter quorum. Floored at 1.
|
|
/// Set this to <= the number of independently-operated reporters the
|
|
/// Foundation actually runs before repointing consumers here.
|
|
function setMinContributors(uint256 v) external onlyOwner {
|
|
require(v >= 1, "minContributors");
|
|
minContributors = v;
|
|
emit MinContributorsUpdated(v);
|
|
}
|
|
|
|
function submit(bytes32 symbol, uint128 price) external {
|
|
require(isReporter[msg.sender], "not reporter");
|
|
require(price > 0, "price");
|
|
if (!_knownSymbol[symbol]) { _knownSymbol[symbol] = true; symbols.push(symbol); emit SymbolAdded(symbol); }
|
|
submissions[symbol][msg.sender] = Submission(price, uint64(block.timestamp));
|
|
emit PriceSubmitted(symbol, msg.sender, price, uint64(block.timestamp));
|
|
}
|
|
|
|
/// @notice Returns the median fresh price for a symbol and the count of
|
|
/// contributors. Reverts InsufficientQuorum unless at least
|
|
/// `minContributors` authorized reporters are fresh.
|
|
function getPrice(bytes32 symbol) external view returns (uint128 price, uint64 timestamp, uint256 contributors) {
|
|
uint256 n = reporters.length;
|
|
uint128[] memory fresh = new uint128[](n);
|
|
uint64 newest = 0;
|
|
uint256 count = 0;
|
|
|
|
for (uint256 i = 0; i < n; i++) {
|
|
Submission memory s = submissions[symbol][reporters[i]];
|
|
if (s.timestamp == 0) continue;
|
|
if (block.timestamp - s.timestamp > maxStaleness) continue;
|
|
fresh[count++] = s.price;
|
|
if (s.timestamp > newest) newest = s.timestamp;
|
|
}
|
|
// QUORUM-STALENESS GUARD: require a real quorum of FRESH reporters. This
|
|
// is the fix over V1, which required only count > 0.
|
|
if (count < minContributors) revert InsufficientQuorum(count, minContributors);
|
|
|
|
// insertion sort (small n in practice)
|
|
for (uint256 i = 1; i < count; i++) {
|
|
uint128 key = fresh[i];
|
|
uint256 j = i;
|
|
while (j > 0 && fresh[j - 1] > key) { fresh[j] = fresh[j - 1]; j--; }
|
|
fresh[j] = key;
|
|
}
|
|
price = (count % 2 == 1) ? fresh[count / 2] : uint128((uint256(fresh[count / 2 - 1]) + uint256(fresh[count / 2])) / 2);
|
|
return (price, newest, count);
|
|
}
|
|
|
|
function symbolCount() external view returns (uint256) { return symbols.length; }
|
|
function reporterCount() external view returns (uint256) { return reporters.length; }
|
|
}
|