Initial commit

This commit is contained in:
defiQUG
2025-12-12 14:56:07 -08:00
commit a1466e4005
15 changed files with 2523 additions and 0 deletions

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

View 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);
}
}