Description:
Governance contract for decentralized decision-making.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/DualAggregator.sol": {
"content": "// SPDX-License-Identifier: BUSL 1.1
pragma solidity 0.8.24;
import {AccessControllerInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AccessControllerInterface.sol";
import {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
import {AggregatorValidatorInterface} from
"@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorValidatorInterface.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {SimpleReadAccessController} from "@chainlink/contracts/src/v0.8/shared/access/SimpleReadAccessController.sol";
import {CallWithExactGas} from "@chainlink/contracts/src/v0.8/shared/call/CallWithExactGas.sol";
import {OCR2Abstract} from "@chainlink/contracts/src/v0.8/shared/ocr2/OCR2Abstract.sol";
// This contract is a port of OCR2Aggregator from `libocr` it is being used
// for a new feeds based project that is ongoing, there will be some modernization
// that happens to this contract as the project progresses.
// solhint-disable max-states-count
contract DualAggregator is OCR2Abstract, AggregatorV2V3Interface, SimpleReadAccessController {
string public constant override typeAndVersion = "DualAggregator 1.0.0";
// This contract is divided into sections. Each section defines a set of
// variables, events, and functions that belong together.
// ================================================================
// │ Variables used in multiple other sections │
// ================================================================
// Transmitter information.
struct Transmitter {
bool active; // ─────────╮ True if active.
uint8 index; // │ Index in `s_transmittersList`.
// │
// │ Juels-denominated payment for transmitters, covering gas costs incurred
// │ by the transmitter plus additional rewards. The entire LINK supply (1e9
uint96 paymentJuels; // ─╯ LINK = 1e27 Juels) will always fit into a uint96.
}
// Signer information.
struct Signer {
bool active; // ─╮ True if active.
uint8 index; // ─╯ Index of oracle in `s_signersList`.
}
// Storing these fields used on the hot path in a HotVars variable reduces the
// retrieval of all of them to one SLOAD.
struct HotVars {
uint8 f; // ─────────────────────────╮ Maximum number of faulty oracles.
// │
uint40 latestEpochAndRound; // │ Epoch and round from OCR protocol,
// │ 32 most significant bits for epoch, 8 least sig bits for round.
// │
uint32 latestAggregatorRoundId; // │ Chainlink Aggregators expose a roundId to consumers. The offchain protocol
// │ does not use this id anywhere. We increment it whenever a new transmission
// │ is made to provide callers with contiguous ids for successive reports.
uint32 latestSecondaryRoundId; // │ Latest transmission round arrived from the Secondary Proxy.
uint32 maximumGasPriceGwei; // │ Highest compensated gas price, in gwei uints.
uint32 reasonableGasPriceGwei; // │ If gas price is less (in gwei units), transmitter gets half the savings.
uint32 observationPaymentGjuels; // │ Fixed LINK reward for each observer.
uint32 transmissionPaymentGjuels; // │ Fixed reward for transmitter.
bool isLatestSecondary; // ───────────╯ Whether the latest report was secondary or not
}
/// @notice mapping containing the transmitter information of each transmitter address.
mapping(address transmitterAddress => Transmitter transmitter) internal s_transmitters;
/// @notice mapping containing the signer information of each signer address.
mapping(address signerAddress => Signer signer) internal s_signers;
/// @notice s_signersList contains the signing address of each oracle.
address[] internal s_signersList;
/// @notice s_transmittersList contains the transmission address of each oracle,
/// i.e. the address the oracle actually sends transactions to the contract from.
address[] internal s_transmittersList;
/// @notice We assume that all oracles contribute observations to all rounds. This
/// variable tracks (per-oracle) from what round an oracle should be rewarded,
/// i.e. the oracle gets (latestAggregatorRoundId - rewardFromAggregatorRoundId) * reward.
uint32[MAX_NUM_ORACLES] internal s_rewardFromAggregatorRoundId;
/// @notice latest setted config.
bytes32 internal s_latestConfigDigest;
/// @notice overhead incurred by accounting logic.
uint24 internal s_accountingGas;
/// @notice most common fields used on the hot path.
HotVars internal s_hotVars;
/// @notice lowest answer the system is allowed to report in response to transmissions.
int192 internal immutable i_minAnswer;
/// @notice highest answer the system is allowed to report in response to transmissions.
int192 internal immutable i_maxAnswer;
/// @param link address of the LINK contract.
/// @param minAnswer_ lowest answer the median of a report is allowed to be.
/// @param maxAnswer_ highest answer the median of a report is allowed to be.
/// @param billingAccessController access controller for managing the billing.
/// @param requesterAccessController access controller for requesting new rounds.
/// @param decimals_ answers are stored in fixed-point format, with this many digits of precision.
/// @param description_ short human-readable description of observable this contract's answers pertain to.
/// @param secondaryProxy_ proxy address to manage the secondary reports.
/// @param cutoffTime_ timetamp to define the window in which a secondary report is valid.
/// @param maxSyncIterations_ max iterations the secondary proxy will be able to loop to sync with the primary rounds.
constructor(
LinkTokenInterface link,
int192 minAnswer_,
int192 maxAnswer_,
AccessControllerInterface billingAccessController,
AccessControllerInterface requesterAccessController,
uint8 decimals_,
string memory description_,
address secondaryProxy_,
uint32 cutoffTime_,
uint32 maxSyncIterations_
) {
i_decimals = decimals_;
i_minAnswer = minAnswer_;
i_maxAnswer = maxAnswer_;
i_secondaryProxy = secondaryProxy_;
i_maxSyncIterations = maxSyncIterations_;
s_linkToken = link;
emit LinkTokenSet(LinkTokenInterface(address(0)), link);
_setBillingAccessController(billingAccessController);
setRequesterAccessController(requesterAccessController);
setValidatorConfig(AggregatorValidatorInterface(address(0x0)), 0);
s_cutoffTime = cutoffTime_;
emit CutoffTimeSet(cutoffTime_);
s_description = description_;
}
// ================================================================
// │ OCR2Abstract Configuration │
// ================================================================
// SetConfig information
struct SetConfigArgs {
uint64 offchainConfigVersion; // ─╮ OffchainConfig version.
uint8 f; // ──────────────────────╯ Faulty Oracles amount.
bytes onchainConfig; // Onchain configuration.
bytes offchainConfig; // Offchain configuration.
address[] signers; // Signing addresses of each oracle.
address[] transmitters; // Transmitting addresses of each oracle.
}
error FMustBePositive();
error TooManyOracles();
error OracleLengthMismatch();
error FaultyOracleFTooHigh();
error InvalidOnChainConfig();
error RepeatedSignerAddress();
error RepeatedTransmitterAddress();
/// @notice incremented each time a new config is posted. This count is incorporated
/// into the config digest to prevent replay attacks.
uint32 internal s_configCount;
/// @notice makes it easier for offchain systems to extract config from logs.
uint32 internal s_latestConfigBlockNumber;
/// @notice check if `f` is a positive number.
/// @dev left as a function so this check can be disabled in derived contracts.
/// @param f amount of faulty oracles to check.
function _requirePositiveF(
uint256 f
) internal pure virtual {
if (f <= 0) {
revert FMustBePositive();
}
}
/// @inheritdoc OCR2Abstract
function setConfig(
address[] memory signers,
address[] memory transmitters,
uint8 f,
bytes memory onchainConfig,
uint64 offchainConfigVersion,
bytes memory offchainConfig
) external override onlyOwner {
if (signers.length > MAX_NUM_ORACLES) {
revert TooManyOracles();
}
if (signers.length != transmitters.length) {
revert OracleLengthMismatch();
}
if (3 * f >= signers.length) {
revert FaultyOracleFTooHigh();
}
_requirePositiveF(f);
if (keccak256(onchainConfig) != keccak256(abi.encodePacked(uint8(1), /*version*/ i_minAnswer, i_maxAnswer))) {
revert InvalidOnChainConfig();
}
SetConfigArgs memory args = SetConfigArgs({
signers: signers,
transmitters: transmitters,
f: f,
onchainConfig: onchainConfig,
offchainConfigVersion: offchainConfigVersion,
offchainConfig: offchainConfig
});
s_hotVars.latestEpochAndRound = 0;
_payOracles();
// Remove any old signer/transmitter addresses.
uint256 oldLength = s_signersList.length;
for (uint256 i = 0; i < oldLength; ++i) {
address signer = s_signersList[i];
address transmitter = s_transmittersList[i];
delete s_signers[signer];
delete s_transmitters[transmitter];
}
delete s_signersList;
delete s_transmittersList;
// Add new signer/transmitter addresses.
for (uint256 i = 0; i < args.signers.length; ++i) {
if (s_signers[args.signers[i]].active) {
revert RepeatedSignerAddress();
}
s_signers[args.signers[i]] = Signer({active: true, index: uint8(i)});
if (s_transmitters[args.transmitters[i]].active) {
revert RepeatedTransmitterAddress();
}
s_transmitters[args.transmitters[i]] = Transmitter({active: true, index: uint8(i), paymentJuels: 0});
}
s_signersList = args.signers;
s_transmittersList = args.transmitters;
s_hotVars.f = args.f;
uint32 previousConfigBlockNumber = s_latestConfigBlockNumber;
s_latestConfigBlockNumber = uint32(block.number);
s_configCount += 1;
s_latestConfigDigest = _configDigestFromConfigData(
block.chainid,
address(this),
s_configCount,
args.signers,
args.transmitters,
args.f,
args.onchainConfig,
args.offchainConfigVersion,
args.offchainConfig
);
emit ConfigSet(
previousConfigBlockNumber,
s_latestConfigDigest,
s_configCount,
args.signers,
args.transmitters,
args.f,
args.onchainConfig,
args.offchainConfigVersion,
args.offchainConfig
);
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
for (uint256 i = 0; i < args.signers.length; ++i) {
s_rewardFromAggregatorRoundId[i] = latestAggregatorRoundId;
}
}
/// @inheritdoc OCR2Abstract
function latestConfigDetails()
external
view
override
returns (uint32 configCount, uint32 blockNumber, bytes32 configDigest)
{
return (s_configCount, s_latestConfigBlockNumber, s_latestConfigDigest);
}
/// @notice get the transmitters list.
/// @dev The list will match the order used to specify the transmitter during setConfig.
/// @return s_transmittersList list of addresses permitted to transmit reports to this contract.
function getTransmitters() external view returns (address[] memory) {
return s_transmittersList;
}
/// @notice Get the mininum answer value.
/// @return minAnswer the lowest answer the system is allowed to report in a transmission.
function minAnswer() public view returns (int256) {
return i_minAnswer;
}
/// @notice Get the maximum answer value.
/// @return maxAnswer the highest answer the system is allowed to report in a transmission.
function maxAnswer() public view returns (int256) {
return i_maxAnswer;
}
// ================================================================
// │ Onchain Validation │
// ================================================================
// Configuration for validator.
struct ValidatorConfig {
AggregatorValidatorInterface validator; // ─╮ Validator contract interface.
uint32 gasLimit; // ────────────────────────╯ Gas limit defined for the validation call.
}
/// @notice indicates that the validator configuration has been set.
/// @param previousValidator previous validator contract.
/// @param previousGasLimit previous gas limit for validate calls.
/// @param currentValidator current validator contract.
/// @param currentGasLimit current gas limit for validate calls.
event ValidatorConfigSet(
AggregatorValidatorInterface indexed previousValidator,
uint32 previousGasLimit,
AggregatorValidatorInterface indexed currentValidator,
uint32 currentGasLimit
);
error InsufficientGas();
/// @notice contstant exact gas cushion defined to do a call.
uint16 private constant CALL_WITH_EXACT_GAS_CUSHION = 5_000;
/// @notice validator configuration.
ValidatorConfig private s_validatorConfig;
/// @notice get the validator configuration.
/// @return validator validator contract.
/// @return gasLimit gas limit for validate calls.
function getValidatorConfig() external view returns (AggregatorValidatorInterface validator, uint32 gasLimit) {
ValidatorConfig memory vc = s_validatorConfig;
return (vc.validator, vc.gasLimit);
}
/// @notice sets validator configuration.
/// @dev set newValidator to 0x0 to disable validate calls.
/// @param newValidator address of the new validator contract.
/// @param newGasLimit new gas limit for validate calls.
function setValidatorConfig(AggregatorValidatorInterface newValidator, uint32 newGasLimit) public onlyOwner {
ValidatorConfig memory previous = s_validatorConfig;
if (previous.validator != newValidator || previous.gasLimit != newGasLimit) {
s_validatorConfig = ValidatorConfig({validator: newValidator, gasLimit: newGasLimit});
emit ValidatorConfigSet(previous.validator, previous.gasLimit, newValidator, newGasLimit);
}
}
/// @notice validate the answer against the validator configuration.
/// @param aggregatorRoundId report round id to validate.
/// @param answer report answer to validate.
function _validateAnswer(uint32 aggregatorRoundId, int256 answer) private {
ValidatorConfig memory vc = s_validatorConfig;
if (address(vc.validator) == address(0)) {
return;
}
uint32 prevAggregatorRoundId = aggregatorRoundId - 1;
int256 prevAggregatorRoundAnswer = s_transmissions[prevAggregatorRoundId].answer;
(, bool sufficientGas) = CallWithExactGas._callWithExactGasEvenIfTargetIsNoContract(
abi.encodeCall(
AggregatorValidatorInterface.validate,
(uint256(prevAggregatorRoundId), prevAggregatorRoundAnswer, uint256(aggregatorRoundId), answer)
),
address(vc.validator),
vc.gasLimit,
CALL_WITH_EXACT_GAS_CUSHION
);
if (!sufficientGas) {
revert InsufficientGas();
}
}
// ================================================================
// │ RequestNewRound │
// ================================================================
/// @notice contract address with AccessController Interface.
AccessControllerInterface internal s_requesterAccessController;
/// @notice emitted when a new requester access controller contract is set.
/// @param old the address prior to the current setting.
/// @param current the address of the new access controller contract.
event RequesterAccessControllerSet(AccessControllerInterface old, AccessControllerInterface current);
/// @notice emitted to immediately request a new round.
/// @param requester the address of the requester.
/// @param configDigest the latest transmission's configDigest.
/// @param epoch the latest transmission's epoch.
/// @param round the latest transmission's round.
event RoundRequested(address indexed requester, bytes32 configDigest, uint32 epoch, uint8 round);
error OnlyOwnerAndRequesterCanCall();
/// @notice address of the requester access controller contract.
/// @return s_requesterAccessController requester access controller address.
function getRequesterAccessController() external view returns (AccessControllerInterface) {
return s_requesterAccessController;
}
/// @notice sets the new requester access controller.
/// @param requesterAccessController designates the address of the new requester access controller.
function setRequesterAccessController(
AccessControllerInterface requesterAccessController
) public onlyOwner {
AccessControllerInterface oldController = s_requesterAccessController;
if (requesterAccessController != oldController) {
s_requesterAccessController = AccessControllerInterface(requesterAccessController);
emit RequesterAccessControllerSet(oldController, requesterAccessController);
}
}
/// @notice immediately requests a new round.
/// @dev access control provided by requesterAccessController.
/// @return aggregatorRoundId round id of the next round. Note: The report for this round may have been
/// transmitted (but not yet mined) *before* requestNewRound() was even called. There is *no*
/// guarantee of causality between the request and the report at aggregatorRoundId.
function requestNewRound() external returns (uint80) {
if (msg.sender != owner() && !s_requesterAccessController.hasAccess(msg.sender, msg.data)) {
revert OnlyOwnerAndRequesterCanCall();
}
uint40 latestEpochAndRound = s_hotVars.latestEpochAndRound;
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
emit RoundRequested(msg.sender, s_latestConfigDigest, uint32(latestEpochAndRound >> 8), uint8(latestEpochAndRound));
return latestAggregatorRoundId + 1;
}
// ================================================================
// │ Secondary Proxy │
// ================================================================
// Used to relieve stack pressure in transmit.
struct Report {
int192 juelsPerFeeCoin; // ───────╮ Exchange rate between feeCoin (e.g. ETH on Ethereum) and LINK, denominated in juels.
uint32 observationsTimestamp; // ─╯ Timestamp when the observations were made offchain.
bytes observers; // i-th element is the index of the ith observer.
int192[] observations; // i-th element is the ith observation.
}
// Transmission records the median answer from the transmit transaction at
// time timestamp.
struct Transmission {
int192 answer; // ───────────────╮ 192 bits ought to be enough for anyone.
uint32 observationsTimestamp; // │ When were observations made offchain.
uint32 recordedTimestamp; // ────╯ When was report received onchain.
}
/// @notice indicates that a new report arrived from the secondary feed and the round id was updated.
/// @param secondaryRoundId the new secondary round id.
event SecondaryRoundIdUpdated(uint32 indexed secondaryRoundId);
/// @notice indicates that a new report arrived from the primary feed and the report had already been stored .
/// @param primaryRoundId the new primary round id (if we're at the next block since the report it should be the same).
event PrimaryFeedUnlocked(uint32 indexed primaryRoundId);
/// @notice emitted when a new cutoff time is set.
/// @param cutoffTime the new defined cutoff time.
event CutoffTimeSet(uint32 cutoffTime);
/// @notice revert when the loop reaches the max sync iterations amount.
error MaxSyncIterationsReached();
/// @notice mapping containing the Transmission records of each round id.
mapping(uint32 aggregatorRoundId => Transmission transmission) internal s_transmissions;
/// @notice secondary proxy address, used to detect who's calling the contract methods.
address internal immutable i_secondaryProxy;
/// @notice cutoff time defines the time window in which a secondary report is valid.
uint32 internal s_cutoffTime;
/// @notice max iterations the secondary proxy will be able to loop to sync with the primary rounds.
uint32 internal immutable i_maxSyncIterations;
/// @notice sets the max time cutoff.
/// @param _cutoffTime new max cutoff timestamp.
function setCutoffTime(
uint32 _cutoffTime
) external onlyOwner {
s_cutoffTime = _cutoffTime;
emit CutoffTimeSet(_cutoffTime);
}
/// @notice check if a report has already been transmitted.
/// @param report the report to check.
/// @return exist whether the report exist or not.
/// @return roundId the round id where the report was found.
function _doesReportExist(
Report memory report
) internal view returns (bool exist, uint32 roundId) {
// Get the latest round id.
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
for (uint32 round_ = latestAggregatorRoundId; round_ > 0; --round_) {
// In case the loop reaches the max iterations revert it, the
// function is not able to check if the report exists or not
if (latestAggregatorRoundId - round_ == i_maxSyncIterations) {
revert MaxSyncIterationsReached();
}
Transmission memory transmission = s_transmissions[round_];
if (transmission.observationsTimestamp < report.observationsTimestamp) {
return (false, 0);
}
if (
transmission.observationsTimestamp == report.observationsTimestamp
&& transmission.answer == report.observations[report.observations.length / 2]
) {
return (true, round_);
}
}
return (false, 0);
}
/// @notice sync data with the primary rounds, return the freshest valid round id.
/// @return roundId synced round id with the primary feed.
function _getSyncPrimaryRound() internal view returns (uint32 roundId) {
// Get the latest round id and the max iterations.
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
// Decreasing loop from the latest primary round id.
for (uint32 round_ = latestAggregatorRoundId; round_ > 0; --round_) {
// In case the loop reached the maxIterations, break it.
if (latestAggregatorRoundId - round_ == i_maxSyncIterations) {
break;
}
// Check if this round does not accomplish the cutoff time condition.
if (s_transmissions[round_].recordedTimestamp + s_cutoffTime < block.timestamp) {
return round_;
}
}
// If the loop couldn't find a match, return the latest secondary round id.
return s_hotVars.latestSecondaryRoundId;
}
/// @notice aggregator round in which the latest report was conceded depending on the caller.
/// @return roundId the latest valid round id.
function _getLatestRound() internal view returns (uint32) {
// Get the latest round ids.
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
uint32 latestSecondaryRoundId = s_hotVars.latestSecondaryRoundId;
bool isLatestSecondary = s_hotVars.isLatestSecondary;
// Check if the message sender is the secondary proxy.
if (msg.sender == i_secondaryProxy) {
// In case the latest secondary round does not accomplish the cutoff time condition,
// get the round id syncing with the primary rounds.
if (s_transmissions[latestSecondaryRoundId].recordedTimestamp + s_cutoffTime < block.timestamp) {
return _getSyncPrimaryRound();
}
// In case the latest secondary round accomplish the cutoff time condition, return it.
return latestSecondaryRoundId;
}
// In case the report was sent by the secondary proxy.
if (latestAggregatorRoundId == latestSecondaryRoundId) {
// In case the transmission was sent in this same block only by the secondary proxy, return the previous round id.
if (isLatestSecondary && s_transmissions[latestAggregatorRoundId].recordedTimestamp == block.timestamp) {
return latestAggregatorRoundId - 1;
}
}
return latestAggregatorRoundId;
}
// ================================================================
// │ Transmission │
// ================================================================
/// @notice indicates that a new report was transmitted.
/// @param aggregatorRoundId the round to which this report was assigned.
/// @param answer median of the observations attached to this report.
/// @param transmitter address from which the report was transmitted.
/// @param observationsTimestamp when were observations made offchain.
/// @param observations observations transmitted with this report.
/// @param observers i-th element is the oracle id of the oracle that made the i-th observation.
/// @param juelsPerFeeCoin exchange rate between feeCoin (e.g. ETH on Ethereum) and LINK, denominated in juels.
/// @param configDigest configDigest of transmission.
/// @param epochAndRound least-significant byte is the OCR protocol round number, the other bytes give the OCR protocol epoch number.
event NewTransmission(
uint32 indexed aggregatorRoundId,
int192 answer,
address transmitter,
uint32 observationsTimestamp,
int192[] observations,
bytes observers,
int192 juelsPerFeeCoin,
bytes32 configDigest,
uint40 epochAndRound
);
error CalldataLengthMismatch();
error StaleReport();
error UnauthorizedTransmitter();
error ConfigDigestMismatch();
error WrongNumberOfSignatures();
error SignaturesOutOfRegistration();
error SignatureError();
error DuplicateSigner();
error OnlyCallableByEOA();
error ReportLengthMismatch();
error NumObservationsOutOfBounds();
error TooFewValuesToTrustMedian();
error MedianIsOutOfMinMaxRange();
/// @notice the constant-length components of the msg.data sent to transmit.
// See the "If we wanted to call sam" example on for example reasoning
// https://solidity.readthedocs.io/en/v0.7.2/abi-spec.html
uint256 private constant TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT = 4 // Function selector.
+ 32 * 3 // 3 words containing reportContext.
+ 32 // Word containing start location of abiencoded report value.
+ 32 // Word containing start location of abiencoded rs value.
+ 32 // Word containing start location of abiencoded ss value.
+ 32 // RawVs value.
+ 32 // Word containing length of report.
+ 32 // Word containing length rs.
+ 32 // Word containing length of ss.
+ 0; // Placeholder.
/// @notice decodes a serialized report into a Report struct.
/// @param rawReport serialized report in raw format.
/// @return report the decoded report in Report struct format.
function _decodeReport(
bytes memory rawReport
) internal pure returns (Report memory) {
(uint32 observationsTimestamp, bytes32 rawObservers, int192[] memory observations, int192 juelsPerFeeCoin) =
abi.decode(rawReport, (uint32, bytes32, int192[], int192));
_requireExpectedReportLength(rawReport, observations);
uint256 numObservations = observations.length;
bytes memory observers = abi.encodePacked(rawObservers);
assembly {
// We truncate observers from length 32 to the number of observations.
mstore(observers, numObservations)
}
return Report({
observationsTimestamp: observationsTimestamp,
observers: observers,
observations: observations,
juelsPerFeeCoin: juelsPerFeeCoin
});
}
/// @notice make sure the calldata length matches the inputs. Otherwise, the
/// transmitter could append an arbitrarily long (up to gas-block limit)
/// string of 0 bytes, which we would reimburse at a rate of 16 gas/byte, but
/// which would only cost the transmitter 4 gas/byte.
/// @param reportLength the length of the serialized report.
/// @param rsLength the length of the rs signatures.
/// @param ssLength the length of the ss signatures.
function _requireExpectedMsgDataLength(uint256 reportLength, uint256 rsLength, uint256 ssLength) private pure {
// Calldata will never be big enough to make this overflow.
uint256 expected = TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT + reportLength // One byte per entry in report.
+ rsLength * 32 // 32 bytes per entry in rs.
+ ssLength * 32 // 32 bytes per entry in ss.
+ 0; // Placeholder.
if (msg.data.length != expected) {
revert CalldataLengthMismatch();
}
}
/// @inheritdoc OCR2Abstract
function transmit(
// reportContext consists of:
// reportContext[0]: ConfigDigest.
// reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round.
// reportContext[2]: ExtraHash.
bytes32[3] calldata reportContext,
bytes calldata report,
// ECDSA signatures.
bytes32[] calldata rs,
bytes32[] calldata ss,
bytes32 rawVs
) external override {
// Call the internal transmit function without the isSecondary flag.
_transmit(reportContext, report, rs, ss, rawVs, false);
}
/// @notice secondary proxy transmit entrypoint, call the internal transmit function with the isSecondary flag.
/// @param reportContext serialized report context containing configDigest, epoch, round and extraHash.
/// @param report serialized report, which the signatures are signing.
/// @param rs i-th element is the R components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param ss i-th element is the S components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param rawVs i-th element is the the V component of the i-th signature.
function transmitSecondary(
// reportContext consists of:
// reportContext[0]: ConfigDigest.
// reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round.
// reportContext[2]: ExtraHash.
bytes32[3] calldata reportContext,
bytes calldata report,
// ECDSA signatures.
bytes32[] calldata rs,
bytes32[] calldata ss,
bytes32 rawVs
) external {
_transmit(reportContext, report, rs, ss, rawVs, true);
}
/// @notice internal transmit function, is called to post a new report to the contract.
/// @param reportContext serialized report context containing configDigest, epoch, round and extraHash.
/// @param report serialized report, which the signatures are signing.
/// @param rs i-th element is the R components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param ss i-th element is the S components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param rawVs i-th element is the the V component of the i-th signature.
/// @param isSecondary whether the transmission was sent by the secondary proxy or not.
function _transmit(
// reportContext consists of:
// reportContext[0]: ConfigDigest.
// reportContext[1]: 27 byte padding, 4-byte epoch and 1-byte round.
// reportContext[2]: ExtraHash.
bytes32[3] calldata reportContext,
bytes calldata report,
// ECDSA signatures.
bytes32[] calldata rs,
bytes32[] calldata ss,
bytes32 rawVs,
bool isSecondary
) internal {
// NOTE: If the arguments to this function are changed, _requireExpectedMsgDataLength and/or
// TRANSMIT_MSGDATA_CONSTANT_LENGTH_COMPONENT need to be changed accordingly.
uint256 initialGas = gasleft(); // This line must come first.
// Validate the report data.
_validateReport(reportContext, report.length, rs.length, ss.length);
Report memory report_ = _decodeReport(report); // Decode the report.
HotVars memory hotVars = s_hotVars; // Load hotVars into memory.
if (isSecondary) {
(bool exist, uint32 roundId) = _doesReportExist(report_);
// In case the report exists, copy the round id and pay the transmitter.
if (exist) {
// In case the round has already been processed by the secondary feed.
if (hotVars.latestSecondaryRoundId >= roundId) {
revert StaleReport();
}
s_hotVars.latestSecondaryRoundId = roundId;
emit SecondaryRoundIdUpdated(roundId);
_payTransmitter(hotVars, report_.juelsPerFeeCoin, uint32(initialGas));
return;
}
}
// Report epoch and round.
uint40 epochAndRound = uint40(uint256(reportContext[1]));
// Only skip the report transmission in case the epochAndRound is equal to the latestEpochAndRound
// and the latest sender was the secondary feed.
if (epochAndRound != hotVars.latestEpochAndRound || !hotVars.isLatestSecondary) {
// In case the epochAndRound is lower or equal than the latestEpochAndRound, it's a stale report
// because it's older or has already been transmitted.
if (epochAndRound <= hotVars.latestEpochAndRound) {
revert StaleReport();
}
// Verify signatures attached to report.
_verifySignatures(reportContext, report, rs, ss, rawVs);
_report(hotVars, reportContext[0], epochAndRound, report_, isSecondary);
} else {
// If the report is the same and the latest sender was the secondary feed,
// we're effectively unlocking the primary feed with this
emit PrimaryFeedUnlocked(s_hotVars.latestAggregatorRoundId);
}
// Store if the latest report was secondary or not.
s_hotVars.isLatestSecondary = isSecondary;
_payTransmitter(hotVars, report_.juelsPerFeeCoin, uint32(initialGas));
}
/// @notice helper function to validate the report data.
/// @param reportContext serialized report context containing configDigest, epoch, round and extraHash.
/// @param reportLength the length of the serialized report.
/// @param rsLength the length of the rs signatures.
/// @param ssLength the length of the ss signatures.
function _validateReport(
bytes32[3] calldata reportContext,
uint256 reportLength,
uint256 rsLength,
uint256 ssLength
) internal view {
if (!s_transmitters[msg.sender].active) {
revert UnauthorizedTransmitter();
}
if (s_latestConfigDigest != reportContext[0]) {
revert ConfigDigestMismatch();
}
_requireExpectedMsgDataLength(reportLength, rsLength, ssLength);
if (rsLength != s_hotVars.f + 1) {
revert WrongNumberOfSignatures();
}
if (rsLength != ssLength) {
revert SignaturesOutOfRegistration();
}
}
/// @notice helper function to verify the report signatures.
/// @param reportContext serialized report context containing configDigest, epoch, round and extraHash.
/// @param report serialized report, which the signatures are signing.
/// @param rs i-th element is the R components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param ss i-th element is the S components of the i-th signature on report. Must have at most maxNumOracles entries.
/// @param rawVs i-th element is the the V component of the i-th signature.
function _verifySignatures(
bytes32[3] calldata reportContext,
bytes calldata report,
bytes32[] calldata rs,
bytes32[] calldata ss,
bytes32 rawVs
) internal view {
bytes32 h = keccak256(abi.encode(keccak256(report), reportContext));
// i-th byte counts number of sigs made by i-th signer.
uint256 signedCount = 0;
Signer memory signer;
for (uint256 i = 0; i < rs.length; ++i) {
address signerAddress = ecrecover(h, uint8(rawVs[i]) + 27, rs[i], ss[i]);
signer = s_signers[signerAddress];
if (!signer.active) {
revert SignatureError();
}
unchecked {
signedCount += 1 << (8 * signer.index);
}
}
// The first byte of the mask can be 0, because we only ever have 31 oracles.
if (signedCount & 0x0001010101010101010101010101010101010101010101010101010101010101 != signedCount) {
revert DuplicateSigner();
}
}
/// @notice details about the most recent report.
/// @return configDigest domain separation tag for the latest report.
/// @return epoch epoch in which the latest report was generated.
/// @return round OCR round in which the latest report was generated.
/// @return latestAnswer_ median value from latest report.
/// @return latestTimestamp_ when the latest report was transmitted.
function latestTransmissionDetails()
external
view
returns (bytes32 configDigest, uint32 epoch, uint8 round, int192 latestAnswer_, uint64 latestTimestamp_)
{
// solhint-disable-next-line avoid-tx-origin
if (msg.sender != tx.origin) revert OnlyCallableByEOA();
return (
s_latestConfigDigest,
uint32(s_hotVars.latestEpochAndRound >> 8),
uint8(s_hotVars.latestEpochAndRound),
s_transmissions[s_hotVars.latestAggregatorRoundId].answer,
s_transmissions[s_hotVars.latestAggregatorRoundId].recordedTimestamp
);
}
/// @inheritdoc OCR2Abstract
function latestConfigDigestAndEpoch()
external
view
virtual
override
returns (bool scanLogs, bytes32 configDigest, uint32 epoch)
{
return (false, s_latestConfigDigest, uint32(s_hotVars.latestEpochAndRound >> 8));
}
/// @notice evaluate the serialized report length and compare it with the expected length.
/// @param report serialized report, which the signatures are signing.
/// @param observations decoded observations from the report.
function _requireExpectedReportLength(bytes memory report, int192[] memory observations) private pure {
uint256 expected = 32 // ObservationsTimestamp.
+ 32 // RawObservers.
+ 32 // Observations offset.
+ 32 // JuelsPerFeeCoin.
+ 32 // Observations length.
+ 32 * observations.length // Observations payload.
+ 0;
if (report.length != expected) revert ReportLengthMismatch();
}
/// @notice report a new transmission and emit the necessary events.
/// @param hotVars most common fields used in the hot path.
/// @param configDigest digested configuration.
/// @param epochAndRound report epoch and round.
/// @param report decoded report in Report struct format.
/// @param isSecondary whether the report was sent by the secondary proxy or not.
function _report(
HotVars memory hotVars,
bytes32 configDigest,
uint40 epochAndRound,
Report memory report,
bool isSecondary
) internal {
if (report.observations.length > MAX_NUM_ORACLES) revert NumObservationsOutOfBounds();
// Offchain logic ensures that a quorum of oracles is operating on a matching set of at least
// 2f+1 observations. By assumption, up to f of those can be faulty, which includes being
// malformed. Conversely, more than f observations have to be well-formed and sent on chain.
if (report.observations.length <= hotVars.f) revert TooFewValuesToTrustMedian();
hotVars.latestEpochAndRound = epochAndRound;
// Get median, validate its range, store it in new aggregator round.
int192 median = report.observations[report.observations.length / 2];
if (i_minAnswer > median || median > i_maxAnswer) revert MedianIsOutOfMinMaxRange();
hotVars.latestAggregatorRoundId++;
s_transmissions[hotVars.latestAggregatorRoundId] = Transmission({
answer: median,
observationsTimestamp: report.observationsTimestamp,
recordedTimestamp: uint32(block.timestamp)
});
// In case the sender is the secondary proxy, update the latest secondary round id.
if (isSecondary) {
hotVars.latestSecondaryRoundId = hotVars.latestAggregatorRoundId;
emit SecondaryRoundIdUpdated(hotVars.latestSecondaryRoundId);
}
// Persist updates to hotVars.
s_hotVars = hotVars;
emit NewTransmission(
hotVars.latestAggregatorRoundId,
median,
msg.sender,
report.observationsTimestamp,
report.observations,
report.observers,
report.juelsPerFeeCoin,
configDigest,
epochAndRound
);
// Emit these for backwards compatibility with offchain consumers
// that only support legacy events.
emit NewRound(
hotVars.latestAggregatorRoundId,
address(0x0), // Use zero address since we don't have anybody "starting" the round here.
report.observationsTimestamp
);
emit AnswerUpdated(median, hotVars.latestAggregatorRoundId, block.timestamp);
_validateAnswer(hotVars.latestAggregatorRoundId, median);
}
// ================================================================
// │ v2 AggregatorInterface │
// ================================================================
/// @notice median from the most recent report.
/// @return answer the latest answer.
function latestAnswer() public view virtual override returns (int256) {
return s_transmissions[_getLatestRound()].answer;
}
/// @notice timestamp of block in which last report was transmitted.
/// @return recordedTimestamp the latest recorded timestamp.
function latestTimestamp() public view virtual override returns (uint256) {
return s_transmissions[_getLatestRound()].recordedTimestamp;
}
/// @notice Aggregator round (NOT OCR round) in which last report was transmitted.
/// @return roundId the latest round id.
function latestRound() public view virtual override returns (uint256) {
return _getLatestRound();
}
/// @notice median of report from given aggregator round (NOT OCR round).
/// @param roundId the aggregator round of the target report.
/// @return answer the answer of the round id.
function getAnswer(
uint256 roundId
) public view virtual override returns (int256) {
if (roundId > _getLatestRound()) return 0;
return s_transmissions[uint32(roundId)].answer;
}
/// @notice timestamp of block in which report from given aggregator round was transmitted.
/// @param roundId aggregator round (NOT OCR round) of target report.
/// @return recordedTimestamp the recorded timestamp of the round id.
function getTimestamp(
uint256 roundId
) public view virtual override returns (uint256) {
if (roundId > _getLatestRound()) return 0;
return s_transmissions[uint32(roundId)].recordedTimestamp;
}
// ================================================================
// │ v3 AggregatorInterface │
// ================================================================
error RoundNotFound();
/// @notice amount of decimals.
uint8 private immutable i_decimals;
/// @notice aggregator contract version.
uint256 internal constant VERSION = 6;
/// @notice human readable description.
string internal s_description;
/// @notice get the amount of decimals.
/// @return i_decimals amount of decimals.
function decimals() public view virtual override returns (uint8) {
return i_decimals;
}
/// @notice get the contract version.
/// @return VERSION the contract version.
function version() public view virtual override returns (uint256) {
return VERSION;
}
/// @notice human-readable description of observable this contract is reporting on.
/// @return s_description the contract description.
function description() public view virtual override returns (string memory) {
return s_description;
}
/// @notice details for the given aggregator round.
/// @param roundId target aggregator round, must fit in uint32.
/// @return roundId_ roundId.
/// @return answer median of report from given roundId.
/// @return startedAt timestamp of when observations were made offchain.
/// @return updatedAt timestamp of block in which report from given roundId was transmitted.
/// @return answeredInRound roundId.
function getRoundData(
uint80 roundId
)
public
view
virtual
override
returns (uint80 roundId_, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
if (roundId > _getLatestRound()) return (0, 0, 0, 0, 0);
Transmission memory transmission = s_transmissions[uint32(roundId)];
return (roundId, transmission.answer, transmission.observationsTimestamp, transmission.recordedTimestamp, roundId);
}
/// @notice aggregator details for the most recently transmitted report.
/// @return roundId aggregator round of latest report (NOT OCR round).
/// @return answer median of latest report.
/// @return startedAt timestamp of when observations were made offchain.
/// @return updatedAt timestamp of block containing latest report.
/// @return answeredInRound aggregator round of latest report.
function latestRoundData()
public
view
virtual
override
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
uint80 latestRoundId = _getLatestRound();
Transmission memory transmission = s_transmissions[uint32(latestRoundId)];
return (
latestRoundId,
transmission.answer,
transmission.observationsTimestamp,
transmission.recordedTimestamp,
latestRoundId
);
}
// ================================================================
// │ Configurable LINK Token │
// ================================================================
/// @notice emitted when the LINK token contract is set.
/// @param oldLinkToken the address of the old LINK token contract.
/// @param newLinkToken the address of the new LINK token contract.
event LinkTokenSet(LinkTokenInterface indexed oldLinkToken, LinkTokenInterface indexed newLinkToken);
error TransferRemainingFundsFailed();
/// @notice we assume that the token contract is correct. This contract is not written
/// to handle misbehaving ERC20 tokens!
LinkTokenInterface internal s_linkToken;
/// @notice sets the LINK token contract used for paying oracles.
/// @dev this function will return early (without an error) without changing any state
/// if linkToken equals getLinkToken().
/// @dev this will trigger a payout so that a malicious owner cannot take from oracles
/// what is already owed to them.
/// @dev we assume that the token contract is correct. This contract is not written
/// to handle misbehaving ERC20 tokens!
/// @param linkToken the address of the LINK token contract.
/// @param recipient remaining funds from the previous token contract are transferred
/// here.
function setLinkToken(LinkTokenInterface linkToken, address recipient) external onlyOwner {
LinkTokenInterface oldLinkToken = s_linkToken;
if (linkToken == oldLinkToken) {
// No change, nothing to be done.
return;
}
// Call balanceOf as a sanity check on whether we're talking to a token
// contract.
linkToken.balanceOf(address(this));
// We break CEI here, but that's okay because we're dealing with a correct
// token contract (by assumption).
_payOracles();
uint256 remainingBalance = oldLinkToken.balanceOf(address(this));
if (!oldLinkToken.transfer(recipient, remainingBalance)) revert TransferRemainingFundsFailed();
// solhint-disable-next-line reentrancy
s_linkToken = linkToken;
emit LinkTokenSet(oldLinkToken, linkToken);
}
/// @notice gets the LINK token contract used for paying oracles.
/// @return linkToken the address of the LINK token contract.
function getLinkToken() external view returns (LinkTokenInterface linkToken) {
return s_linkToken;
}
// ================================================================
// │ BillingAccessController Management │
// ================================================================
/// @notice emitted when a new access-control contract is set.
/// @param old the address prior to the current setting.
/// @param current the address of the new access-control contract.
event BillingAccessControllerSet(AccessControllerInterface old, AccessControllerInterface current);
/// @notice controls who can change billing parameters. A billingAdmin is not able to
/// affect any OCR protocol settings and therefore cannot tamper with the
/// liveness or integrity of a data feed. However, a billingAdmin can set
/// faulty billing parameters causing oracles to be underpaid, or causing them
/// to be paid so much that further calls to setConfig, setBilling,
/// setLinkToken will always fail due to the contract being underfunded.
AccessControllerInterface internal s_billingAccessController;
/// @notice internal function to set a new billingAccessController.
/// @param billingAccessController new billingAccessController contract address.
function _setBillingAccessController(
AccessControllerInterface billingAccessController
) internal {
AccessControllerInterface oldController = s_billingAccessController;
if (billingAccessController != oldController) {
s_billingAccessController = billingAccessController;
emit BillingAccessControllerSet(oldController, billingAccessController);
}
}
/// @notice sets billingAccessController.
/// @param _billingAccessController new billingAccessController contract address.
function setBillingAccessController(
AccessControllerInterface _billingAccessController
) external onlyOwner {
_setBillingAccessController(_billingAccessController);
}
/// @notice gets billingAccessController.
/// @return s_billingAccessController address of billingAccessController contract.
function getBillingAccessController() external view returns (AccessControllerInterface) {
return s_billingAccessController;
}
// ================================================================
// │ Billing Configuration │
// ================================================================
/// @notice emitted when billing parameters are set.
/// @param maximumGasPriceGwei highest gas price for which transmitter will be compensated.
/// @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value.
/// @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report.
/// @param transmissionPaymentGjuels reward to transmitter of a successful report.
/// @param accountingGas gas overhead incurred by accounting logic.
event BillingSet(
uint32 maximumGasPriceGwei,
uint32 reasonableGasPriceGwei,
uint32 observationPaymentGjuels,
uint32 transmissionPaymentGjuels,
uint24 accountingGas
);
error OnlyOwnerAndBillingAdminCanCall();
/// @notice sets billing parameters.
/// @dev access control provided by billingAccessController.
/// @param maximumGasPriceGwei highest gas price for which transmitter will be compensated.
/// @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value.
/// @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report.
/// @param transmissionPaymentGjuels reward to transmitter of a successful report.
/// @param accountingGas gas overhead incurred by accounting logic.
function setBilling(
uint32 maximumGasPriceGwei,
uint32 reasonableGasPriceGwei,
uint32 observationPaymentGjuels,
uint32 transmissionPaymentGjuels,
uint24 accountingGas
) external {
if (!(msg.sender == owner() || s_billingAccessController.hasAccess(msg.sender, msg.data))) {
revert OnlyOwnerAndBillingAdminCanCall();
}
_payOracles();
s_hotVars.maximumGasPriceGwei = maximumGasPriceGwei;
s_hotVars.reasonableGasPriceGwei = reasonableGasPriceGwei;
s_hotVars.observationPaymentGjuels = observationPaymentGjuels;
s_hotVars.transmissionPaymentGjuels = transmissionPaymentGjuels;
s_accountingGas = accountingGas;
emit BillingSet(
maximumGasPriceGwei, reasonableGasPriceGwei, observationPaymentGjuels, transmissionPaymentGjuels, accountingGas
);
}
/// @notice gets billing parameters.
/// @param maximumGasPriceGwei highest gas price for which transmitter will be compensated.
/// @param reasonableGasPriceGwei transmitter will receive reward for gas prices under this value.
/// @param observationPaymentGjuels reward to oracle for contributing an observation to a successfully transmitted report.
/// @param transmissionPaymentGjuels reward to transmitter of a successful report.
/// @param accountingGas gas overhead of the accounting logic.
function getBilling()
external
view
returns (
uint32 maximumGasPriceGwei,
uint32 reasonableGasPriceGwei,
uint32 observationPaymentGjuels,
uint32 transmissionPaymentGjuels,
uint24 accountingGas
)
{
return (
s_hotVars.maximumGasPriceGwei,
s_hotVars.reasonableGasPriceGwei,
s_hotVars.observationPaymentGjuels,
s_hotVars.transmissionPaymentGjuels,
s_accountingGas
);
}
// ================================================================
// │ Payments and Withdrawals │
// ================================================================
/// @notice emitted when an oracle has been paid LINK.
/// @param transmitter address from which the oracle sends reports to the transmit method.
/// @param payee address to which the payment is sent.
/// @param amount amount of LINK sent.
/// @param linkToken address of the LINK token contract.
event OraclePaid(
address indexed transmitter, address indexed payee, uint256 amount, LinkTokenInterface indexed linkToken
);
error OnlyPayeeCanWithdraw();
error InsufficientFunds();
error InsufficientBalance();
/// @notice withdraws an oracle's payment from the contract.
/// @param transmitter the transmitter address of the oracle.
/// @dev must be called by oracle's payee address.
function withdrawPayment(
address transmitter
) external {
if (msg.sender != s_payees[transmitter]) revert OnlyPayeeCanWithdraw();
_payOracle(transmitter);
}
/// @notice query an oracle's payment amount, denominated in juels.
/// @param transmitterAddress the transmitter address of the oracle.
function owedPayment(
address transmitterAddress
) public view returns (uint256) {
Transmitter memory transmitter = s_transmitters[transmitterAddress];
if (!transmitter.active) return 0;
// Safe from overflow:
// s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index] <= 2**32.
// s_hotVars.observationPaymentGjuels <= 2**32.
// 1 gwei <= 2**32.
// hence juelsAmount <= 2**96.
uint256 juelsAmount = uint256(s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index])
* uint256(s_hotVars.observationPaymentGjuels) * (1 gwei);
juelsAmount += transmitter.paymentJuels;
return juelsAmount;
}
/// @notice pays out transmitter's oracle balance to the corresponding payee, and zeros it out.
/// @param transmitterAddress the transmitter address of the oracle.
function _payOracle(
address transmitterAddress
) internal {
Transmitter memory transmitter = s_transmitters[transmitterAddress];
if (!transmitter.active) return;
uint256 juelsAmount = owedPayment(transmitterAddress);
if (juelsAmount > 0) {
address payee = s_payees[transmitterAddress];
// Poses no re-entrancy issues, because LINK.transfer does not yield
// control flow.
if (!s_linkToken.transfer(payee, juelsAmount)) {
revert InsufficientFunds();
}
// solhint-disable-next-line reentrancy
s_rewardFromAggregatorRoundId[transmitter.index] = s_hotVars.latestAggregatorRoundId;
// solhint-disable-next-line reentrancy
s_transmitters[transmitterAddress].paymentJuels = 0;
emit OraclePaid(transmitterAddress, payee, juelsAmount, s_linkToken);
}
}
/// @notice pays out all transmitters oracles, and zeros out their balances.
/// It's much more gas-efficient to do this as a single operation, to avoid
/// hitting storage too much.
function _payOracles() internal {
unchecked {
LinkTokenInterface linkToken = s_linkToken;
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
uint32[MAX_NUM_ORACLES] memory rewardFromAggregatorRoundId = s_rewardFromAggregatorRoundId;
address[] memory transmitters = s_transmittersList;
for (uint256 transmitteridx = 0; transmitteridx < transmitters.length; transmitteridx++) {
uint256 reimbursementAmountJuels = s_transmitters[transmitters[transmitteridx]].paymentJuels;
s_transmitters[transmitters[transmitteridx]].paymentJuels = 0;
uint256 obsCount = latestAggregatorRoundId - rewardFromAggregatorRoundId[transmitteridx];
uint256 juelsAmount =
obsCount * uint256(s_hotVars.observationPaymentGjuels) * (1 gwei) + reimbursementAmountJuels;
if (juelsAmount > 0) {
address payee = s_payees[transmitters[transmitteridx]];
// Poses no re-entrancy issues, because LINK.transfer does not yield
// control flow.
if (!linkToken.transfer(payee, juelsAmount)) {
revert InsufficientFunds();
}
rewardFromAggregatorRoundId[transmitteridx] = latestAggregatorRoundId;
emit OraclePaid(transmitters[transmitteridx], payee, juelsAmount, linkToken);
}
}
// "Zero" the accounting storage variables.
// solhint-disable-next-line reentrancy
s_rewardFromAggregatorRoundId = rewardFromAggregatorRoundId;
}
}
/// @notice withdraw any available funds left in the contract, up to amount, after accounting for the funds due to participants in past reports.
/// @dev access control provided by billingAccessController.
/// @param recipient address to send funds to.
/// @param amount maximum amount to withdraw, denominated in LINK-wei.
function withdrawFunds(address recipient, uint256 amount) external {
if (msg.sender != owner() && !s_billingAccessController.hasAccess(msg.sender, msg.data)) {
revert OnlyOwnerAndBillingAdminCanCall();
}
uint256 linkDue = _totalLinkDue();
uint256 linkBalance = s_linkToken.balanceOf(address(this));
if (linkBalance < linkDue) {
revert InsufficientBalance();
}
if (!s_linkToken.transfer(recipient, _min(linkBalance - linkDue, amount))) {
revert InsufficientFunds();
}
}
/// @notice total LINK due to participants in past reports (denominated in Juels).
/// @return linkDue total LINK due.
function _totalLinkDue() internal view returns (uint256 linkDue) {
// Argument for overflow safety: We do all computations in
// uint256s. The inputs to linkDue are:
// - the <= 31 observation rewards each of which has less than
// 64 bits (32 bits for observationPaymentGjuels, 32 bits
// for wei/gwei conversion). Hence 69 bits are sufficient for this part.
// - the <= 31 gas reimbursements, each of which consists of at most 96
// bits. Hence 101 bits are sufficient for this part.
// So we never need more than 102 bits.
address[] memory transmitters = s_transmittersList;
uint256 n = transmitters.length;
uint32 latestAggregatorRoundId = s_hotVars.latestAggregatorRoundId;
uint32[MAX_NUM_ORACLES] memory rewardFromAggregatorRoundId = s_rewardFromAggregatorRoundId;
for (uint256 i = 0; i < n; ++i) {
linkDue += latestAggregatorRoundId - rewardFromAggregatorRoundId[i];
}
// Convert observationPaymentGjuels to uint256, or this overflows!
linkDue *= uint256(s_hotVars.observationPaymentGjuels) * (1 gwei);
for (uint256 i = 0; i < n; ++i) {
linkDue += uint256(s_transmitters[transmitters[i]].paymentJuels);
}
return linkDue;
}
/// @notice allows oracles to check that sufficient LINK balance is available.
/// @return availableBalance LINK available on this contract, after accounting for outstanding obligations, can become negative.
function linkAvailableForPayment() external view returns (int256 availableBalance) {
// There are at most one billion LINK, so this cast is safe.
int256 balance = int256(s_linkToken.balanceOf(address(this)));
// According to the argument in the definition of _totalLinkDue,
// _totalLinkDue is never greater than 2**102, so this cast is safe.
int256 due = int256(_totalLinkDue());
// Safe from overflow according to above sizes.
return int256(balance) - int256(due);
}
/// @notice number of observations oracle is due to be reimbursed for.
/// @param transmitterAddress address used by oracle for signing or transmitting reports.
/// @return observations difference between the latest oracle reimbursement round id and the latest hotVars round id.
function oracleObservationCount(
address transmitterAddress
) external view returns (uint32) {
Transmitter memory transmitter = s_transmitters[transmitterAddress];
if (!transmitter.active) return 0;
return s_hotVars.latestAggregatorRoundId - s_rewardFromAggregatorRoundId[transmitter.index];
}
// ================================================================
// │ Transmitter Payment │
// ================================================================
error LeftGasCannotExceedInitialGas();
/// @notice gas price at which the transmitter should be reimbursed, in gwei/gas.
/// @param txGasPriceGwei transaction gas price in ETH-gwei units.
/// @param reasonableGasPriceGwei reasonable gas price in ETH-gwei units.
/// @param maximumGasPriceGwei maximum gas price in ETH-gwei units.
/// @return gasPrice resulting gas price to reimburse.
function _reimbursementGasPriceGwei(
uint256 txGasPriceGwei,
uint256 reasonableGasPriceGwei,
uint256 maximumGasPriceGwei
) internal pure returns (uint256) {
// This happens on the path for transmissions. We'd rather pay out
// a wrong reward than risk a liveness failure due to a revert.
unchecked {
// Reward the transmitter for choosing an efficient gas price: if they manage
// to come in lower than considered reasonable, give them half the savings.
uint256 gasPriceGwei = txGasPriceGwei;
if (txGasPriceGwei < reasonableGasPriceGwei) {
// Give transmitter half the savings for coming in under the reasonable gas price.
gasPriceGwei += (reasonableGasPriceGwei - txGasPriceGwei) / 2;
}
// Don't reimburse a gas price higher than maximumGasPriceGwei.
return _min(gasPriceGwei, maximumGasPriceGwei);
}
}
/// @notice gas reimbursement due the transmitter, in wei.
/// @param initialGas initial remaining gas.
/// @param gasPriceGwei gas price in ETH-gwei units.
/// @param callDataGas calldata gas cost.
/// @param accountingGas overhead incurred by accounting logic.
/// @param leftGas actual remaining gas.
/// @return fullGasCostWei final calculated gas cost in wei.
function _transmitterGasCostW
Submitted on: 2025-10-24 09:59:18
Comments
Log in to comment.
No comments yet.