// 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; } }