Initial commit
This commit is contained in:
263
contracts/oracle/Aggregator.sol
Normal file
263
contracts/oracle/Aggregator.sol
Normal file
@@ -0,0 +1,263 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
138
contracts/oracle/OracleWithCCIP.sol
Normal file
138
contracts/oracle/OracleWithCCIP.sol
Normal file
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "./Aggregator.sol";
|
||||
import "../ccip/CCIPSender.sol";
|
||||
|
||||
/**
|
||||
* @title Oracle Aggregator with CCIP Integration
|
||||
* @notice Extends Aggregator with CCIP cross-chain messaging capabilities
|
||||
* @dev Automatically sends oracle updates to other chains via CCIP when updates occur
|
||||
*/
|
||||
contract OracleWithCCIP is Aggregator {
|
||||
CCIPSender public ccipSender;
|
||||
bool public ccipEnabled;
|
||||
|
||||
// Destination chain configurations (using CCIPSender's destinations)
|
||||
uint64[] public ccipDestinationChains;
|
||||
|
||||
event CCIPUpdateSent(
|
||||
bytes32 indexed messageId,
|
||||
uint64 indexed destinationChainSelector,
|
||||
uint256 answer,
|
||||
uint256 roundId
|
||||
);
|
||||
event CCIPEnabled(bool enabled);
|
||||
event CCIPSenderUpdated(address oldSender, address newSender);
|
||||
|
||||
constructor(
|
||||
string memory _description,
|
||||
address _admin,
|
||||
uint256 _heartbeat,
|
||||
uint256 _deviationThreshold,
|
||||
address _ccipSender
|
||||
) Aggregator(_description, _admin, _heartbeat, _deviationThreshold) {
|
||||
require(_ccipSender != address(0), "OracleWithCCIP: zero sender address");
|
||||
ccipSender = CCIPSender(_ccipSender);
|
||||
ccipEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update the answer and send to CCIP destinations
|
||||
* @param answer New answer value
|
||||
*/
|
||||
function updateAnswer(uint256 answer) external override 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);
|
||||
|
||||
// Send to CCIP destinations if enabled
|
||||
if (ccipEnabled) {
|
||||
_sendToCCIP(currentRound, answer);
|
||||
}
|
||||
} else {
|
||||
// Update existing round
|
||||
round.updatedAt = block.timestamp;
|
||||
round.transmitter = msg.sender;
|
||||
}
|
||||
|
||||
emit AnswerUpdated(int256(answer), currentRound, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Send oracle update to all CCIP destinations
|
||||
*/
|
||||
function _sendToCCIP(uint256 roundId, uint256 answer) internal {
|
||||
uint64[] memory destinations = ccipSender.getDestinationChains();
|
||||
|
||||
for (uint256 i = 0; i < destinations.length; i++) {
|
||||
uint64 chainSelector = destinations[i];
|
||||
|
||||
try ccipSender.sendOracleUpdate(
|
||||
chainSelector,
|
||||
answer,
|
||||
roundId,
|
||||
block.timestamp
|
||||
) returns (bytes32 messageId) {
|
||||
emit CCIPUpdateSent(messageId, chainSelector, answer, roundId);
|
||||
} catch {
|
||||
// Log error but don't revert
|
||||
// In production, consider adding error tracking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable/disable CCIP
|
||||
*/
|
||||
function setCCIPEnabled(bool enabled) external {
|
||||
require(msg.sender == admin, "OracleWithCCIP: only admin");
|
||||
ccipEnabled = enabled;
|
||||
emit CCIPEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update CCIP sender
|
||||
*/
|
||||
function updateCCIPSender(address newSender) external {
|
||||
require(msg.sender == admin, "OracleWithCCIP: only admin");
|
||||
require(newSender != address(0), "OracleWithCCIP: zero address");
|
||||
|
||||
address oldSender = address(ccipSender);
|
||||
ccipSender = CCIPSender(newSender);
|
||||
|
||||
emit CCIPSenderUpdated(oldSender, newSender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get CCIP destinations
|
||||
*/
|
||||
function getCCIPDestinations() external view returns (uint64[] memory) {
|
||||
return ccipSender.getDestinationChains();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get fee for sending to CCIP destination
|
||||
*/
|
||||
function getCCIPFee(uint64 chainSelector) external view returns (uint256) {
|
||||
bytes memory data = abi.encode(uint256(0), uint256(0), uint256(0));
|
||||
return ccipSender.calculateFee(chainSelector, data);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user