264 lines
7.5 KiB
Solidity
264 lines
7.5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.19;
|
|
|
|
/**
|
|
* @title Oracle Aggregator
|
|
* @notice Chainlink-compatible oracle aggregator for price feeds
|
|
* @dev Implements round-based oracle updates with access control
|
|
*/
|
|
contract Aggregator {
|
|
struct Round {
|
|
uint256 answer;
|
|
uint256 startedAt;
|
|
uint256 updatedAt;
|
|
uint256 answeredInRound;
|
|
address transmitter;
|
|
}
|
|
|
|
uint8 public constant decimals = 8;
|
|
string public description;
|
|
|
|
uint256 public version = 1;
|
|
uint256 public latestRound;
|
|
|
|
mapping(uint256 => Round) public rounds;
|
|
|
|
// Access control
|
|
address public admin;
|
|
address[] public transmitters;
|
|
mapping(address => bool) public isTransmitter;
|
|
|
|
// Round parameters
|
|
uint256 public heartbeat;
|
|
uint256 public deviationThreshold; // in basis points (e.g., 50 = 0.5%)
|
|
bool public paused;
|
|
|
|
event AnswerUpdated(
|
|
int256 indexed current,
|
|
uint256 indexed roundId,
|
|
uint256 updatedAt
|
|
);
|
|
event NewRound(
|
|
uint256 indexed roundId,
|
|
address indexed startedBy,
|
|
uint256 startedAt
|
|
);
|
|
event TransmitterAdded(address indexed transmitter);
|
|
event TransmitterRemoved(address indexed transmitter);
|
|
event AdminChanged(address indexed oldAdmin, address indexed newAdmin);
|
|
event HeartbeatUpdated(uint256 oldHeartbeat, uint256 newHeartbeat);
|
|
event DeviationThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
|
|
event Paused(address account);
|
|
event Unpaused(address account);
|
|
|
|
modifier onlyAdmin() {
|
|
require(msg.sender == admin, "Aggregator: only admin");
|
|
_;
|
|
}
|
|
|
|
modifier onlyTransmitter() {
|
|
require(isTransmitter[msg.sender], "Aggregator: only transmitter");
|
|
_;
|
|
}
|
|
|
|
modifier whenNotPaused() {
|
|
require(!paused, "Aggregator: paused");
|
|
_;
|
|
}
|
|
|
|
constructor(
|
|
string memory _description,
|
|
address _admin,
|
|
uint256 _heartbeat,
|
|
uint256 _deviationThreshold
|
|
) {
|
|
description = _description;
|
|
admin = _admin;
|
|
heartbeat = _heartbeat;
|
|
deviationThreshold = _deviationThreshold;
|
|
}
|
|
|
|
/**
|
|
* @notice Update the answer for the current round
|
|
* @param answer New answer value
|
|
*/
|
|
function updateAnswer(uint256 answer) external virtual onlyTransmitter whenNotPaused {
|
|
uint256 currentRound = latestRound;
|
|
Round storage round = rounds[currentRound];
|
|
|
|
// Check if we need to start a new round
|
|
if (round.updatedAt == 0 ||
|
|
block.timestamp >= round.startedAt + heartbeat ||
|
|
shouldUpdate(answer, round.answer)) {
|
|
currentRound = latestRound + 1;
|
|
latestRound = currentRound;
|
|
|
|
rounds[currentRound] = Round({
|
|
answer: answer,
|
|
startedAt: block.timestamp,
|
|
updatedAt: block.timestamp,
|
|
answeredInRound: currentRound,
|
|
transmitter: msg.sender
|
|
});
|
|
|
|
emit NewRound(currentRound, msg.sender, block.timestamp);
|
|
} else {
|
|
// Update existing round (median or weighted average logic can be added)
|
|
round.updatedAt = block.timestamp;
|
|
round.transmitter = msg.sender;
|
|
}
|
|
|
|
emit AnswerUpdated(int256(answer), currentRound, block.timestamp);
|
|
}
|
|
|
|
/**
|
|
* @notice Check if answer should be updated based on deviation threshold
|
|
*/
|
|
function shouldUpdate(uint256 newAnswer, uint256 oldAnswer) internal view returns (bool) {
|
|
if (oldAnswer == 0) return true;
|
|
|
|
uint256 deviation = newAnswer > oldAnswer
|
|
? ((newAnswer - oldAnswer) * 10000) / oldAnswer
|
|
: ((oldAnswer - newAnswer) * 10000) / oldAnswer;
|
|
|
|
return deviation >= deviationThreshold;
|
|
}
|
|
|
|
/**
|
|
* @notice Get the latest answer
|
|
*/
|
|
function latestAnswer() external view returns (int256) {
|
|
return int256(rounds[latestRound].answer);
|
|
}
|
|
|
|
/**
|
|
* @notice Get the latest round data
|
|
*/
|
|
function latestRoundData()
|
|
external
|
|
view
|
|
returns (
|
|
uint80 roundId,
|
|
int256 answer,
|
|
uint256 startedAt,
|
|
uint256 updatedAt,
|
|
uint80 answeredInRound
|
|
)
|
|
{
|
|
Round storage round = rounds[latestRound];
|
|
return (
|
|
uint80(latestRound),
|
|
int256(round.answer),
|
|
round.startedAt,
|
|
round.updatedAt,
|
|
uint80(round.answeredInRound)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Get round data for a specific round
|
|
*/
|
|
function getRoundData(uint80 _roundId)
|
|
external
|
|
view
|
|
returns (
|
|
uint80 roundId,
|
|
int256 answer,
|
|
uint256 startedAt,
|
|
uint256 updatedAt,
|
|
uint80 answeredInRound
|
|
)
|
|
{
|
|
Round storage round = rounds[_roundId];
|
|
require(round.updatedAt > 0, "Aggregator: round not found");
|
|
return (
|
|
_roundId,
|
|
int256(round.answer),
|
|
round.startedAt,
|
|
round.updatedAt,
|
|
uint80(round.answeredInRound)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice Add a transmitter
|
|
*/
|
|
function addTransmitter(address transmitter) external onlyAdmin {
|
|
require(!isTransmitter[transmitter], "Aggregator: already transmitter");
|
|
isTransmitter[transmitter] = true;
|
|
transmitters.push(transmitter);
|
|
emit TransmitterAdded(transmitter);
|
|
}
|
|
|
|
/**
|
|
* @notice Remove a transmitter
|
|
*/
|
|
function removeTransmitter(address transmitter) external onlyAdmin {
|
|
require(isTransmitter[transmitter], "Aggregator: not transmitter");
|
|
isTransmitter[transmitter] = false;
|
|
|
|
// Remove from array
|
|
for (uint256 i = 0; i < transmitters.length; i++) {
|
|
if (transmitters[i] == transmitter) {
|
|
transmitters[i] = transmitters[transmitters.length - 1];
|
|
transmitters.pop();
|
|
break;
|
|
}
|
|
}
|
|
|
|
emit TransmitterRemoved(transmitter);
|
|
}
|
|
|
|
/**
|
|
* @notice Change admin
|
|
*/
|
|
function changeAdmin(address newAdmin) external onlyAdmin {
|
|
require(newAdmin != address(0), "Aggregator: zero address");
|
|
address oldAdmin = admin;
|
|
admin = newAdmin;
|
|
emit AdminChanged(oldAdmin, newAdmin);
|
|
}
|
|
|
|
/**
|
|
* @notice Update heartbeat
|
|
*/
|
|
function updateHeartbeat(uint256 newHeartbeat) external onlyAdmin {
|
|
uint256 oldHeartbeat = heartbeat;
|
|
heartbeat = newHeartbeat;
|
|
emit HeartbeatUpdated(oldHeartbeat, newHeartbeat);
|
|
}
|
|
|
|
/**
|
|
* @notice Update deviation threshold
|
|
*/
|
|
function updateDeviationThreshold(uint256 newThreshold) external onlyAdmin {
|
|
uint256 oldThreshold = deviationThreshold;
|
|
deviationThreshold = newThreshold;
|
|
emit DeviationThresholdUpdated(oldThreshold, newThreshold);
|
|
}
|
|
|
|
/**
|
|
* @notice Pause the aggregator
|
|
*/
|
|
function pause() external onlyAdmin {
|
|
paused = true;
|
|
emit Paused(msg.sender);
|
|
}
|
|
|
|
/**
|
|
* @notice Unpause the aggregator
|
|
*/
|
|
function unpause() external onlyAdmin {
|
|
paused = false;
|
|
emit Unpaused(msg.sender);
|
|
}
|
|
|
|
/**
|
|
* @notice Get list of transmitters
|
|
*/
|
|
function getTransmitters() external view returns (address[] memory) {
|
|
return transmitters;
|
|
}
|
|
}
|
|
|