diff --git a/.solhint.test.json b/.solhint.test.json index 3a62c8a..2152fde 100644 --- a/.solhint.test.json +++ b/.solhint.test.json @@ -1,6 +1,5 @@ { "extends": "solhint:recommended", - "plugins": ["prettier"], "rules": { "one-contract-per-file": "off", "no-unused-vars": "off", @@ -17,12 +16,6 @@ "const-name-snakecase": "off", "contract-name-camelcase": "off", "no-empty-blocks": "off", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], "reason-string": ["warn", { "maxLength": 64 }] } } diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index e5d0c8c..00ae0d0 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -1,6 +1,7 @@ export default { "*.md": ["prettier --write"], - "*.sol": ["prettier --write", "solhint -w 0"], + "{src,script}/**/*.sol": ["prettier --write", "solhint -w 0"], + "test/**/*.sol": ["prettier --write", "solhint -c .solhint.test.json -w 0"], "*.json": ["prettier --write"], "*.yml": ["prettier --write"], }; diff --git a/src/Oracles.sol b/src/Oracles.sol index f967415..859b90e 100644 --- a/src/Oracles.sol +++ b/src/Oracles.sol @@ -1,40 +1,138 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.24; +import {RedstoneConsumerNumericBase, NumericArrayLib} from "redstone/contracts/core/RedstoneConsumerNumericBase.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IOracles} from "./interfaces/IOracles.sol"; import {OracleValue, OracleValueLib} from "./lib/OracleValueLib.sol"; -// TODO: reanable some/all of these once code written +// TODO: reenable some/all of these once code written // solhint-disable no-empty-blocks, gas-struct-packing, named-parameters-mapping -contract Oracles is IOracles { +contract Oracles is IOracles, RedstoneConsumerNumericBase { using OracleValueLib for OracleValue; - struct OracleBuffer { - uint256[100] medians; - OracleBufferInfo bufferInfo; + // prettier-ignore + struct RateFeed { + /// @dev A cyclic buffer of the 100 most recent price reports. + /// @dev A price is an unwrapped OracleValue converted to uint256. + uint256[100] latestPrices; + + /// @dev Tightly packed rate feed details fitting into a single storage slot. + RateFeedDetails details; } - struct OracleBufferInfo { - uint8 lastIndex; - uint8 windowSize; - bool bufferFull; - OracleValue windowSum; - OracleValue windowAverage; - uint40 latestTimestamp; - uint8 validityFlags; + // prettier-ignore + // Designed to fit within 1 storage slot. + struct RateFeedDetails { + /**********************/ + /* Rate Feed Config */ + /**********************/ + + /// @dev Number of the most recent price values to average over. + uint8 priceWindowSize; + + /// @dev The maximal allowed deviation between reported prices within a batch, expressed as a factor < 1. uint16 allowedDeviation; + + /// @dev The minimal number of data providers that need to have reported a value in the last report. uint8 quorum; + + /// @dev The minimal number of data providers that need to be certain of their value. uint8 certaintyThreshold; + + /// @dev The allowed age of a report before it is considered stale, in seconds. uint16 allowedStaleness; + + + /**********************/ + /* Rate Feed Values */ + /**********************/ + + /// @dev Index of the most recently reported price. + uint8 latestPriceIndex; + + /// @dev True if the buffer of the 100 most recent prices has been filled up at least once. + bool bufferFull; + + /// @dev Sum of the last `priceWindowSize` prices. + OracleValue priceWindowSum; + + /// @dev Average of the last `priceWindowSize` prices (i.e. `priceWindowSum / priceWindowSize`). + OracleValue priceWindowAverage; + + /// @dev Timestamp of the latest price report. + uint40 latestTimestamp; + + /// @dev A bitmask of the following validity flags: + /// 0x001 - isFresh => the last price report is fresh + /// 0x010 - isCertain => at least `certaintyThreshold` data providers are certain of their price report + /// 0x100 - isWithinAllowedDeviation => the latest price is within the allowed deviation. + uint8 validityFlags; } - mapping(address => OracleBuffer) public rateFeeds; + /// @dev Set of supported rate feed IDs. + EnumerableSet.AddressSet private _rateFeedIds; + + /// @dev mapping from rateFeedId to a set of data provider addresses allowed to report prices for `rateFeedId`. + mapping(address rateFeedId => EnumerableSet.AddressSet dataProviders) + private _rateFeedProviders; + + /// @dev Mapping from rateFeedId to a rateFeed containing price medians and metadata. + mapping(address rateFeedId => RateFeed rateFeed) private _rateFeeds; + + address internal _currentlyUpdatedRateFeedId; + + /**************************************/ + /* */ + /* RedStone Base Contract Overrides */ + /* */ + /**************************************/ + + error InvalidProviderForRateFeed(address rateFeedId, address provider); + error TimestampFromFutureIsNotAllowed( + uint256 receivedTimestampMilliseconds, + uint256 blockTimestamp + ); + error DataIsNotFresh( + uint256 receivedTimestampMilliseconds, + uint256 minAllowedTimestampForNewDataSeconds + ); + error CertaintyThresholdNotReached( + uint8 receivedCertainties, + uint8 certaintyThreshold + ); + error MinAndMaxValuesDeviateTooMuch(uint256 minVal, uint256 maxVal); + + // TODO: This is a placeholder for the actual implementation based on the 2023 PoC. + // solhint-disable-next-line max-line-length + // https://github.com/redstone-finance/redstone-evm-examples/blob/mento-v2-oracles-poc/contracts/mento-v2-oracles/MentoV2Oracles.sol + function report(address rateFeedId) external { + _currentlyUpdatedRateFeedId = rateFeedId; + RateFeed storage rateFeed = _rateFeeds[rateFeedId]; + + // Extracts values from calldata via assembly + uint256 redstoneValue = getOracleNumericValueFromTxMsg( + bytes32(uint256(uint160(rateFeedId))) + ); + rateFeed.details.priceWindowSum = OracleValueLib.fromRedStoneValue( + redstoneValue + ); - function report(address rateFeedId) external {} + // TODO: We still would need to decide how to select the latest data timestamp + // Because currently we assume providers provide the same timestamp + // If not - we need to discuss the way to calculate its aggregated value (e.g. median or min) + // TODO: naive uint40 conversion to satisfy compiler, needs to be done properly + rateFeed.details.latestTimestamp = uint40( + extractTimestampsAndAssertAllAreEqual() + ); + } function markStale(address rateFeedId) external {} - function setWindowSize(address rateFeedId, uint8 windowSize) external {} + function setPriceWindowSize( + address rateFeedId, + uint8 priceWindowSize + ) external {} function setAllowedDeviation( address rateFeedId, @@ -50,16 +148,16 @@ contract Oracles is IOracles { function setAllowedStaleness( address rateFeedId, - uint16 allowedStaleness + uint16 allowedStalenessInSeconds ) external {} function addRateFeed( address rateFeedId, - uint8 windowSize, + uint8 priceWindowSize, uint16 allowedDeviation, uint8 quorum, uint8 certaintyThreshold, - uint16 allowedStaleness, + uint16 allowedStalenessInSeconds, address[] calldata dataProviders ) external {} @@ -72,38 +170,207 @@ contract Oracles is IOracles { address provider ) external {} - function medianRate( + function getExchangeRateFor( address rateFeedId - ) external view returns (uint256 numerator, uint256 denominator) {} + ) + external + view + returns ( + uint256 numerator, + uint256 denominator, + uint40 lastUpdateTimestamp + ) + {} - function medianRateUint64( + function getExchangeRateAsUint64( address rateFeedId - ) external view returns (uint64 medianRate) {} + ) external view returns (uint64 exchangeRate) {} - function rateInfo( + function rateFeedInfo( address rateFeedId - ) external view returns (uint64 medianRate, uint8 validityFlags) {} + ) external view returns (uint64 exchangeRate, uint8 validityFlags) { + RateFeedDetails storage details = _rateFeeds[rateFeedId].details; + return (details.priceWindowAverage.unwrap(), details.validityFlags); + } - function rateFeedParameters( + function rateFeedConfig( address rateFeedId ) external view returns ( - uint8 windowSize, + uint8 priceWindowSize, uint16 allowedDeviation, uint8 quorum, uint8 certaintyThreshold, uint16 allowedStaleness ) { - OracleBufferInfo memory bufferInfo = rateFeeds[rateFeedId].bufferInfo; + RateFeedDetails memory details = _rateFeeds[rateFeedId].details; return ( - bufferInfo.windowSize, - bufferInfo.allowedDeviation, - bufferInfo.quorum, - bufferInfo.certaintyThreshold, - bufferInfo.allowedStaleness + details.priceWindowSize, + details.allowedDeviation, + details.quorum, + details.certaintyThreshold, + details.allowedStaleness ); } + + /**************************************/ + /* */ + /* RedStone Base Contract Overrides */ + /* */ + /**************************************/ + // solhint-disable-next-line max-line-length + // From Alex's original Oracles PoC: https://github.com/redstone-finance/redstone-evm-examples/blob/mento-v2-oracles-poc/contracts/mento-v2-oracles/MentoV2Oracles.sol + // TODO: Reimplement based on latest design + + function getUniqueSignersThreshold() + public + view + virtual + override + returns (uint8 quorum) + { + return _rateFeeds[_currentlyUpdatedRateFeedId].details.quorum; + } + + function validateTimestamp( + uint256 receivedTimestampMilliseconds + ) public view override { + uint256 receivedTimestampSeconds = receivedTimestampMilliseconds / 1000; + RateFeed storage rateFeed = _rateFeeds[_currentlyUpdatedRateFeedId]; + uint256 previousDataTimestampSeconds = rateFeed.details.latestTimestamp; + uint256 minAllowedTimestampForNewDataInSeconds = previousDataTimestampSeconds + + rateFeed.details.allowedStaleness; + + if ( + // solhint-disable gas-strict-inequalities + receivedTimestampSeconds <= minAllowedTimestampForNewDataInSeconds + ) { + revert DataIsNotFresh( + receivedTimestampMilliseconds, + minAllowedTimestampForNewDataInSeconds + ); + } + + if (receivedTimestampSeconds > block.timestamp) { + revert TimestampFromFutureIsNotAllowed( + receivedTimestampMilliseconds, + block.timestamp + ); + } + } + + function aggregateValues( + uint256[] memory valuesWithCertainties + ) public view virtual override returns (uint256 median) { + RateFeed storage rateFeed = _rateFeeds[_currentlyUpdatedRateFeedId]; + + uint256 valuesWithCertaintiesLength = valuesWithCertainties.length; + uint256[] memory values = new uint256[](valuesWithCertaintiesLength); + uint8 certainties = 0; + uint256 maxVal = 0; + uint256 minVal = type(uint256).max; + + for (uint256 i = 0; i < valuesWithCertaintiesLength; ++i) { + (bool certainty, uint256 value) = parseValueWithCertainty( + valuesWithCertainties[i] + ); + values[i] = value; + if (certainty) { + ++certainties; + } + if (value > maxVal) { + maxVal = value; + } + if (value < minVal) { + minVal = value; + } + } + + if (certainties < rateFeed.details.certaintyThreshold) { + revert CertaintyThresholdNotReached( + certainties, + rateFeed.details.certaintyThreshold + ); + } + + if ((maxVal - minVal) > rateFeed.details.allowedDeviation) { + revert MinAndMaxValuesDeviateTooMuch(minVal, maxVal); + } + + // In this implementation, we do not require sorted values, but we can add it + return NumericArrayLib.pickMedian(values); + } + + /// @notice Check each provider is a member of _rateFeedProviders[rateFeedId], revert if not. + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8 signerIndex) { + if ( + !EnumerableSet.contains( + _rateFeedProviders[_currentlyUpdatedRateFeedId], + signerAddress + ) + ) { + revert InvalidProviderForRateFeed( + _currentlyUpdatedRateFeedId, + signerAddress + ); + } + + // TODO: Replace hardcoding with dynamic signerAddresses + if (signerAddress == 0x8BB8F32Df04c8b654987DAaeD53D6B6091e3B774) { + return 0; + } else if ( + signerAddress == 0xdEB22f54738d54976C4c0fe5ce6d408E40d88499 + ) { + return 1; + } else if ( + signerAddress == 0x51Ce04Be4b3E32572C4Ec9135221d0691Ba7d202 + ) { + return 2; + } else if ( + signerAddress == 0xDD682daEC5A90dD295d14DA4b0bec9281017b5bE + ) { + return 3; + } else if ( + signerAddress == 0x9c5AE89C4Af6aA32cE58588DBaF90d18a855B6de + ) { + return 4; + } else { + revert SignerNotAuthorised(signerAddress); + } + } + + function parseValueWithCertainty( + uint256 valueWithCertainty + ) public pure returns (bool certainty, uint256 value) { + certainty = valueWithCertainty >= 2 ** 255; // most significant bit + value = valueWithCertainty & ((2 ** 255) - 1); // 255 least significant bits + } + + /********************************************************/ + /* FOR BACKWARDS-COMPATIBILITY ONLY */ + /* The below functions are only required for backwards- */ + /* compatibility with the old SortedOracles interface. */ + /* Once we fully retire it, we can remove them. */ + /********************************************************/ + // solhint-disable ordering + function medianRate( + address rateFeedId + ) external view returns (uint256 numerator, uint256 denominator) {} + + function medianTimestamp( + address rateFeedId + ) external view returns (uint256 timestamp) {} + + function numRates( + address rateFeedId + ) external view returns (uint256 _numRates) {} + + function isOldestReportExpired( + address rateFeedId + ) external view returns (bool isExpired, address zeroAddress) {} } diff --git a/src/OraclesV2.sol b/src/OraclesV2.sol deleted file mode 100644 index 3b7e46e..0000000 --- a/src/OraclesV2.sol +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; -import {RedstoneConsumerNumericBase, NumericArrayLib} from "redstone/contracts/core/RedstoneConsumerNumericBase.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {OracleValueLib, OracleValue} from "./lib/OracleValueLib.sol"; - -// Martin's original OracleValueLib: https://github.com/mento-protocol/OracleValueLib -// solhint-disable-next-line max-line-length -// Alex's original Oracles PoC: https://github.com/redstone-finance/redstone-evm-examples/blob/mento-v2-oracles-poc/contracts/mento-v2-oracles/MentoV2Oracles.sol - -// solhint-disable gas-struct-packing -contract OraclesV2 is RedstoneConsumerNumericBase { - using OracleValueLib for OracleValue; - - struct OracleBuffer { - /// @dev the cyclic buffer of most recent medians. - // A median is an unwrapped OracleValue converted to uint256 - uint256[100] medians; - OracleBufferInfo info; - } - - struct OracleBufferInfo { - /// @dev index of the most recently reported median. - uint8 lastIndex; - /// @dev the size of the window to average over. - uint8 windowSize; - /// @dev the buffer has been filled up at least once. - bool bufferFull; - /// @dev sum of the last `windowSize` values. - OracleValue windowSum; - /// @dev average of the last `windowSize` values (i.e. `windowSum / windowSize`). - OracleValue windowAverage; - /// @dev timestamp of the latest report. - uint40 latestTimestamp; - bool hasFreshness; - bool hasQuorum; - bool hasCertainty; - /// @dev the maximal deviation between providers’ values within a batch allowed, expressed as a factor < 1. - uint16 allowedDeviation; - /// @dev the minimal number of providers that need to be included in a report. - uint8 quorum; - /// @dev the minimal number of providers that need to be certain of their value. - uint8 certaintyThreshold; - /// @dev the allowed age of a report before it is considered stale, in seconds. - uint16 allowedStaleness; - } - - // Set of supported rate feeds. - EnumerableSet.AddressSet private _rateFeedIds; - - // mapping from _rateFeedIds to a set of provider addresses allowed to report values for the given rate feed. - mapping(address rateFeedId => EnumerableSet.AddressSet whitelistedRelayers) - private _rateFeedProviders; - - /// @dev mapping from rate feed id to the cyclic buffer of most recent medians. - mapping(address rateFeedId => OracleBuffer buffer) private _rateFeeds; - - address internal _currentlyUpdatedRateFeedId; - - error InvalidProviderForRateFeed(address rateFeedId, address provider); - error TimestampFromFutureIsNotAllowed( - uint256 receivedTimestampMilliseconds, - uint256 blockTimestamp - ); - error DataIsNotFresh( - uint256 receivedTimestampMilliseconds, - uint256 minAllowedTimestampForNewDataSeconds - ); - error CertaintyThresholdNotReached( - uint8 receivedCertainties, - uint8 certaintyThreshold - ); - error MinAndMaxValuesDeviateTooMuch(uint256 minVal, uint256 maxVal); - - /// @notice Main oracle function through which relayers submit new price data on-chain - /// @dev Receives a RedStone calldata payload, which includes signed and timestamped reports. - /// The calldata payload is extracted in assembly, hence no function param for the price data. - /// @dev The relayer is expected to ensure that each provider is allowed to report for the rate feed. - /// If not, the function will revert. - /// @dev The relayer is expected to sort the price values from lowest to highest. - /// If not, the function will revert. - function report(address rateFeedId) public { - _currentlyUpdatedRateFeedId = rateFeedId; - OracleBuffer storage rateFeed = _rateFeeds[rateFeedId]; - - // Extracts values from calldata via assembly - uint256 redstoneValue = getOracleNumericValueFromTxMsg( - bytes32(uint256(uint160(rateFeedId))) - ); - rateFeed.info.windowSum = OracleValueLib.fromRedStoneValue( - redstoneValue - ); - - // TODO: We still would need to decide how to select the latest data timestamp - // Because currently we assume providers provide the same timestamp - // If not - we need to discuss the way to calculate its aggregated value (e.g. median or min) - // TODO: naive uint40 conversion to satisfy compiler, needs to be done properly - rateFeed.info.latestTimestamp = uint40( - extractTimestampsAndAssertAllAreEqual() - ); - } - - function getUniqueSignersThreshold() - public - view - virtual - override - returns (uint8 quorum) - { - return _rateFeeds[_currentlyUpdatedRateFeedId].info.quorum; - } - - function validateTimestamp( - uint256 receivedTimestampMilliseconds - ) public view override { - uint256 receivedTimestampSeconds = receivedTimestampMilliseconds / 1000; - OracleBuffer storage rateFeed = _rateFeeds[_currentlyUpdatedRateFeedId]; - uint256 previousDataTimestampSeconds = rateFeed.info.latestTimestamp; - uint256 minAllowedTimestampForNewDataInSeconds = previousDataTimestampSeconds + - rateFeed.info.allowedStaleness; - - if ( - // solhint-disable gas-strict-inequalities - receivedTimestampSeconds <= minAllowedTimestampForNewDataInSeconds - ) { - revert DataIsNotFresh( - receivedTimestampMilliseconds, - minAllowedTimestampForNewDataInSeconds - ); - } - - if (receivedTimestampSeconds > block.timestamp) { - revert TimestampFromFutureIsNotAllowed( - receivedTimestampMilliseconds, - block.timestamp - ); - } - } - - function aggregateValues( - uint256[] memory valuesWithCertainties - ) public view virtual override returns (uint256 median) { - OracleBuffer storage rateFeed = _rateFeeds[_currentlyUpdatedRateFeedId]; - - uint256 valuesWithCertaintiesLength = valuesWithCertainties.length; - uint256[] memory values = new uint256[](valuesWithCertaintiesLength); - uint8 certainties = 0; - uint256 maxVal = 0; - uint256 minVal = type(uint256).max; - - for (uint256 i = 0; i < valuesWithCertaintiesLength; ++i) { - (bool certainty, uint256 value) = parseValueWithCertainty( - valuesWithCertainties[i] - ); - values[i] = value; - if (certainty) { - ++certainties; - } - if (value > maxVal) { - maxVal = value; - } - if (value < minVal) { - minVal = value; - } - } - - if (certainties < rateFeed.info.certaintyThreshold) { - revert CertaintyThresholdNotReached( - certainties, - rateFeed.info.certaintyThreshold - ); - } - - if ((maxVal - minVal) > rateFeed.info.allowedDeviation) { - revert MinAndMaxValuesDeviateTooMuch(minVal, maxVal); - } - - // In this implementation, we do not require sorted values, but we can add it - return NumericArrayLib.pickMedian(values); - } - - /// @notice Check each provider is a member of _rateFeedProviders[rateFeedId], revert if not - function getAuthorisedSignerIndex( - address signerAddress - ) public view virtual override returns (uint8 signerIndex) { - if ( - !EnumerableSet.contains( - _rateFeedProviders[_currentlyUpdatedRateFeedId], - signerAddress - ) - ) { - revert InvalidProviderForRateFeed( - _currentlyUpdatedRateFeedId, - signerAddress - ); - } - - // TODO: Replace hardcoding with dynamic signerAddresses - if (signerAddress == 0x8BB8F32Df04c8b654987DAaeD53D6B6091e3B774) { - return 0; - } else if ( - signerAddress == 0xdEB22f54738d54976C4c0fe5ce6d408E40d88499 - ) { - return 1; - } else if ( - signerAddress == 0x51Ce04Be4b3E32572C4Ec9135221d0691Ba7d202 - ) { - return 2; - } else if ( - signerAddress == 0xDD682daEC5A90dD295d14DA4b0bec9281017b5bE - ) { - return 3; - } else if ( - signerAddress == 0x9c5AE89C4Af6aA32cE58588DBaF90d18a855B6de - ) { - return 4; - } else { - revert SignerNotAuthorised(signerAddress); - } - } - - function parseValueWithCertainty( - uint256 valueWithCertainty - ) public pure returns (bool certainty, uint256 value) { - certainty = valueWithCertainty >= 2 ** 255; // most significant bit - value = valueWithCertainty & ((2 ** 255) - 1); // 255 least significant bits - } -} diff --git a/src/interfaces/IOracles.sol b/src/interfaces/IOracles.sol index 6d58eb3..3d8775d 100644 --- a/src/interfaces/IOracles.sol +++ b/src/interfaces/IOracles.sol @@ -1,41 +1,142 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// NOTE: Disabled ordering rule to allow for more logical grouping of functions starting with the most important ones. +/* solhint-disable max-line-length, ordering */ pragma solidity ^0.8.24; +/** + * @title IOracles Interface + * + * KEY ENTITIES + * @dev Owner = Mento Governance + * @dev Data Provider = Oracle node operator reporting individual price points to RedStone's DDL. + * @dev DDL = RedStone's Data Distribution Layer, an off-chain caching service for price data. + * @dev Relayer = Entities batching together indvidual price points from the DDL and submitting them on-chain. + * + * + * KEY CONCEPTS + * @dev Rate Feed = A specific price feed, e.g. "CELO/USD". + * @dev Rate Feed ID = A unique rate feed identifier calculated as e.g. address(uint160(uint256(keccak256("CELOUSD")))). + * + * @dev Price Value = A single price point from a data provider for a single rate feed. + * @dev Price Report = A batch of multiple price values from different data providers for a single rate feed. + * @dev Price Median = The median price value from a price report. For every rate feed, we store the 100 latest medians in a cyclic buffer + * @dev Price Window = The most recent n median prices in the cyclic buffer (n = `priceWindowSize`) + * @dev Average Price = The final price being returned to rate feed consumers. This is the average of the last `priceWindowSize` median prices in the cyclic buffer. + * + * @dev Validity Flags = A set of boolean flags that indicate the current state of a rate feed: `isCertain` | `isFresh` | `isWithinAllowedDeviation` + * @dev Certainty = Each price value is accompanied by a certainty score (boolean). This score is determined by + * the data provider and indicates their confidence in the reported value. If a more than + * `certaintyThreshold` providers are certain about a price value, a price report is considered + * "certain" in our contract. Breakers can be triggered if a report is not certain. + * @dev Freshness = A price report is considered fresh if the latest price value is not older than `allowedStalenessInSeconds`. + * @dev Allowed Deviation = The maximum allowed deviation between the lowest and highest price values in a price report. + */ interface IOracles { /** - * @notice Used to submit a new batch of signed price data. + * @notice Main input function through which relayers submit new batched price reports on-chain. * @param rateFeedId The rate feed for which prices are being submitted. - * @dev This function expects additional calldata in the form of a RedStone - * packed data payload, containing the signed reports from oracle node - * operators. - * See RedStone docs: - * https://docs.redstone.finance/docs/smart-contract-devs/how-it-works#data-packing-off-chain-data-encoding + * @dev This function expects additional calldata in form of a RedStone data package payload, + * which includes signed and timestamped price points from oracle node operators. The + * calldata payload is extracted in assembly, hence no function param for the price data. + * See RedStone docs: + * https://docs.redstone.finance/docs/smart-contract-devs/how-it-works#data-packing-off-chain-data-encoding + * @dev Relayers must ensure that each provider is allowed to report for the rate feed. + * If any data provider signature is invalid, the function will revert. + * @dev Relayers must sort the price values from lowest to highest. If not, the function will revert. */ function report(address rateFeedId) external; /** - * @notice Sets `hasFreshness` to `false` if the most recent report has - * become outdated. + * @notice Main output function returning the current price for a given rate feed + * @dev The price being returned is the average of the latest `priceWindowSize` median prices in the rate feed's cyclic price buffer. + * @param rateFeedId The rate feed to fetch the latest price for + * @return numerator The numerator of the price. + * @return denominator The denominator of the price, fixed at 1e24. + * @return lastUpdateTimestamp The timestamp of the last price update. + * @dev The denominator was chosen based on Celo's FixidityLib, which is used in the legacy SortedOracles oracle. See here: + * https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/common/FixidityLib.sol#L26 + * To get the price in this contract's internal format, and save a bit of gas on the consumer side, see `getExchangeRateAsUint64()`. + */ + function getExchangeRateFor( + address rateFeedId + ) + external + view + returns ( + uint256 numerator, + uint256 denominator, + uint40 lastUpdateTimestamp + ); + + /** + * @notice Adds a new supported rate feed. + * @param rateFeedId The new rate feed's ID, calculated as, i.e.: `address(uint160(uint256(keccak256("CELOUSD"))))` + * @param priceWindowSize The number of most recent median prices to average over for the final reported price. + * @param allowedDeviation The maximum allowed deviation between the lowest and highest price values in a price report + * @param quorum The minimum number of values per report. + * @param certaintyThreshold The minimum number of certain values per report. + * @param allowedStalenessInSeconds The allowed staleness in seconds. + * @param dataProviders The initial set of data providers for the new rate feed. + * @dev Only callable by the owner. + */ + function addRateFeed( + address rateFeedId, + uint8 priceWindowSize, + uint16 allowedDeviation, + uint8 quorum, + uint8 certaintyThreshold, + uint16 allowedStalenessInSeconds, + address[] calldata dataProviders + ) external; + + /** + * @notice Removes a rate feed. + * @param rateFeedId The rate feed's ID. + * @dev Only callable by the owner. + */ + function removeRateFeed(address rateFeedId) external; + + /** + * @notice Adds a new trusted data provider, i.e. an oracle node operator + * @param rateFeedId The rate feed for which the new data provider is allowed to report. + * @param provider The new data provider's address. + * @dev Only callable by the owner. + */ + function addDataProvider(address rateFeedId, address provider) external; + + /** + * @notice Removes a data provider from being allowed to report for a rate feed. + * @param rateFeedId The rate feed from which the data provider should be removed. + * @param provider The data provider's address. + * @dev Only callable by the owner. + */ + function removeDataProvider(address rateFeedId, address provider) external; + + /** + * @notice Sets validity flag `isFresh` to `0` for a rate feed if the most recent report has become outdated. * @param rateFeedId The rate feed to mark stale. */ function markStale(address rateFeedId) external; /** - * @notice Sets the window size over which a rate feed's medians will be - * averaged. + * @notice Sets the price window size over which a rate feed's median prices will be averaged. * @param rateFeedId The rate feed being configured. - * @param windowSize The number of most recent medians to average over for - * the final reported median. + * @param priceWindowSize The number of most recent median prices to average over for the final reported price. + * @dev For example, if `priceWindowSize` was 3 and the latest 5 median prices were [1, 3, 2, 3, 4], then we would + * average over the last three values [2, 3, 4] to get the final reported average price of (2 + 3 + 4) / 3 = 3 * @dev Only callable by the owner. */ - function setWindowSize(address rateFeedId, uint8 windowSize) external; + function setPriceWindowSize( + address rateFeedId, + uint8 priceWindowSize + ) external; /** - * @notice Sets the allowed deviation for a rate feed. + * @notice Sets the allowed price deviation between the lowest and highest price values in a report for a rate feed. * @param rateFeedId The rate feed being configured. - * @param allowedDeviation The maximal multiplicative deviation allowed - * between two values in a report batch, expressed as the numerator of a - * fraction over uint16.max. + * @param allowedDeviation The difference between the lowest and highest value in a price report. + * Expressed as the numerator of a fraction over uint16.max (65535). I.e., if allowedDeviation was 10_000, + * then the difference between price values can't be greater than 1_000 / 65_535 = 0.015259... ≈ 1.526% * @dev Only callable by the owner. */ function setAllowedDeviation( @@ -44,10 +145,9 @@ interface IOracles { ) external; /** - * @notice Sets the required quorum for a rate feed. + * @notice Sets the required quorum of data providers per price report for a rate feed. * @param rateFeedId The rate feed being configured. - * @param quorum The minimum number of individual reporters that need to be - * present in a report batch. + * @param quorum The minimum number of individual data providers that need to have reported a price in a batch. * @dev Only callable by the owner. */ function setQuorum(address rateFeedId, uint8 quorum) external; @@ -55,8 +155,8 @@ interface IOracles { /** * @notice Sets the certainty threshold for a rate feed. * @param rateFeedId The rate feed being configured. - * @param certaintyThreshold The minimum number of values that need to be - * denoted as certain in a batch for it to be considered valid. + * @param certaintyThreshold The minimum number of price values in a batch that need to be + * denoted as "certain" by the data providers for the report to be considered valid. * @dev Only callable by the owner. */ function setCertaintyThreshold( @@ -67,105 +167,106 @@ interface IOracles { /** * @notice Sets the allowed staleness for a rate feed. * @param rateFeedId The rate feed being configured. - * @param allowedStaleness The number of seconds before a report becomes - * considered stale and no longer valid. + * @param allowedStalenessInSeconds The number of seconds before a report is considered stale and no longer valid. * @dev Only callable by the owner. */ function setAllowedStaleness( address rateFeedId, - uint16 allowedStaleness + uint16 allowedStalenessInSeconds ) external; /** - * @notice Adds a new supported rate feed. - * @param rateFeedId The new rate feed's ID. - * @param windowSize The new rate feed's averaging window size. - * @param allowedDeviation The new rate feed's allowed deviation. - * @param quorum The new rate feed's required minimum number of values per - * report. - * @param certaintyThreshold The new rate feed's required minimum number of - * certain values per report. - * @param allowedStaleness The new rate feed's allowed staleness. - * @param dataProviders The initial set of data providers for the new rate - * feed. - * @dev Only callable by the owner. + * @notice Returns the price as a fixed fraction with 8 decimal digits after the decimal point. + * @dev Gas-optimized version of `getExchangeRateFor()`. Use this function if you only need the price as a uint64. + * @param rateFeedId The rate feed being queried. + * @return exchangeRate The price, expressed as the numerator of a fraction over 1e8 as a fixed denominator. */ - function addRateFeed( - address rateFeedId, - uint8 windowSize, - uint16 allowedDeviation, - uint8 quorum, - uint8 certaintyThreshold, - uint16 allowedStaleness, - address[] calldata dataProviders - ) external; + function getExchangeRateAsUint64( + address rateFeedId + ) external view returns (uint64 exchangeRate); /** - * @notice Removes a rate feed. - * @param rateFeedId The rate feed's ID. - * @dev Only callable by the owner. + * @notice Returns the latest price and validity flags. + * @param rateFeedId The rate feed being queried. + * @return medianRate The median rate. + * @return validityFlags The feed's current validity flags, packed into a uint8. + * Specifically: + * - Bit 0 (least significant): `isFresh` + * - Bit 1: `isCertain` + * - Bit 2: `isWithinAllowedDeviation` + * - Bits 3-7: unused. */ - function removeRateFeed(address rateFeedId) external; + function rateFeedInfo( + address rateFeedId + ) external view returns (uint64 medianRate, uint8 validityFlags); /** - * @notice Adds a new trusted data provider, i.e. a new offchain oracle node - * operator. - * @param rateFeedId The rate feed for which the new provider is allowed to - * report. - * @param provider The new provider's address. - * @dev Only callable by the owner. + * @notice Returns the current configuration parameters for a rate feed. + * @param rateFeedId The rate feed being queried. + * @return priceWindowSize The number of most recent median prices to average over for the final reported price. + * @return allowedDeviation The maximum allowed deviation between the lowest and highest price values in a price report. + * @return quorum The minimum number of values per report. + * @return certaintyThreshold The minimum number of certain values per report. + * @return allowedStalenessInSeconds The allowed staleness in seconds. */ - function addDataProvider(address rateFeedId, address provider) external; + function rateFeedConfig( + address rateFeedId + ) + external + view + returns ( + uint8 priceWindowSize, + uint16 allowedDeviation, + uint8 quorum, + uint8 certaintyThreshold, + uint16 allowedStalenessInSeconds + ); - /** - * @notice Removes a data provider from being allowed to report for a rate - * feed. - * @param rateFeedId The rate feed for which the provider was allowed to - * report. - * @param provider The provider's address. - * @dev Only callable by the owner. - */ - function removeDataProvider(address rateFeedId, address provider) external; + /********************************************************/ + /* FOR BACKWARDS-COMPATIBILITY ONLY */ + /* The below functions are only required for backwards- */ + /* compatibility with the old SortedOracles interface. */ + /* Once we fully retire it, we can remove them. */ + /********************************************************/ /** - * @notice Returns the median rate as a numerator and denominator, the - * denominator being fixed to 1e24. - * @param rateFeedId The rate feed whose median rate is queried. - * @return numerator The numerator of the median rate. - * @return denominator The denominator of the median rate, fixed at 1e24. - * @dev The denominator is chosen based on Celo's FixidityLib, which is used - * in the legacy SortedOracles oracle. See here: - * https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/contracts/common/FixidityLib.sol#L26 - * To get the rate in this contract's internal format, and save a bit of gas - * on the consumer side, see `medianRateUint64`. + * @notice Passthrough function that calls the new main interface `getExchangeRateFor()`. + * @dev We're ignoring the `lastUpdateTimestamp` as this wasn't part of the old SortedOracles interface. + * @param rateFeedId The rate feed to fetch the latest price for. + * @return numerator The numerator of the price. + * @return denominator The denominator of the price, fixed at 1e24. */ function medianRate( address rateFeedId ) external view returns (uint256 numerator, uint256 denominator); /** - * @notice Returns the median rate as a fixed fraction with 8 decimal digits - * after the decimal point. - * @param rateFeedId The rate feed whose median rate is queried. - * @return medianRate The median rate, expressed as the numerator of a fraction - * over 1e8. + * @notice Returns the timestamp of the latest price report. + * @dev Uses the new interface's `latestTimestamp` cast to uint256. + * @param rateFeedId The rate feed being queried. + * @return timestamp The timestamp of the latest price report for the specified rateFeedId. + */ + function medianTimestamp( + address rateFeedId + ) external view returns (uint256 timestamp); + + /** + * @notice Returns the rate feed's quorum as a proxy for the number of price values in the last report. + * @param rateFeedId The rateFeed being queried. + * @return _numRates The number of reported price values in the last report. */ - function medianRateUint64( + function numRates( address rateFeedId - ) external view returns (uint64 medianRate); + ) external view returns (uint256 _numRates); /** - * @notice Returns the median rate and validity flags. - * @param rateFeedId The rate feed being queried. - * @return medianRate The median rate. - * @return validityFlags The feed's current validity flags, packed into a uint8. - * Specifically: - * - Bit 0 (least significant): `hasFresnhess` - * - Bit 1: `hasQuorum` - * - Bit 2: `hasCertainty` - * - Bits 3-7: unused + * @notice Checks if the latest price report for a rate feed is stale. + * @param rateFeedId The rate feed being queried. + * @return isExpired A boolean returning the inverse of the `isFresh` validity flag from the new interface. + * @return zeroAddress We no longer store the oldest report's oracle address, so we return a zero address. + * This should be safe because SortedOracle consumers only care about the `isExpired` flag. */ - function rateInfo( + function isOldestReportExpired( address rateFeedId - ) external view returns (uint64 medianRate, uint8 validityFlags); + ) external view returns (bool isExpired, address zeroAddress); } diff --git a/test/Oracles.t.sol b/test/Oracles.t.sol index bf835ec..365ade6 100644 --- a/test/Oracles.t.sol +++ b/test/Oracles.t.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: UNLICENSED -// solhint-disable func-name-mixedcase, gas-strict-inequalities, ordering +// TODO: Re-enable rules after implementation is complete +// solhint-disable no-empty-blocks +// solhint-disable contract-name-camelcase, func-name-mixedcase, gas-strict-inequalities, ordering pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol"; import {Oracles} from "../src/Oracles.sol"; contract OraclesTest is Test { - Oracles oracles; + Oracles public oracles; - address aRateFeed; + address public aRateFeed; // solhint-disable-next-line no-empty-blocks function setUp() public virtual { oracles = new Oracles(); @@ -16,29 +18,32 @@ contract OraclesTest is Test { } } +// solhint-disable-next-line max-line-length +// Example for a Redstone calldata payload: +// https://github.com/redstone-finance/redstone-near-connectors/blob/7bc02fc5421200b15da56c33c5c6130130b3ff8a/js/test/integration.test.ts#L17-L37 contract Oracles_report is OraclesTest {} contract Oracles_markStale is OraclesTest {} -contract Oracles_setWindowSize is OraclesTest { - function testFuzz_setsWindowSize(uint8 windowSize) public { - vm.assume(windowSize != 0 && windowSize <= 100); - oracles.setWindowSize(aRateFeed, windowSize); - (uint8 realWindowSize, , , , ) = oracles.rateFeedParameters(aRateFeed); - assertEq(realWindowSize, windowSize); +contract Oracles_setPriceWindowSize is OraclesTest { + function testFuzz_setsWindowSize(uint8 priceWindowSize) public { + vm.assume(priceWindowSize != 0 && priceWindowSize <= 100); + oracles.setPriceWindowSize(aRateFeed, priceWindowSize); + (uint8 realWindowSize, , , , ) = oracles.rateFeedConfig(aRateFeed); + assertEq(realWindowSize, priceWindowSize); } function test_setTo0Fail() public { // TODO: set the exact expected error vm.expectRevert(); - oracles.setWindowSize(aRateFeed, 0); + oracles.setPriceWindowSize(aRateFeed, 0); } - function testFuzz_setToOver100Fail(uint8 windowSize) public { - vm.assume(windowSize > 100); + function testFuzz_setToOver100Fail(uint8 priceWindowSize) public { + vm.assume(priceWindowSize > 100); // TODO: set the exact expected error vm.expectRevert(); - oracles.setWindowSize(aRateFeed, windowSize); + oracles.setPriceWindowSize(aRateFeed, priceWindowSize); } /* @@ -60,7 +65,7 @@ contract Oracles_setWindowSize is OraclesTest { contract Oracles_setAllowedDeviation is OraclesTest { function testFuzz_setsAllowedDeviation(uint16 allowedDeviation) public { oracles.setAllowedDeviation(aRateFeed, allowedDeviation); - (, uint16 realAllowedDeviation, , , ) = oracles.rateFeedParameters( + (, uint16 realAllowedDeviation, , , ) = oracles.rateFeedConfig( aRateFeed ); assertEq(realAllowedDeviation, allowedDeviation); @@ -78,7 +83,7 @@ contract Oracles_setAllowedDeviation is OraclesTest { contract Oracles_setQuorum is OraclesTest { function testFuzz_setsQuorum(uint8 quorum) public { oracles.setQuorum(aRateFeed, quorum); - (, , uint8 realQuorum, , ) = oracles.rateFeedParameters(aRateFeed); + (, , uint8 realQuorum, , ) = oracles.rateFeedConfig(aRateFeed); assertEq(realQuorum, quorum); } @@ -95,7 +100,7 @@ contract Oracles_setQuorum is OraclesTest { contract Oracles_setCertaintyThreshold is OraclesTest { function testFuzz_setsCertaintyThreshold(uint8 certaintyThreshold) public { oracles.setCertaintyThreshold(aRateFeed, certaintyThreshold); - (, , , uint8 realCertaintyThreshold, ) = oracles.rateFeedParameters( + (, , , uint8 realCertaintyThreshold, ) = oracles.rateFeedConfig( aRateFeed ); assertEq(realCertaintyThreshold, certaintyThreshold); @@ -114,12 +119,14 @@ contract Oracles_setCertaintyThreshold is OraclesTest { } contract Oracles_setAllowedStaleness is OraclesTest { - function testFuzz_setsAllowedStaleness(uint16 allowedStaleness) public { - oracles.setAllowedStaleness(aRateFeed, allowedStaleness); - (, , , , uint16 realAllowedStaleness) = oracles.rateFeedParameters( + function testFuzz_setsAllowedStaleness( + uint16 allowedStalenessInSeconds + ) public { + oracles.setAllowedStaleness(aRateFeed, allowedStalenessInSeconds); + (, , , , uint16 realAllowedStaleness) = oracles.rateFeedConfig( aRateFeed ); - assertEq(realAllowedStaleness, allowedStaleness); + assertEq(realAllowedStaleness, allowedStalenessInSeconds); } /* @@ -149,7 +156,7 @@ contract Oracles_addRateFeed is OraclesTest { uint8 realQuorum, uint8 realCertaintyThreshold, uint16 realAllowedStaleness - ) = oracles.rateFeedParameters(anotherRateFeed); + ) = oracles.rateFeedConfig(anotherRateFeed); assertEq(realWindowSize, 2); assertEq(realAllowedDeviation, 100); @@ -166,29 +173,29 @@ contract Oracles_addRateFeed is OraclesTest { } contract Oracles_removeRateFeed is OraclesTest { - address anotherRateFeed = address(0xbeef); - address aDataProvider = address(0xcafe); + address private _anotherRateFeed = address(0xbeef); + address private _aDataProvider = address(0xcafe); function setUp() public override { super.setUp(); address[] memory dataProviders = new address[](1); dataProviders[0] = address(0xcafe); - oracles.addRateFeed(anotherRateFeed, 2, 100, 5, 3, 120, dataProviders); - - ( - uint8 realWindowSize, - uint16 realAllowedDeviation, - uint8 realQuorum, - uint8 realCertaintyThreshold, - uint16 realAllowedStaleness - ) = oracles.rateFeedParameters(anotherRateFeed); + oracles.addRateFeed({ + rateFeedId: _anotherRateFeed, + priceWindowSize: 2, + allowedDeviation: 100, + quorum: 5, + certaintyThreshold: 3, + allowedStalenessInSeconds: 120, + dataProviders: dataProviders + }); } function test_removesTheRateFeed() public { - oracles.removeRateFeed(anotherRateFeed); + oracles.removeRateFeed(_anotherRateFeed); - (uint8 realWindowSize, , , , ) = oracles.rateFeedParameters( - anotherRateFeed + (uint8 realWindowSize, , , , ) = oracles.rateFeedConfig( + _anotherRateFeed ); assertEq(realWindowSize, 0); } diff --git a/test/RedStonePayload.t.sol b/test/RedStonePayload.t.sol index a04db70..4a27ad2 100644 --- a/test/RedStonePayload.t.sol +++ b/test/RedStonePayload.t.sol @@ -8,7 +8,7 @@ import {RedStonePayload} from "./lib/RedStonePayload.sol"; contract RedStonePayloadTest is Test {} contract RedStonePayloadTest_makePayload is RedStonePayloadTest { - function test_makePayload() public { + function test_makePayload() public pure { bytes32 dataFeedId = "USDCELO"; uint256[] memory values = new uint256[](1); bytes32[] memory rs = new bytes32[](1); @@ -40,7 +40,7 @@ contract RedStonePayloadTest_makePayload is RedStonePayloadTest { } contract RedStonePayload_serializePayload is RedStonePayloadTest { - function test_serializePayload() public { + function test_serializePayload() public pure { bytes32 dataFeedId = "USDCELO"; uint256[] memory values = new uint256[](1); bytes32[] memory rs = new bytes32[](1); diff --git a/test/lib/RedStonePayload.sol b/test/lib/RedStonePayload.sol index 94e9d0d..9711615 100644 --- a/test/lib/RedStonePayload.sol +++ b/test/lib/RedStonePayload.sol @@ -12,47 +12,6 @@ library RedStonePayload { uint256 currentIndex; } - // Constants from - // solhint-disable-next-line max-line-length - // https://github.com/redstone-finance/redstone-oracles-monorepo/blob/main/packages/protocol/src/common/redstone-constants.ts - // Number of bytes reserved to store timestamp - uint256 constant TIMESTAMP_BS = 6; - - // Number of bytes reserved to store the number of data points - uint256 constant DATA_POINTS_COUNT_BS = 3; - - // Number of bytes reserved to store datapoints byte size - uint256 constant DATA_POINT_VALUE_BYTE_SIZE_BS = 4; - - // Default value byte size for numeric values - uint256 constant DEFAULT_NUM_VALUE_BS = 32; - - // Default precision for numeric values - uint256 constant DEFAULT_NUM_VALUE_DECIMALS = 8; - - // Number of bytes reserved for data packages count - uint256 constant DATA_PACKAGES_COUNT_BS = 2; - - // Number of bytes reserved for unsigned metadata byte size - uint256 constant UNSIGNED_METADATA_BYTE_SIZE_BS = 3; - - // RedStone marker, which will be appended in the end of each transaction - uint256 constant REDSTONE_MARKER = 0x000002ed57011e0000; - - // Byte size of RedStone marker - // we subtract 1 because of the 0x prefix - uint256 constant REDSTONE_MARKER_BS = 9; - - // Byte size of signatures - uint256 constant SIGNATURE_BS = 65; - - // Byte size of data feed id - uint256 constant DATA_FEED_ID_BS = 32; - - // RedStone allows a single oracle to report for multiple feeds in a single - // batch, but our model assumes each batch is for a single data feed. - uint256 constant DATA_POINTS_PER_PACKAGE = 1; - struct DataPoint { bytes32 dataFeedId; uint256 value; @@ -84,6 +43,47 @@ library RedStonePayload { string metadata; } + // Constants from + // solhint-disable-next-line max-line-length + // https://github.com/redstone-finance/redstone-oracles-monorepo/blob/main/packages/protocol/src/common/redstone-constants.ts + // Number of bytes reserved to store timestamp + uint256 private constant TIMESTAMP_BS = 6; + + // Number of bytes reserved to store the number of data points + uint256 private constant DATA_POINTS_COUNT_BS = 3; + + // Number of bytes reserved to store datapoints byte size + uint256 private constant DATA_POINT_VALUE_BYTE_SIZE_BS = 4; + + // Default value byte size for numeric values + uint256 private constant DEFAULT_NUM_VALUE_BS = 32; + + // Default precision for numeric values + uint256 private constant DEFAULT_NUM_VALUE_DECIMALS = 8; + + // Number of bytes reserved for data packages count + uint256 private constant DATA_PACKAGES_COUNT_BS = 2; + + // Number of bytes reserved for unsigned metadata byte size + uint256 private constant UNSIGNED_METADATA_BYTE_SIZE_BS = 3; + + // RedStone marker, which will be appended in the end of each transaction + uint256 private constant REDSTONE_MARKER = 0x000002ed57011e0000; + + // Byte size of RedStone marker + // we subtract 1 because of the 0x prefix + uint256 private constant REDSTONE_MARKER_BS = 9; + + // Byte size of signatures + uint256 private constant SIGNATURE_BS = 65; + + // Byte size of data feed id + uint256 private constant DATA_FEED_ID_BS = 32; + + // RedStone allows a single oracle to report for multiple feeds in a single + // batch, but our model assumes each batch is for a single data feed. + uint256 private constant DATA_POINTS_PER_PACKAGE = 1; + function makePayload( bytes32 dataFeedId, uint256[] memory values, @@ -220,7 +220,7 @@ library RedStonePayload { Payload memory payload ) internal pure returns (bytes memory) { uint256 numberDataPackages = payload.dataPackages.length; - // solhint-disable prettier/prettier + // prettier-ignore uint256 serializedPayloadLength = REDSTONE_MARKER_BS + UNSIGNED_METADATA_BYTE_SIZE_BS + // + 0 for actual metadata in our case DATA_PACKAGES_COUNT_BS + @@ -234,7 +234,6 @@ library RedStonePayload { DEFAULT_NUM_VALUE_BS ) ); - // solhint-enable prettier/prettier SerializationBuffer memory buffer = newSerializationBuffer( serializedPayloadLength