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.
256 lines
11 KiB
Solidity
256 lines
11 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.23;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
|
|
|
|
/**
|
|
* @title AerePyth
|
|
* @notice Pyth-Network-compatible pull-oracle receiver for AERE Network.
|
|
*
|
|
* Implements Pyth's IPyth interface so applications written against the
|
|
* canonical Pyth SDK work unchanged on AERE. Once AERE is added to the
|
|
* public Pyth network (via PR to pyth-network/pyth-crosschain), the same
|
|
* contract accepts updates signed by Pyth's Guardian set with no migration.
|
|
*
|
|
* In the bootstrap phase, prices are pushed by AERE Foundation's
|
|
* publisher set (initially: our existing oracle reporter container,
|
|
* later: extended to multiple publishers for redundancy). Publishers
|
|
* sign price updates with ECDSA — same signature scheme as AereMessenger.
|
|
*
|
|
* Reference: https://docs.pyth.network/price-feeds/contract-addresses/evm
|
|
* https://github.com/pyth-network/pyth-sdk-solidity
|
|
*/
|
|
|
|
struct PythPrice {
|
|
int64 price;
|
|
uint64 conf;
|
|
int32 expo;
|
|
uint256 publishTime;
|
|
}
|
|
|
|
struct PythPriceFeed {
|
|
bytes32 id;
|
|
PythPrice price;
|
|
PythPrice emaPrice;
|
|
}
|
|
|
|
interface IPyth {
|
|
function getPrice(bytes32 id) external view returns (PythPrice memory price);
|
|
function getEmaPrice(bytes32 id) external view returns (PythPrice memory price);
|
|
function getPriceUnsafe(bytes32 id) external view returns (PythPrice memory price);
|
|
function getEmaPriceUnsafe(bytes32 id) external view returns (PythPrice memory price);
|
|
function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PythPrice memory price);
|
|
function getEmaPriceNoOlderThan(bytes32 id, uint256 age) external view returns (PythPrice memory price);
|
|
function updatePriceFeeds(bytes[] calldata updateData) external payable;
|
|
function updatePriceFeedsIfNecessary(bytes[] calldata updateData, bytes32[] calldata priceIds, uint64[] calldata publishTimes) external payable;
|
|
function getUpdateFee(bytes[] calldata updateData) external view returns (uint256 feeAmount);
|
|
function getValidTimePeriod() external view returns (uint256);
|
|
}
|
|
|
|
contract AerePyth is IPyth, Ownable, ReentrancyGuard {
|
|
/// Authorised publisher set. Each signs price updates with ECDSA.
|
|
mapping(address => bool) public isPublisher;
|
|
address[] public publishers;
|
|
|
|
/// Threshold of distinct publishers required to accept an update.
|
|
uint8 public threshold = 1;
|
|
|
|
/// Stored price feed by id (e.g. keccak256("BTC/USD") or canonical Pyth feed ID).
|
|
mapping(bytes32 => PythPrice) internal _prices;
|
|
mapping(bytes32 => PythPrice) internal _emaPrices;
|
|
|
|
/// Per-update fee in wei AERE. Default 0; owner can raise it later
|
|
/// to fund Foundation infrastructure or distribute back to publishers.
|
|
uint256 public updateFeePerUpdate = 0;
|
|
|
|
/// Maximum age (seconds) a price can be without being considered stale.
|
|
uint256 public validTimePeriod = 60;
|
|
|
|
event PriceUpdated(bytes32 indexed id, int64 price, uint64 conf, int32 expo, uint256 publishTime);
|
|
event PublisherAdded(address indexed publisher);
|
|
event PublisherRemoved(address indexed publisher);
|
|
event ThresholdChanged(uint8 newThreshold);
|
|
event FeeChanged(uint256 newFee);
|
|
|
|
// ───────────────────── Admin ─────────────────────
|
|
|
|
function addPublisher(address p) external onlyOwner {
|
|
require(!isPublisher[p], "AerePyth: already");
|
|
isPublisher[p] = true;
|
|
publishers.push(p);
|
|
emit PublisherAdded(p);
|
|
}
|
|
|
|
function removePublisher(address p) external onlyOwner {
|
|
require(isPublisher[p], "AerePyth: not found");
|
|
isPublisher[p] = false;
|
|
for (uint256 i = 0; i < publishers.length; i++) {
|
|
if (publishers[i] == p) {
|
|
publishers[i] = publishers[publishers.length - 1];
|
|
publishers.pop();
|
|
break;
|
|
}
|
|
}
|
|
emit PublisherRemoved(p);
|
|
}
|
|
|
|
function setThreshold(uint8 _t) external onlyOwner {
|
|
require(_t > 0 && _t <= publishers.length, "AerePyth: bad threshold");
|
|
threshold = _t;
|
|
emit ThresholdChanged(_t);
|
|
}
|
|
|
|
function setUpdateFee(uint256 _fee) external onlyOwner {
|
|
updateFeePerUpdate = _fee;
|
|
emit FeeChanged(_fee);
|
|
}
|
|
|
|
function setValidTimePeriod(uint256 _seconds) external onlyOwner {
|
|
validTimePeriod = _seconds;
|
|
}
|
|
|
|
function withdrawFees(address payable to, uint256 amount) external onlyOwner {
|
|
(bool ok, ) = to.call{ value: amount }("");
|
|
require(ok, "AerePyth: withdraw failed");
|
|
}
|
|
|
|
function publisherCount() external view returns (uint256) {
|
|
return publishers.length;
|
|
}
|
|
|
|
// ───────────────────── Reads (IPyth) ─────────────────────
|
|
|
|
function getPrice(bytes32 id) external view override returns (PythPrice memory price) {
|
|
price = _prices[id];
|
|
require(price.publishTime > 0, "AerePyth: feed unknown");
|
|
require(block.timestamp - price.publishTime <= validTimePeriod, "AerePyth: stale");
|
|
}
|
|
|
|
function getPriceUnsafe(bytes32 id) external view override returns (PythPrice memory price) {
|
|
price = _prices[id];
|
|
require(price.publishTime > 0, "AerePyth: feed unknown");
|
|
}
|
|
|
|
function getEmaPrice(bytes32 id) external view override returns (PythPrice memory price) {
|
|
price = _emaPrices[id];
|
|
require(price.publishTime > 0, "AerePyth: ema unknown");
|
|
require(block.timestamp - price.publishTime <= validTimePeriod, "AerePyth: stale");
|
|
}
|
|
|
|
function getEmaPriceUnsafe(bytes32 id) external view override returns (PythPrice memory price) {
|
|
price = _emaPrices[id];
|
|
require(price.publishTime > 0, "AerePyth: ema unknown");
|
|
}
|
|
|
|
function getPriceNoOlderThan(bytes32 id, uint256 age) external view override returns (PythPrice memory price) {
|
|
price = _prices[id];
|
|
require(price.publishTime > 0, "AerePyth: feed unknown");
|
|
require(block.timestamp - price.publishTime <= age, "AerePyth: older than allowed");
|
|
}
|
|
|
|
function getEmaPriceNoOlderThan(bytes32 id, uint256 age) external view override returns (PythPrice memory price) {
|
|
price = _emaPrices[id];
|
|
require(price.publishTime > 0, "AerePyth: ema unknown");
|
|
require(block.timestamp - price.publishTime <= age, "AerePyth: older than allowed");
|
|
}
|
|
|
|
function getValidTimePeriod() external view override returns (uint256) {
|
|
return validTimePeriod;
|
|
}
|
|
|
|
// ───────────────────── Updates ─────────────────────
|
|
|
|
/**
|
|
* @notice Pyth-style pull-oracle update.
|
|
* @dev Each `updateData[i]` is the abi-encoded tuple
|
|
* (priceFeed, signatures)
|
|
* where signatures is the concatenated 65-byte ECDSA signatures
|
|
* of the publisher set over keccak256(abi.encode(priceFeed)).
|
|
*/
|
|
function updatePriceFeeds(bytes[] calldata updateData) external payable override nonReentrant {
|
|
uint256 required = updateFeePerUpdate * updateData.length;
|
|
require(msg.value >= required, "AerePyth: insufficient fee");
|
|
|
|
for (uint256 i = 0; i < updateData.length; i++) {
|
|
_applyUpdate(updateData[i]);
|
|
}
|
|
}
|
|
|
|
function updatePriceFeedsIfNecessary(
|
|
bytes[] calldata updateData,
|
|
bytes32[] calldata priceIds,
|
|
uint64[] calldata publishTimes
|
|
) external payable override nonReentrant {
|
|
require(priceIds.length == publishTimes.length, "AerePyth: length mismatch");
|
|
|
|
// Determine which updates are actually needed (publishTimes are newer than what we have).
|
|
for (uint256 i = 0; i < priceIds.length; i++) {
|
|
if (_prices[priceIds[i]].publishTime < publishTimes[i]) {
|
|
// Walk updateData looking for the matching id and apply it.
|
|
// Caller is expected to send only the relevant updates in updateData order.
|
|
for (uint256 j = 0; j < updateData.length; j++) {
|
|
PythPriceFeed memory feed = abi.decode(updateData[j][:_payloadLength(updateData[j])], (PythPriceFeed));
|
|
if (feed.id == priceIds[i]) {
|
|
_applyUpdate(updateData[j]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function _payloadLength(bytes calldata data) internal pure returns (uint256) {
|
|
// Update layout: <PythPriceFeed encoded> + <signatures (each 65 bytes)>
|
|
// The encoded PythPriceFeed length is fixed (struct with no dynamic arrays).
|
|
// We compute by stripping signatures from the tail.
|
|
uint256 sigsTail = ((data.length - 64) / 65) * 65;
|
|
return data.length - sigsTail;
|
|
}
|
|
|
|
function _applyUpdate(bytes calldata data) internal {
|
|
// Decode the price feed from the head of the payload, then verify
|
|
// threshold signatures over the digest of the encoded feed.
|
|
uint256 headLen = _payloadLength(data);
|
|
PythPriceFeed memory feed = abi.decode(data[:headLen], (PythPriceFeed));
|
|
bytes calldata sigs = data[headLen:];
|
|
require(sigs.length % 65 == 0, "AerePyth: bad sig length");
|
|
uint256 sigCount = sigs.length / 65;
|
|
require(sigCount >= threshold, "AerePyth: below threshold");
|
|
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(abi.encode(feed))));
|
|
|
|
address[] memory seen = new address[](sigCount);
|
|
uint256 validCount = 0;
|
|
for (uint256 i = 0; i < sigCount; i++) {
|
|
bytes32 r; bytes32 s; uint8 v;
|
|
assembly {
|
|
let off := add(sigs.offset, mul(i, 65))
|
|
r := calldataload(off)
|
|
s := calldataload(add(off, 32))
|
|
v := byte(0, calldataload(add(off, 64)))
|
|
}
|
|
address signer = ecrecover(digest, v, r, s);
|
|
require(signer != address(0) && isPublisher[signer], "AerePyth: bad publisher");
|
|
for (uint256 j = 0; j < validCount; j++) {
|
|
require(seen[j] != signer, "AerePyth: duplicate signer");
|
|
}
|
|
seen[validCount] = signer;
|
|
validCount++;
|
|
}
|
|
|
|
// Only apply if newer than what we have.
|
|
if (feed.price.publishTime > _prices[feed.id].publishTime) {
|
|
_prices[feed.id] = feed.price;
|
|
_emaPrices[feed.id] = feed.emaPrice;
|
|
emit PriceUpdated(feed.id, feed.price.price, feed.price.conf, feed.price.expo, feed.price.publishTime);
|
|
}
|
|
}
|
|
|
|
function getUpdateFee(bytes[] calldata updateData) external view override returns (uint256 feeAmount) {
|
|
return updateFeePerUpdate * updateData.length;
|
|
}
|
|
|
|
receive() external payable {}
|
|
}
|