diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9296efd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable diff --git a/README.md b/README.md index 9265b45..ed907f8 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,3 @@ -## Foundry +# Redstone Contracts -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** - -Foundry consists of: - -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. - -## Documentation - -https://book.getfoundry.sh/ - -## Usage - -### Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +This repository serves as an auxiliary collection of smart contracts specifically designed to be used within a Foundry development environment. The primary purpose of this repository is to import contracts from the [Redstone Oracles Monorepo](https://github.com/redstone-finance/redstone-oracles-monorepo) for integration in Foundry as importing all the monorepo contracts would be impractical (and causes build errors). diff --git a/lib/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol b/lib/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol new file mode 100644 index 0000000..3007a2a --- /dev/null +++ b/lib/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..978ac6f --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..0457042 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 0457042d93d9dfd760dbaa06a4d2f1216fdbe297 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..58fa0f8 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21 diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..832ecb7 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,4 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@redstone-finance/evm-connector/contracts/=src/evm-connector/ +@chainlink=lib/@chainlink \ No newline at end of file diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index df9ee8b..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/evm-connector/core/CalldataExtractor.sol b/src/evm-connector/core/CalldataExtractor.sol new file mode 100644 index 0000000..d1c5f7a --- /dev/null +++ b/src/evm-connector/core/CalldataExtractor.sol @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +import "./RedstoneConstants.sol"; + +/** + * @title The base contract with the main logic of data extraction from calldata + * @author The Redstone Oracles team + * @dev This contract was created to reuse the same logic in the RedstoneConsumerBase + * and the ProxyConnector contracts + */ +contract CalldataExtractor is RedstoneConstants { + using SafeMath for uint256; + + error DataPackageTimestampMustNotBeZero(); + error DataPackageTimestampsMustBeEqual(); + error RedstonePayloadMustHaveAtLeastOneDataPackage(); + + function extractTimestampsAndAssertAllAreEqual() public pure returns (uint256 extractedTimestamp) { + uint256 calldataNegativeOffset = _extractByteSizeOfUnsignedMetadata(); + uint256 dataPackagesCount = _extractDataPackagesCountFromCalldata(calldataNegativeOffset); + + if (dataPackagesCount == 0) { + revert RedstonePayloadMustHaveAtLeastOneDataPackage(); + } + + calldataNegativeOffset += DATA_PACKAGES_COUNT_BS; + for (uint256 dataPackageIndex = 0; dataPackageIndex < dataPackagesCount; dataPackageIndex++) { + uint256 dataPackageByteSize = _getDataPackageByteSize(calldataNegativeOffset); + + // Extracting timestamp for the current data package + uint48 dataPackageTimestamp; // uint48, because timestamp uses 6 bytes + uint256 timestampNegativeOffset = (calldataNegativeOffset + TIMESTAMP_NEGATIVE_OFFSET_IN_DATA_PACKAGE_WITH_STANDARD_SLOT_BS); + uint256 timestampOffset = msg.data.length - timestampNegativeOffset; + assembly { + dataPackageTimestamp := calldataload(timestampOffset) + } + + if (dataPackageTimestamp == 0) { + revert DataPackageTimestampMustNotBeZero(); + } + + if (extractedTimestamp == 0) { + extractedTimestamp = dataPackageTimestamp; + } else if (dataPackageTimestamp != extractedTimestamp) { + revert DataPackageTimestampsMustBeEqual(); + } + + calldataNegativeOffset += dataPackageByteSize; + } + } + + function _getDataPackageByteSize(uint256 calldataNegativeOffset) internal pure returns (uint256) { + ( + uint256 dataPointsCount, + uint256 eachDataPointValueByteSize + ) = _extractDataPointsDetailsForDataPackage(calldataNegativeOffset); + + return + dataPointsCount * + (DATA_POINT_SYMBOL_BS + eachDataPointValueByteSize) + + DATA_PACKAGE_WITHOUT_DATA_POINTS_BS; + } + + function _extractByteSizeOfUnsignedMetadata() internal pure returns (uint256) { + // Checking if the calldata ends with the RedStone marker + bool hasValidRedstoneMarker; + assembly { + let calldataLast32Bytes := calldataload(sub(calldatasize(), STANDARD_SLOT_BS)) + hasValidRedstoneMarker := eq( + REDSTONE_MARKER_MASK, + and(calldataLast32Bytes, REDSTONE_MARKER_MASK) + ) + } + if (!hasValidRedstoneMarker) { + revert CalldataMustHaveValidPayload(); + } + + // Using uint24, because unsigned metadata byte size number has 3 bytes + uint24 unsignedMetadataByteSize; + if (REDSTONE_MARKER_BS_PLUS_STANDARD_SLOT_BS > msg.data.length) { + revert CalldataOverOrUnderFlow(); + } + assembly { + unsignedMetadataByteSize := calldataload( + sub(calldatasize(), REDSTONE_MARKER_BS_PLUS_STANDARD_SLOT_BS) + ) + } + uint256 calldataNegativeOffset = unsignedMetadataByteSize + + UNSIGNED_METADATA_BYTE_SIZE_BS + + REDSTONE_MARKER_BS; + if (calldataNegativeOffset + DATA_PACKAGES_COUNT_BS > msg.data.length) { + revert IncorrectUnsignedMetadataSize(); + } + return calldataNegativeOffset; + } + + // We return uint16, because unsigned metadata byte size number has 2 bytes + function _extractDataPackagesCountFromCalldata(uint256 calldataNegativeOffset) + internal + pure + returns (uint16 dataPackagesCount) + { + uint256 calldataNegativeOffsetWithStandardSlot = calldataNegativeOffset + STANDARD_SLOT_BS; + if (calldataNegativeOffsetWithStandardSlot > msg.data.length) { + revert CalldataOverOrUnderFlow(); + } + assembly { + dataPackagesCount := calldataload( + sub(calldatasize(), calldataNegativeOffsetWithStandardSlot) + ) + } + return dataPackagesCount; + } + + function _extractDataPointValueAndDataFeedId( + uint256 calldataNegativeOffsetForDataPackage, + uint256 defaultDataPointValueByteSize, + uint256 dataPointIndex + ) internal pure virtual returns (bytes32 dataPointDataFeedId, uint256 dataPointValue) { + uint256 negativeOffsetToDataPoints = calldataNegativeOffsetForDataPackage + DATA_PACKAGE_WITHOUT_DATA_POINTS_BS; + uint256 dataPointNegativeOffset = negativeOffsetToDataPoints.add( + (1 + dataPointIndex).mul((defaultDataPointValueByteSize + DATA_POINT_SYMBOL_BS)) + ); + uint256 dataPointCalldataOffset = msg.data.length.sub(dataPointNegativeOffset); + assembly { + dataPointDataFeedId := calldataload(dataPointCalldataOffset) + dataPointValue := calldataload(add(dataPointCalldataOffset, DATA_POINT_SYMBOL_BS)) + } + } + + function _extractDataPointsDetailsForDataPackage(uint256 calldataNegativeOffsetForDataPackage) + internal + pure + returns (uint256 dataPointsCount, uint256 eachDataPointValueByteSize) + { + // Using uint24, because data points count byte size number has 3 bytes + uint24 dataPointsCount_; + + // Using uint32, because data point value byte size has 4 bytes + uint32 eachDataPointValueByteSize_; + + // Extract data points count + uint256 negativeCalldataOffset = calldataNegativeOffsetForDataPackage + SIG_BS; + uint256 calldataOffset = msg.data.length.sub(negativeCalldataOffset + STANDARD_SLOT_BS); + assembly { + dataPointsCount_ := calldataload(calldataOffset) + } + + // Extract each data point value size + calldataOffset = calldataOffset.sub(DATA_POINTS_COUNT_BS); + assembly { + eachDataPointValueByteSize_ := calldataload(calldataOffset) + } + + // Prepare returned values + dataPointsCount = dataPointsCount_; + eachDataPointValueByteSize = eachDataPointValueByteSize_; + } +} diff --git a/src/evm-connector/core/ProxyConnector.sol b/src/evm-connector/core/ProxyConnector.sol new file mode 100644 index 0000000..8a3ec87 --- /dev/null +++ b/src/evm-connector/core/ProxyConnector.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "./RedstoneConstants.sol"; +import "./CalldataExtractor.sol"; + +/** + * @title The base contract for forwarding redstone payload to other contracts + * @author The Redstone Oracles team + */ +contract ProxyConnector is RedstoneConstants, CalldataExtractor { + error ProxyCalldataFailedWithoutErrMsg(); + error ProxyCalldataFailedWithStringMessage(string message); + error ProxyCalldataFailedWithCustomError(bytes result); + + function proxyCalldata( + address contractAddress, + bytes memory encodedFunction, + bool forwardValue + ) internal returns (bytes memory) { + bytes memory message = _prepareMessage(encodedFunction); + + (bool success, bytes memory result) = + contractAddress.call{value: forwardValue ? msg.value : 0}(message); + + return _prepareReturnValue(success, result); + } + + function proxyDelegateCalldata(address contractAddress, bytes memory encodedFunction) + internal + returns (bytes memory) + { + bytes memory message = _prepareMessage(encodedFunction); + (bool success, bytes memory result) = contractAddress.delegatecall(message); + return _prepareReturnValue(success, result); + } + + function proxyCalldataView(address contractAddress, bytes memory encodedFunction) + internal + view + returns (bytes memory) + { + bytes memory message = _prepareMessage(encodedFunction); + (bool success, bytes memory result) = contractAddress.staticcall(message); + return _prepareReturnValue(success, result); + } + + function _prepareMessage(bytes memory encodedFunction) private pure returns (bytes memory) { + uint256 encodedFunctionBytesCount = encodedFunction.length; + uint256 redstonePayloadByteSize = _getRedstonePayloadByteSize(); + uint256 resultMessageByteSize = encodedFunctionBytesCount + redstonePayloadByteSize; + + if (redstonePayloadByteSize > msg.data.length) { + revert CalldataOverOrUnderFlow(); + } + + bytes memory message; + + assembly { + message := mload(FREE_MEMORY_PTR) // sets message pointer to first free place in memory + + // Saving the byte size of the result message (it's a standard in EVM) + mstore(message, resultMessageByteSize) + + // Copying function and its arguments + for { + let from := add(BYTES_ARR_LEN_VAR_BS, encodedFunction) + let fromEnd := add(from, encodedFunctionBytesCount) + let to := add(BYTES_ARR_LEN_VAR_BS, message) + } lt (from, fromEnd) { + from := add(from, STANDARD_SLOT_BS) + to := add(to, STANDARD_SLOT_BS) + } { + // Copying data from encodedFunction to message (32 bytes at a time) + mstore(to, mload(from)) + } + + // Copying redstone payload to the message bytes + calldatacopy( + add(message, add(BYTES_ARR_LEN_VAR_BS, encodedFunctionBytesCount)), // address + sub(calldatasize(), redstonePayloadByteSize), // offset + redstonePayloadByteSize // bytes length to copy + ) + + // Updating free memory pointer + mstore( + FREE_MEMORY_PTR, + add( + add(message, add(redstonePayloadByteSize, encodedFunctionBytesCount)), + BYTES_ARR_LEN_VAR_BS + ) + ) + } + + return message; + } + + function _getRedstonePayloadByteSize() private pure returns (uint256) { + uint256 calldataNegativeOffset = _extractByteSizeOfUnsignedMetadata(); + uint256 dataPackagesCount = _extractDataPackagesCountFromCalldata(calldataNegativeOffset); + calldataNegativeOffset += DATA_PACKAGES_COUNT_BS; + for (uint256 dataPackageIndex = 0; dataPackageIndex < dataPackagesCount; dataPackageIndex++) { + uint256 dataPackageByteSize = _getDataPackageByteSize(calldataNegativeOffset); + calldataNegativeOffset += dataPackageByteSize; + } + + return calldataNegativeOffset; + } + + function _prepareReturnValue(bool success, bytes memory result) + internal + pure + returns (bytes memory) + { + if (!success) { + + if (result.length == 0) { + revert ProxyCalldataFailedWithoutErrMsg(); + } else { + bool isStringErrorMessage; + assembly { + let first32BytesOfResult := mload(add(result, BYTES_ARR_LEN_VAR_BS)) + isStringErrorMessage := eq(first32BytesOfResult, STRING_ERR_MESSAGE_MASK) + } + + if (isStringErrorMessage) { + string memory receivedErrMsg; + assembly { + receivedErrMsg := add(result, REVERT_MSG_OFFSET) + } + revert ProxyCalldataFailedWithStringMessage(receivedErrMsg); + } else { + revert ProxyCalldataFailedWithCustomError(result); + } + } + } + + return result; + } +} diff --git a/src/evm-connector/core/RedstoneConstants.sol b/src/evm-connector/core/RedstoneConstants.sol new file mode 100644 index 0000000..ad4bf2f --- /dev/null +++ b/src/evm-connector/core/RedstoneConstants.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +/** + * @title The base contract with helpful constants + * @author The Redstone Oracles team + * @dev It mainly contains redstone-related values, which improve readability + * of other contracts (e.g. CalldataExtractor and RedstoneConsumerBase) + */ +contract RedstoneConstants { + // === Abbreviations === + // BS - Bytes size + // PTR - Pointer (memory location) + // SIG - Signature + + // Solidity and YUL constants + uint256 internal constant STANDARD_SLOT_BS = 32; + uint256 internal constant FREE_MEMORY_PTR = 0x40; + uint256 internal constant BYTES_ARR_LEN_VAR_BS = 32; + uint256 internal constant FUNCTION_SIGNATURE_BS = 4; + uint256 internal constant REVERT_MSG_OFFSET = 68; // Revert message structure described here: https://ethereum.stackexchange.com/a/66173/106364 + uint256 internal constant STRING_ERR_MESSAGE_MASK = 0x08c379a000000000000000000000000000000000000000000000000000000000; + + // RedStone protocol consts + uint256 internal constant SIG_BS = 65; + uint256 internal constant TIMESTAMP_BS = 6; + uint256 internal constant DATA_PACKAGES_COUNT_BS = 2; + uint256 internal constant DATA_POINTS_COUNT_BS = 3; + uint256 internal constant DATA_POINT_VALUE_BYTE_SIZE_BS = 4; + uint256 internal constant DATA_POINT_SYMBOL_BS = 32; + uint256 internal constant DEFAULT_DATA_POINT_VALUE_BS = 32; + uint256 internal constant UNSIGNED_METADATA_BYTE_SIZE_BS = 3; + uint256 internal constant REDSTONE_MARKER_BS = 9; // byte size of 0x000002ed57011e0000 + uint256 internal constant REDSTONE_MARKER_MASK = 0x0000000000000000000000000000000000000000000000000002ed57011e0000; + + // Derived values (based on consts) + uint256 internal constant TIMESTAMP_NEGATIVE_OFFSET_IN_DATA_PACKAGE_WITH_STANDARD_SLOT_BS = 104; // SIG_BS + DATA_POINTS_COUNT_BS + DATA_POINT_VALUE_BYTE_SIZE_BS + STANDARD_SLOT_BS + uint256 internal constant DATA_PACKAGE_WITHOUT_DATA_POINTS_BS = 78; // DATA_POINT_VALUE_BYTE_SIZE_BS + TIMESTAMP_BS + DATA_POINTS_COUNT_BS + SIG_BS + uint256 internal constant DATA_PACKAGE_WITHOUT_DATA_POINTS_AND_SIG_BS = 13; // DATA_POINT_VALUE_BYTE_SIZE_BS + TIMESTAMP_BS + DATA_POINTS_COUNT_BS + uint256 internal constant REDSTONE_MARKER_BS_PLUS_STANDARD_SLOT_BS = 41; // REDSTONE_MARKER_BS + STANDARD_SLOT_BS + + // Error messages + error CalldataOverOrUnderFlow(); + error IncorrectUnsignedMetadataSize(); + error InsufficientNumberOfUniqueSigners(uint256 receivedSignersCount, uint256 requiredSignersCount); + error EachSignerMustProvideTheSameValue(); + error EmptyCalldataPointersArr(); + error InvalidCalldataPointer(); + error CalldataMustHaveValidPayload(); + error SignerNotAuthorised(address receivedSigner); +} diff --git a/src/evm-connector/core/RedstoneConsumerBase.sol b/src/evm-connector/core/RedstoneConsumerBase.sol new file mode 100644 index 0000000..b15630a --- /dev/null +++ b/src/evm-connector/core/RedstoneConsumerBase.sol @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +import "./RedstoneConstants.sol"; +import "./RedstoneDefaultsLib.sol"; +import "./CalldataExtractor.sol"; +import "../libs/BitmapLib.sol"; +import "../libs/SignatureLib.sol"; + +/** + * @title The base contract with the main Redstone logic + * @author The Redstone Oracles team + * @dev Do not use this contract directly in consumer contracts, take a + * look at `RedstoneConsumerNumericBase` and `RedstoneConsumerBytesBase` instead + */ +abstract contract RedstoneConsumerBase is CalldataExtractor { + using SafeMath for uint256; + + error GetDataServiceIdNotImplemented(); + + /* ========== VIRTUAL FUNCTIONS (MAY BE OVERRIDDEN IN CHILD CONTRACTS) ========== */ + + /** + * @dev This function must be implemented by the child consumer contract. + * It should return dataServiceId which DataServiceWrapper will use if not provided explicitly . + * If not overridden, value will always have to be provided explicitly in DataServiceWrapper. + * @return dataServiceId being consumed by contract + */ + function getDataServiceId() public view virtual returns (string memory) { + revert GetDataServiceIdNotImplemented(); + } + + /** + * @dev This function must be implemented by the child consumer contract. + * It should return a unique index for a given signer address if the signer + * is authorised, otherwise it should revert + * @param receivedSigner The address of a signer, recovered from ECDSA signature + * @return Unique index for a signer in the range [0..255] + */ + function getAuthorisedSignerIndex(address receivedSigner) public view virtual returns (uint8); + + /** + * @dev This function may be overridden by the child consumer contract. + * It should validate the timestamp against the current time (block.timestamp) + * It should revert with a helpful message if the timestamp is not valid + * @param receivedTimestampMilliseconds Timestamp extracted from calldata + */ + function validateTimestamp(uint256 receivedTimestampMilliseconds) public view virtual { + RedstoneDefaultsLib.validateTimestamp(receivedTimestampMilliseconds); + } + + /** + * @dev This function should be overridden by the child consumer contract. + * @return The minimum required value of unique authorised signers + */ + function getUniqueSignersThreshold() public view virtual returns (uint8) { + return 1; + } + + /** + * @dev This function may be overridden by the child consumer contract. + * It should aggregate values from different signers to a single uint value. + * By default, it calculates the median value + * @param values An array of uint256 values from different signers + * @return Result of the aggregation in the form of a single number + */ + function aggregateValues(uint256[] memory values) public view virtual returns (uint256) { + return RedstoneDefaultsLib.aggregateValues(values); + } + + /* ========== FUNCTIONS WITH IMPLEMENTATION (CAN NOT BE OVERRIDDEN) ========== */ + + /** + * @dev This is an internal helpful function for secure extraction oracle values + * from the tx calldata. Security is achieved by signatures verification, timestamp + * validation, and aggregating values from different authorised signers into a + * single numeric value. If any of the required conditions (e.g. too old timestamp or + * insufficient number of authorised signers) do not match, the function will revert. + * + * Note! You should not call this function in a consumer contract. You can use + * `getOracleNumericValuesFromTxMsg` or `getOracleNumericValueFromTxMsg` instead. + * + * @param dataFeedIds An array of unique data feed identifiers + * @return An array of the extracted and verified oracle values in the same order + * as they are requested in dataFeedIds array + */ + function _securelyExtractOracleValuesFromTxMsg(bytes32[] memory dataFeedIds) + internal + view + returns (uint256[] memory) + { + // Initializing helpful variables and allocating memory + uint256[] memory uniqueSignerCountForDataFeedIds = new uint256[](dataFeedIds.length); + uint256[] memory signersBitmapForDataFeedIds = new uint256[](dataFeedIds.length); + uint256[][] memory valuesForDataFeeds = new uint256[][](dataFeedIds.length); + for (uint256 i = 0; i < dataFeedIds.length; i++) { + // The line below is commented because newly allocated arrays are filled with zeros + // But we left it for better readability + // signersBitmapForDataFeedIds[i] = 0; // <- setting to an empty bitmap + valuesForDataFeeds[i] = new uint256[](getUniqueSignersThreshold()); + } + + // Extracting the number of data packages from calldata + uint256 calldataNegativeOffset = _extractByteSizeOfUnsignedMetadata(); + uint256 dataPackagesCount = _extractDataPackagesCountFromCalldata(calldataNegativeOffset); + calldataNegativeOffset += DATA_PACKAGES_COUNT_BS; + + // Saving current free memory pointer + uint256 freeMemPtr; + assembly { + freeMemPtr := mload(FREE_MEMORY_PTR) + } + + // Data packages extraction in a loop + for (uint256 dataPackageIndex = 0; dataPackageIndex < dataPackagesCount; dataPackageIndex++) { + // Extract data package details and update calldata offset + uint256 dataPackageByteSize = _extractDataPackage( + dataFeedIds, + uniqueSignerCountForDataFeedIds, + signersBitmapForDataFeedIds, + valuesForDataFeeds, + calldataNegativeOffset + ); + calldataNegativeOffset += dataPackageByteSize; + + // Shifting memory pointer back to the "safe" value + assembly { + mstore(FREE_MEMORY_PTR, freeMemPtr) + } + } + + // Validating numbers of unique signers and calculating aggregated values for each dataFeedId + return _getAggregatedValues(valuesForDataFeeds, uniqueSignerCountForDataFeedIds); + } + + /** + * @dev This is a private helpful function, which extracts data for a data package based + * on the given negative calldata offset, verifies them, and in the case of successful + * verification updates the corresponding data package values in memory + * + * @param dataFeedIds an array of unique data feed identifiers + * @param uniqueSignerCountForDataFeedIds an array with the numbers of unique signers + * for each data feed + * @param signersBitmapForDataFeedIds an array of signer bitmaps for data feeds + * @param valuesForDataFeeds 2-dimensional array, valuesForDataFeeds[i][j] contains + * j-th value for the i-th data feed + * @param calldataNegativeOffset negative calldata offset for the given data package + * + * @return An array of the aggregated values + */ + function _extractDataPackage( + bytes32[] memory dataFeedIds, + uint256[] memory uniqueSignerCountForDataFeedIds, + uint256[] memory signersBitmapForDataFeedIds, + uint256[][] memory valuesForDataFeeds, + uint256 calldataNegativeOffset + ) private view returns (uint256) { + uint256 signerIndex; + + ( + uint256 dataPointsCount, + uint256 eachDataPointValueByteSize + ) = _extractDataPointsDetailsForDataPackage(calldataNegativeOffset); + + // We use scopes to resolve problem with too deep stack + { + uint48 extractedTimestamp; + address signerAddress; + bytes32 signedHash; + bytes memory signedMessage; + uint256 signedMessageBytesCount; + + signedMessageBytesCount = dataPointsCount.mul(eachDataPointValueByteSize + DATA_POINT_SYMBOL_BS) + + DATA_PACKAGE_WITHOUT_DATA_POINTS_AND_SIG_BS; //DATA_POINT_VALUE_BYTE_SIZE_BS + TIMESTAMP_BS + DATA_POINTS_COUNT_BS + + uint256 timestampCalldataOffset = msg.data.length.sub( + calldataNegativeOffset + TIMESTAMP_NEGATIVE_OFFSET_IN_DATA_PACKAGE_WITH_STANDARD_SLOT_BS); + + uint256 signedMessageCalldataOffset = msg.data.length.sub( + calldataNegativeOffset + SIG_BS + signedMessageBytesCount); + + assembly { + // Extracting the signed message + signedMessage := extractBytesFromCalldata( + signedMessageCalldataOffset, + signedMessageBytesCount + ) + + // Hashing the signed message + signedHash := keccak256(add(signedMessage, BYTES_ARR_LEN_VAR_BS), signedMessageBytesCount) + + // Extracting timestamp + extractedTimestamp := calldataload(timestampCalldataOffset) + + function initByteArray(bytesCount) -> ptr { + ptr := mload(FREE_MEMORY_PTR) + mstore(ptr, bytesCount) + ptr := add(ptr, BYTES_ARR_LEN_VAR_BS) + mstore(FREE_MEMORY_PTR, add(ptr, bytesCount)) + } + + function extractBytesFromCalldata(offset, bytesCount) -> extractedBytes { + let extractedBytesStartPtr := initByteArray(bytesCount) + calldatacopy( + extractedBytesStartPtr, + offset, + bytesCount + ) + extractedBytes := sub(extractedBytesStartPtr, BYTES_ARR_LEN_VAR_BS) + } + } + + // Validating timestamp + validateTimestamp(extractedTimestamp); + + // Verifying the off-chain signature against on-chain hashed data + signerAddress = SignatureLib.recoverSignerAddress( + signedHash, + calldataNegativeOffset + SIG_BS + ); + signerIndex = getAuthorisedSignerIndex(signerAddress); + } + + // Updating helpful arrays + { + bytes32 dataPointDataFeedId; + uint256 dataPointValue; + for (uint256 dataPointIndex = 0; dataPointIndex < dataPointsCount; dataPointIndex++) { + // Extracting data feed id and value for the current data point + (dataPointDataFeedId, dataPointValue) = _extractDataPointValueAndDataFeedId( + calldataNegativeOffset, + eachDataPointValueByteSize, + dataPointIndex + ); + + for ( + uint256 dataFeedIdIndex = 0; + dataFeedIdIndex < dataFeedIds.length; + dataFeedIdIndex++ + ) { + if (dataPointDataFeedId == dataFeedIds[dataFeedIdIndex]) { + uint256 bitmapSignersForDataFeedId = signersBitmapForDataFeedIds[dataFeedIdIndex]; + + if ( + !BitmapLib.getBitFromBitmap(bitmapSignersForDataFeedId, signerIndex) && /* current signer was not counted for current dataFeedId */ + uniqueSignerCountForDataFeedIds[dataFeedIdIndex] < getUniqueSignersThreshold() + ) { + // Increase unique signer counter + uniqueSignerCountForDataFeedIds[dataFeedIdIndex]++; + + // Add new value + valuesForDataFeeds[dataFeedIdIndex][ + uniqueSignerCountForDataFeedIds[dataFeedIdIndex] - 1 + ] = dataPointValue; + + // Update signers bitmap + signersBitmapForDataFeedIds[dataFeedIdIndex] = BitmapLib.setBitInBitmap( + bitmapSignersForDataFeedId, + signerIndex + ); + } + + // Breaking, as there couldn't be several indexes for the same feed ID + break; + } + } + } + } + + // Return total data package byte size + return + DATA_PACKAGE_WITHOUT_DATA_POINTS_BS + + (eachDataPointValueByteSize + DATA_POINT_SYMBOL_BS) * + dataPointsCount; + } + + /** + * @dev This is a private helpful function, which aggregates values from different + * authorised signers for the given arrays of values for each data feed + * + * @param valuesForDataFeeds 2-dimensional array, valuesForDataFeeds[i][j] contains + * j-th value for the i-th data feed + * @param uniqueSignerCountForDataFeedIds an array with the numbers of unique signers + * for each data feed + * + * @return An array of the aggregated values + */ + function _getAggregatedValues( + uint256[][] memory valuesForDataFeeds, + uint256[] memory uniqueSignerCountForDataFeedIds + ) private view returns (uint256[] memory) { + uint256[] memory aggregatedValues = new uint256[](valuesForDataFeeds.length); + uint256 uniqueSignersThreshold = getUniqueSignersThreshold(); + + for (uint256 dataFeedIndex = 0; dataFeedIndex < valuesForDataFeeds.length; dataFeedIndex++) { + if (uniqueSignerCountForDataFeedIds[dataFeedIndex] < uniqueSignersThreshold) { + revert InsufficientNumberOfUniqueSigners( + uniqueSignerCountForDataFeedIds[dataFeedIndex], + uniqueSignersThreshold); + } + uint256 aggregatedValueForDataFeedId = aggregateValues(valuesForDataFeeds[dataFeedIndex]); + aggregatedValues[dataFeedIndex] = aggregatedValueForDataFeedId; + } + + return aggregatedValues; + } +} diff --git a/src/evm-connector/core/RedstoneConsumerBytesBase.sol b/src/evm-connector/core/RedstoneConsumerBytesBase.sol new file mode 100644 index 0000000..f096021 --- /dev/null +++ b/src/evm-connector/core/RedstoneConsumerBytesBase.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +import "./RedstoneConsumerBase.sol"; + +/** + * @title The base contract for Redstone consumers' contracts that allows to + * securely calculate dynamic (array of bytes) redstone oracle values + * @author The Redstone Oracles team + * + * @dev This contract can extend other contracts to allow them + * securely fetch Redstone oracle data from tx calldata in a form of byte arrays. + * + * Note! If you want to use numeric values, use RedstoneConsumerNumericBase contract + * + * We wanted to reuse the core logic from the RedstoneConsumerBase contract, but it + * required few tricks, which are described below: + * + * 1. "Tricky" calldata pointers - we decided to use single uint256 values and store + * the calldata offset in the first 128 bits of those numbers, and the value byte size + * in the last 128 bits of the value. It allowed us to reuse a big part of core logic + * and even slightly optimised memory usage. To optimise gas costs, we left the burden + * of converting tricky calldata pointers to calldata bytes arrays on the consumer + * contracts developers. They can use a helpful `getCalldataBytesFromCalldataPointer` + * function for it + * + * 2. Returning memory pointers instead of actual values - we need to work with + * dynamic bytes arrays in this contract, but the core logic of RedstoneConsumerBase + * contract expects a uint256 number as a result of values aggregation. That's + * why we swtiched to returning a memory pointers instead of actual values. But this is + * more an implementation detail and should not affect end developers during the + * integration with the Redstone protocol + */ +abstract contract RedstoneConsumerBytesBase is RedstoneConsumerBase { + using SafeMath for uint256; + + uint256 constant BITS_COUNT_IN_16_BYTES = 128; + + /** + * @dev This function may be overridden by the child consumer contract. + * It should aggregate values from different signers into a bytes array + * By default, it checks if all the values are identical and returns the first one + * + * @param calldataPointersForValues An array of "tricky" calldata pointers to + * the values provided by different authorised signers. Each tricky calldata pointer + * is a uint256 number, first 128 bits of which represent calldata offset, and the + * last 128 bits - the byte length of the value + * + * @return Result of the aggregation in the form of a bytes array + */ + function aggregateByteValues(uint256[] memory calldataPointersForValues) + public + view + virtual + returns (bytes memory) + { + // Check if all byte arrays are identical + if (calldataPointersForValues.length <= 0) { + revert EmptyCalldataPointersArr(); + } + bytes calldata firstValue = getCalldataBytesFromCalldataPointer(calldataPointersForValues[0]); + bytes32 expectedHash = keccak256(firstValue); + + for (uint256 i = 1; i < calldataPointersForValues.length; i++) { + bytes calldata currentValue = getCalldataBytesFromCalldataPointer( + calldataPointersForValues[i] + ); + if (keccak256(currentValue) != expectedHash) { + revert EachSignerMustProvideTheSameValue(); + } + } + + return firstValue; + } + + /** + * @dev This function may be used to convert a "tricky" calldata pointer into a + * calldata bytes array. You may find it useful while overriding the + * `aggregateByteValues` function + * + * @param trickyCalldataPtr A "tricky" calldata pointer, 128 first bits of which + * represent the offset, and the last 128 bits - the byte length of the value + * + * @return bytesValueInCalldata The corresponding calldata bytes array + */ + function getCalldataBytesFromCalldataPointer(uint256 trickyCalldataPtr) + internal + pure + returns (bytes calldata bytesValueInCalldata) + { + uint256 calldataOffset = _getNumberFromFirst128Bits(trickyCalldataPtr); + uint256 valueByteSize = _getNumberFromLast128Bits(trickyCalldataPtr); + if (calldataOffset + valueByteSize > msg.data.length) { + revert InvalidCalldataPointer(); + } + + assembly { + bytesValueInCalldata.offset := calldataOffset + bytesValueInCalldata.length := valueByteSize + } + } + + /** + * @dev This function can be used in a consumer contract to securely extract an + * oracle value for a given data feed id. Security is achieved by + * signatures verification, timestamp validation, and aggregating bytes values + * from different authorised signers into a single bytes array. If any of the + * required conditions do not match, the function will revert. + * Note! This function expects that tx calldata contains redstone payload in the end + * Learn more about redstone payload here: https://github.com/redstone-finance/redstone-oracles-monorepo/tree/main/packages/evm-connector#readme + * @param dataFeedId bytes32 value that uniquely identifies the data feed + * @return Bytes array with the aggregated oracle value for the given data feed id + */ + function getOracleBytesValueFromTxMsg(bytes32 dataFeedId) internal view returns (bytes memory) { + bytes32[] memory dataFeedIds = new bytes32[](1); + dataFeedIds[0] = dataFeedId; + return getOracleBytesValuesFromTxMsg(dataFeedIds)[0]; + } + + /** + * @dev This function can be used in a consumer contract to securely extract several + * numeric oracle values for a given array of data feed ids. Security is achieved by + * signatures verification, timestamp validation, and aggregating values + * from different authorised signers into a single numeric value. If any of the + * required conditions do not match, the function will revert. + * Note! This function expects that tx calldata contains redstone payload in the end + * Learn more about redstone payload here: https://github.com/redstone-finance/redstone-oracles-monorepo/tree/main/packages/evm-connector#readme + * @param dataFeedIds An array of unique data feed identifiers + * @return arrayOfMemoryPointers TODO + */ + function getOracleBytesValuesFromTxMsg(bytes32[] memory dataFeedIds) + internal + view + returns (bytes[] memory arrayOfMemoryPointers) + { + // The `_securelyExtractOracleValuesFromTxMsg` function contains the main logic + // for the data extraction and validation + uint256[] memory arrayOfExtractedValues = _securelyExtractOracleValuesFromTxMsg(dataFeedIds); + assembly { + arrayOfMemoryPointers := arrayOfExtractedValues + } + } + + /** + * @dev This is a helpful function for the values aggregation + * Unlike in the RedstoneConsumerBase contract, you should not override + * this function. If you want to have a custom aggregation logic, you can + * override the `aggregateByteValues` instead + * + * Note! Unlike in the `RedstoneConsumerBase` this function returns a memory pointer + * to the aggregated bytes array value (instead the value itself) + * + * @param calldataPointersToValues An array of "tricky" calldata pointers to + * the values provided by different authorised signers. Each tricky calldata pointer + * is a uint256 number, first 128 bits of which represent calldata offset, and the + * last 128 bits - the byte length of the value + * + * @return pointerToResultBytesInMemory A memory pointer to the aggregated bytes array + */ + function aggregateValues(uint256[] memory calldataPointersToValues) + public + view + override + returns (uint256 pointerToResultBytesInMemory) + { + bytes memory aggregatedBytes = aggregateByteValues(calldataPointersToValues); + assembly { + pointerToResultBytesInMemory := aggregatedBytes + } + } + + /** + * @dev This function extracts details for a given data point and returns its dataFeedId, + * and a "tricky" calldata pointer for its value + * + * @param calldataNegativeOffsetForDataPackage Calldata offset for the requested data package + * @param dataPointValueByteSize Expected number of bytes for the requested data point value + * @param dataPointIndex Index of the requested data point + * + * @return dataPointDataFeedId a data feed identifier for the extracted data point + * @return dataPointValue a "tricky" calldata pointer for the extracted value + */ + function _extractDataPointValueAndDataFeedId( + uint256 calldataNegativeOffsetForDataPackage, + uint256 dataPointValueByteSize, + uint256 dataPointIndex + ) internal pure override returns (bytes32 dataPointDataFeedId, uint256 dataPointValue) { + uint256 negativeOffsetToDataPoints = calldataNegativeOffsetForDataPackage + DATA_PACKAGE_WITHOUT_DATA_POINTS_BS; + uint256 dataPointNegativeOffset = negativeOffsetToDataPoints + + (1 + dataPointIndex).mul(dataPointValueByteSize + DATA_POINT_SYMBOL_BS); + uint256 dataPointCalldataOffset = msg.data.length.sub(dataPointNegativeOffset); + assembly { + dataPointDataFeedId := calldataload(dataPointCalldataOffset) + dataPointValue := prepareTrickyCalldataPointer( + add(dataPointCalldataOffset, DATA_POINT_SYMBOL_BS), + dataPointValueByteSize + ) + + function prepareTrickyCalldataPointer(calldataOffsetArg, valueByteSize) -> calldataPtr { + calldataPtr := or(shl(BITS_COUNT_IN_16_BYTES, calldataOffsetArg), valueByteSize) + } + } + } + + /// @dev This is a helpful function for "tricky" calldata pointers + function _getNumberFromFirst128Bits(uint256 number) internal pure returns (uint256) { + return number >> 128; + } + + /// @dev This is a helpful function for "tricky" calldata pointers + function _getNumberFromLast128Bits(uint256 number) internal pure returns (uint256) { + return uint128(number); + } +} diff --git a/src/evm-connector/core/RedstoneConsumerNumericBase.sol b/src/evm-connector/core/RedstoneConsumerNumericBase.sol new file mode 100644 index 0000000..212fae5 --- /dev/null +++ b/src/evm-connector/core/RedstoneConsumerNumericBase.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "./RedstoneConsumerBase.sol"; + +/** + * @title The base contract for Redstone consumers' contracts that allows to + * securely calculate numeric redstone oracle values + * @author The Redstone Oracles team + * @dev This contract can extend other contracts to allow them + * securely fetch Redstone oracle data from transactions calldata + */ +abstract contract RedstoneConsumerNumericBase is RedstoneConsumerBase { + /** + * @dev This function can be used in a consumer contract to securely extract an + * oracle value for a given data feed id. Security is achieved by + * signatures verification, timestamp validation, and aggregating values + * from different authorised signers into a single numeric value. If any of the + * required conditions do not match, the function will revert. + * Note! This function expects that tx calldata contains redstone payload in the end + * Learn more about redstone payload here: https://github.com/redstone-finance/redstone-oracles-monorepo/tree/main/packages/evm-connector#readme + * @param dataFeedId bytes32 value that uniquely identifies the data feed + * @return Extracted and verified numeric oracle value for the given data feed id + */ + function getOracleNumericValueFromTxMsg(bytes32 dataFeedId) + internal + view + virtual + returns (uint256) + { + bytes32[] memory dataFeedIds = new bytes32[](1); + dataFeedIds[0] = dataFeedId; + return getOracleNumericValuesFromTxMsg(dataFeedIds)[0]; + } + + /** + * @dev This function can be used in a consumer contract to securely extract several + * numeric oracle values for a given array of data feed ids. Security is achieved by + * signatures verification, timestamp validation, and aggregating values + * from different authorised signers into a single numeric value. If any of the + * required conditions do not match, the function will revert. + * Note! This function expects that tx calldata contains redstone payload in the end + * Learn more about redstone payload here: https://github.com/redstone-finance/redstone-oracles-monorepo/tree/main/packages/evm-connector#readme + * @param dataFeedIds An array of unique data feed identifiers + * @return An array of the extracted and verified oracle values in the same order + * as they are requested in the dataFeedIds array + */ + function getOracleNumericValuesFromTxMsg(bytes32[] memory dataFeedIds) + internal + view + virtual + returns (uint256[] memory) + { + return _securelyExtractOracleValuesFromTxMsg(dataFeedIds); + } + + /** + * @dev This function works similarly to the `getOracleNumericValuesFromTxMsg` with the + * only difference that it allows to request oracle data for an array of data feeds + * that may contain duplicates + * + * @param dataFeedIdsWithDuplicates An array of data feed identifiers (duplicates are allowed) + * @return An array of the extracted and verified oracle values in the same order + * as they are requested in the dataFeedIdsWithDuplicates array + */ + function getOracleNumericValuesWithDuplicatesFromTxMsg(bytes32[] memory dataFeedIdsWithDuplicates) internal view returns (uint256[] memory) { + // Building an array without duplicates + bytes32[] memory dataFeedIdsWithoutDuplicates = new bytes32[](dataFeedIdsWithDuplicates.length); + bool alreadyIncluded; + uint256 uniqueDataFeedIdsCount = 0; + + for (uint256 indexWithDup = 0; indexWithDup < dataFeedIdsWithDuplicates.length; indexWithDup++) { + // Checking if current element is already included in `dataFeedIdsWithoutDuplicates` + alreadyIncluded = false; + for (uint256 indexWithoutDup = 0; indexWithoutDup < uniqueDataFeedIdsCount; indexWithoutDup++) { + if (dataFeedIdsWithoutDuplicates[indexWithoutDup] == dataFeedIdsWithDuplicates[indexWithDup]) { + alreadyIncluded = true; + break; + } + } + + // Adding if not included + if (!alreadyIncluded) { + dataFeedIdsWithoutDuplicates[uniqueDataFeedIdsCount] = dataFeedIdsWithDuplicates[indexWithDup]; + uniqueDataFeedIdsCount++; + } + } + + // Overriding dataFeedIdsWithoutDuplicates.length + // Equivalent to: dataFeedIdsWithoutDuplicates.length = uniqueDataFeedIdsCount; + assembly { + mstore(dataFeedIdsWithoutDuplicates, uniqueDataFeedIdsCount) + } + + // Requesting oracle values (without duplicates) + uint256[] memory valuesWithoutDuplicates = getOracleNumericValuesFromTxMsg(dataFeedIdsWithoutDuplicates); + + // Preparing result values array + uint256[] memory valuesWithDuplicates = new uint256[](dataFeedIdsWithDuplicates.length); + for (uint256 indexWithDup = 0; indexWithDup < dataFeedIdsWithDuplicates.length; indexWithDup++) { + for (uint256 indexWithoutDup = 0; indexWithoutDup < dataFeedIdsWithoutDuplicates.length; indexWithoutDup++) { + if (dataFeedIdsWithDuplicates[indexWithDup] == dataFeedIdsWithoutDuplicates[indexWithoutDup]) { + valuesWithDuplicates[indexWithDup] = valuesWithoutDuplicates[indexWithoutDup]; + break; + } + } + } + + return valuesWithDuplicates; + } +} diff --git a/src/evm-connector/core/RedstoneDefaultsLib.sol b/src/evm-connector/core/RedstoneDefaultsLib.sol new file mode 100644 index 0000000..3618ad3 --- /dev/null +++ b/src/evm-connector/core/RedstoneDefaultsLib.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.4; + +import "../libs/NumericArrayLib.sol"; + +/** + * @title Default implementations of virtual redstone consumer base functions + * @author The Redstone Oracles team + */ +library RedstoneDefaultsLib { + uint256 constant DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS = 3 minutes; + uint256 constant DEFAULT_MAX_DATA_TIMESTAMP_AHEAD_SECONDS = 1 minutes; + + error TimestampFromTooLongFuture(uint256 receivedTimestampSeconds, uint256 blockTimestamp); + error TimestampIsTooOld(uint256 receivedTimestampSeconds, uint256 blockTimestamp); + + function validateTimestamp(uint256 receivedTimestampMilliseconds) internal view { + // Getting data timestamp from future seems quite unlikely + // But we've already spent too much time with different cases + // Where block.timestamp was less than dataPackage.timestamp. + // Some blockchains may case this problem as well. + // That's why we add MAX_BLOCK_TIMESTAMP_DELAY + // and allow data "from future" but with a small delay + uint256 receivedTimestampSeconds = receivedTimestampMilliseconds / 1000; + + if (block.timestamp < receivedTimestampSeconds) { + if ((receivedTimestampSeconds - block.timestamp) > DEFAULT_MAX_DATA_TIMESTAMP_AHEAD_SECONDS) { + revert TimestampFromTooLongFuture(receivedTimestampSeconds, block.timestamp); + } + } else if ((block.timestamp - receivedTimestampSeconds) > DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS) { + revert TimestampIsTooOld(receivedTimestampSeconds, block.timestamp); + } + } + + function aggregateValues(uint256[] memory values) internal pure returns (uint256) { + return NumericArrayLib.pickMedian(values); + } +} diff --git a/src/evm-connector/libs/BitmapLib.sol b/src/evm-connector/libs/BitmapLib.sol new file mode 100644 index 0000000..27d783f --- /dev/null +++ b/src/evm-connector/libs/BitmapLib.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +library BitmapLib { + function setBitInBitmap(uint256 bitmap, uint256 bitIndex) internal pure returns (uint256) { + return bitmap | (1 << bitIndex); + } + + function getBitFromBitmap(uint256 bitmap, uint256 bitIndex) internal pure returns (bool) { + uint256 bitAtIndex = bitmap & (1 << bitIndex); + return bitAtIndex > 0; + } +} diff --git a/src/evm-connector/libs/NumbersLib.sol b/src/evm-connector/libs/NumbersLib.sol new file mode 100644 index 0000000..086a2ca --- /dev/null +++ b/src/evm-connector/libs/NumbersLib.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +library NumbersLib { + uint256 constant BITS_COUNT_IN_16_BYTES = 128; + + function getNumberFromFirst16Bytes(uint256 number) internal pure returns (uint256) { + return uint256(number >> BITS_COUNT_IN_16_BYTES); + } + + function getNumberFromLast16Bytes(uint256 number) internal pure returns (uint256) { + return uint256((number << BITS_COUNT_IN_16_BYTES) >> BITS_COUNT_IN_16_BYTES); + } +} diff --git a/src/evm-connector/libs/NumericArrayLib.sol b/src/evm-connector/libs/NumericArrayLib.sol new file mode 100644 index 0000000..25ccdf0 --- /dev/null +++ b/src/evm-connector/libs/NumericArrayLib.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +library NumericArrayLib { + // This function sort array in memory using bubble sort algorithm, + // which performs even better than quick sort for small arrays + + uint256 constant BYTES_ARR_LEN_VAR_BS = 32; + uint256 constant UINT256_VALUE_BS = 32; + + error CanNotPickMedianOfEmptyArray(); + + // This function modifies the array + function pickMedian(uint256[] memory arr) internal pure returns (uint256) { + if (arr.length == 0) { + revert CanNotPickMedianOfEmptyArray(); + } + sort(arr); + uint256 middleIndex = arr.length / 2; + if (arr.length % 2 == 0) { + uint256 sum = SafeMath.add(arr[middleIndex - 1], arr[middleIndex]); + return sum / 2; + } else { + return arr[middleIndex]; + } + } + + function sort(uint256[] memory arr) internal pure { + assembly { + let arrLength := mload(arr) + let valuesPtr := add(arr, BYTES_ARR_LEN_VAR_BS) + let endPtr := add(valuesPtr, mul(arrLength, UINT256_VALUE_BS)) + for { + let arrIPtr := valuesPtr + } lt(arrIPtr, endPtr) { + arrIPtr := add(arrIPtr, UINT256_VALUE_BS) // arrIPtr += 32 + } { + for { + let arrJPtr := valuesPtr + } lt(arrJPtr, arrIPtr) { + arrJPtr := add(arrJPtr, UINT256_VALUE_BS) // arrJPtr += 32 + } { + let arrI := mload(arrIPtr) + let arrJ := mload(arrJPtr) + if lt(arrI, arrJ) { + mstore(arrIPtr, arrJ) + mstore(arrJPtr, arrI) + } + } + } + } + } +} diff --git a/src/evm-connector/libs/SignatureLib.sol b/src/evm-connector/libs/SignatureLib.sol new file mode 100644 index 0000000..b2c6a35 --- /dev/null +++ b/src/evm-connector/libs/SignatureLib.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +library SignatureLib { + uint256 constant ECDSA_SIG_R_BS = 32; + uint256 constant ECDSA_SIG_S_BS = 32; + + function recoverSignerAddress(bytes32 signedHash, uint256 signatureCalldataNegativeOffset) + internal + pure + returns (address) + { + bytes32 r; + bytes32 s; + uint8 v; + assembly { + let signatureCalldataStartPos := sub(calldatasize(), signatureCalldataNegativeOffset) + r := calldataload(signatureCalldataStartPos) + signatureCalldataStartPos := add(signatureCalldataStartPos, ECDSA_SIG_R_BS) + s := calldataload(signatureCalldataStartPos) + signatureCalldataStartPos := add(signatureCalldataStartPos, ECDSA_SIG_S_BS) + v := byte(0, calldataload(signatureCalldataStartPos)) // last byte of the signature memory array + } + return ecrecover(signedHash, v, r, s); + } +} diff --git a/src/on-chain-relayer/contracts/core/IRedstoneAdapter.sol b/src/on-chain-relayer/contracts/core/IRedstoneAdapter.sol new file mode 100644 index 0000000..36f3575 --- /dev/null +++ b/src/on-chain-relayer/contracts/core/IRedstoneAdapter.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +/** + * @title Interface of RedStone adapter + * @author The Redstone Oracles team + */ +interface IRedstoneAdapter { + + /** + * @notice Updates values of all data feeds supported by the Adapter contract + * @dev This function requires an attached redstone payload to the transaction calldata. + * It also requires each data package to have exactly the same timestamp + * @param dataPackagesTimestamp Timestamp of each signed data package in the redstone payload + */ + function updateDataFeedsValues(uint256 dataPackagesTimestamp) external; + + + /** + * @notice Returns the latest properly reported value of the data feed + * @param dataFeedId The identifier of the requested data feed + * @return value The latest value of the given data feed + */ + function getValueForDataFeed(bytes32 dataFeedId) external view returns (uint256); + + /** + * @notice Returns the latest properly reported values for several data feeds + * @param requestedDataFeedIds The array of identifiers for the requested feeds + * @return values Values of the requested data feeds in the corresponding order + */ + function getValuesForDataFeeds(bytes32[] memory requestedDataFeedIds) external view returns (uint256[] memory); + + /** + * @notice Returns data timestamp from the latest update + * @dev It's virtual, because its implementation can sometimes be different + * (e.g. SinglePriceFeedAdapterWithClearing) + * @return lastDataTimestamp Timestamp of the latest reported data packages + */ + function getDataTimestampFromLatestUpdate() external view returns (uint256 lastDataTimestamp); + + /** + * @notice Returns block timestamp of the latest successful update + * @return blockTimestamp The block timestamp of the latest successful update + */ + function getBlockTimestampFromLatestUpdate() external view returns (uint256 blockTimestamp); + + + /** + * @notice Returns timestamps of the latest successful update + * @return dataTimestamp timestamp (usually in milliseconds) from the signed data packages + * @return blockTimestamp timestamp of the block when the update has happened + */ + function getTimestampsFromLatestUpdate() external view returns (uint128 dataTimestamp, uint128 blockTimestamp); + + /** + * @notice Returns identifiers of all data feeds supported by the Adapter contract + * @return An array of data feed identifiers + */ + function getDataFeedIds() external view returns (bytes32[] memory); + + /** + * @notice Returns the unique index of the given data feed + * @param dataFeedId The data feed identifier + * @return index The index of the data feed + */ + function getDataFeedIndex(bytes32 dataFeedId) external view returns (uint256); + + /** + * @notice Returns minimal required interval (usually in seconds) between subsequent updates + * @return interval The required interval between updates + */ + function getMinIntervalBetweenUpdates() external view returns (uint256); + + /** + * @notice Reverts if the proposed timestamp of data packages it too old or too new + * comparing to the block.timestamp. It also ensures that the proposed timestamp is newer + * Then the one from the previous update + * @param dataPackagesTimestamp The proposed timestamp (usually in milliseconds) + */ + function validateProposedDataPackagesTimestamp(uint256 dataPackagesTimestamp) external view; + + /** + * @notice Reverts if the updater is not authorised + * @dev This function should revert if msg.sender is not allowed to update data feed values + * @param updater The address of the proposed updater + */ + function requireAuthorisedUpdater(address updater) external view; +} diff --git a/src/on-chain-relayer/contracts/core/RedstoneAdapterBase.sol b/src/on-chain-relayer/contracts/core/RedstoneAdapterBase.sol new file mode 100644 index 0000000..4879e36 --- /dev/null +++ b/src/on-chain-relayer/contracts/core/RedstoneAdapterBase.sol @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {RedstoneConsumerNumericBase, RedstoneDefaultsLib} from "@redstone-finance/evm-connector/contracts/core/RedstoneConsumerNumericBase.sol"; +import {IRedstoneAdapter} from "./IRedstoneAdapter.sol"; + +/** + * @title Core logic of Redstone Adapter Contract + * @author The Redstone Oracles team + * @dev This contract is used to repeatedly push Redstone data to blockchain storage + * More details here: https://docs.redstone.finance/docs/smart-contract-devs/get-started/redstone-classic + * + * Key details about the contract: + * - Values for data feeds can be updated using the `updateDataFeedsValues` function + * - All data feeds must be updated within a single call, partial updates are not allowed + * - There is a configurable minimum interval between updates + * - Updaters can be restricted by overriding `requireAuthorisedUpdater` function + * - The contract is designed to force values validation, by default it prevents returning zero values + * - All data packages in redstone payload must have the same timestamp, + * equal to `dataPackagesTimestamp` argument of the `updateDataFeedsValues` function + * - Block timestamp abstraction - even though we call it blockTimestamp in many places, + * it's possible to have a custom logic here, e.g. use block number instead of a timestamp + */ +abstract contract RedstoneAdapterBase is RedstoneConsumerNumericBase, IRedstoneAdapter { + // We don't use storage variables to avoid potential problems with upgradable contracts + bytes32 internal constant LATEST_UPDATE_TIMESTAMPS_STORAGE_LOCATION = 0x3d01e4d77237ea0f771f1786da4d4ff757fcba6a92933aa53b1dcef2d6bd6fe2; // keccak256("RedStone.lastUpdateTimestamp"); + uint256 internal constant MIN_INTERVAL_BETWEEN_UPDATES = 3 seconds; + uint256 internal constant BITS_COUNT_IN_16_BYTES = 128; + uint256 internal constant MAX_NUMBER_FOR_128_BITS = 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff; + + error DataTimestampShouldBeNewerThanBefore( + uint256 receivedDataTimestampMilliseconds, + uint256 lastDataTimestampMilliseconds + ); + + error MinIntervalBetweenUpdatesHasNotPassedYet( + uint256 currentBlockTimestamp, + uint256 lastUpdateTimestamp, + uint256 minIntervalBetweenUpdates + ); + + error DataPackageTimestampMismatch(uint256 expectedDataTimestamp, uint256 dataPackageTimestamp); + + error DataFeedValueCannotBeZero(bytes32 dataFeedId); + + error DataFeedIdNotFound(bytes32 dataFeedId); + + error DataTimestampIsTooBig(uint256 dataTimestamp); + + error BlockTimestampIsTooBig(uint256 blockTimestamp); + + /** + * @notice Reverts if the updater is not authorised + * @dev This function should revert if msg.sender is not allowed to update data feed values + * @param updater The address of the proposed updater + */ + function requireAuthorisedUpdater(address updater) public view virtual { + // By default, anyone can update data feed values, but it can be overridden + } + + /** + * @notice Returns identifiers of all data feeds supported by the Adapter contract + * @dev this function must be implemented in derived contracts + * @return An array of data feed identifiers + */ + function getDataFeedIds() public view virtual returns (bytes32[] memory); + + /** + * @notice Returns the unique index of the given data feed + * @dev This function can (and should) be overriden to reduce gas + * costs of other functions + * @param dataFeedId The data feed identifier + * @return index The index of the data feed + */ + function getDataFeedIndex(bytes32 dataFeedId) public view virtual returns (uint256) { + bytes32[] memory dataFeedIds = getDataFeedIds(); + for (uint256 i = 0; i < dataFeedIds.length;) { + if (dataFeedIds[i] == dataFeedId) { + return i; + } + unchecked { i++; } // reduces gas costs + } + revert DataFeedIdNotFound(dataFeedId); + } + + /** + * @notice Updates values of all data feeds supported by the Adapter contract + * @dev This function requires an attached redstone payload to the transaction calldata. + * It also requires each data package to have exactly the same timestamp + * @param dataPackagesTimestamp Timestamp of each signed data package in the redstone payload + */ + function updateDataFeedsValues(uint256 dataPackagesTimestamp) public virtual { + requireAuthorisedUpdater(msg.sender); + _assertMinIntervalBetweenUpdatesPassed(); + validateProposedDataPackagesTimestamp(dataPackagesTimestamp); + _saveTimestampsOfCurrentUpdate(dataPackagesTimestamp); + + bytes32[] memory dataFeedsIdsArray = getDataFeedIds(); + + // It will trigger timestamp validation for each data package + uint256[] memory oracleValues = getOracleNumericValuesFromTxMsg(dataFeedsIdsArray); + + _validateAndUpdateDataFeedsValues(dataFeedsIdsArray, oracleValues); + } + + /** + * @dev Note! This function is not called directly, it's called for each data package . + * in redstone payload and just verifies if each data package has the same timestamp + * as the one that was saved in the storage + * @param receivedTimestampMilliseconds Timestamp from a data package + */ + function validateTimestamp(uint256 receivedTimestampMilliseconds) public view virtual override { + // It means that we are in the special view context and we can skip validation of the + // timestamp. It can be useful for calling view functions, as they can not modify the contract + // state to pass the timestamp validation below + if (msg.sender == address(0)) { + return; + } + + uint256 expectedDataPackageTimestamp = getDataTimestampFromLatestUpdate(); + if (receivedTimestampMilliseconds != expectedDataPackageTimestamp) { + revert DataPackageTimestampMismatch( + expectedDataPackageTimestamp, + receivedTimestampMilliseconds + ); + } + } + + /** + * @dev This function should be implemented by the actual contract + * and should contain the logic of values validation and reporting. + * Usually, values reporting is based on saving them to the contract storage, + * e.g. in PriceFeedsAdapter, but some custom implementations (e.g. GMX keeper adapter + * or Mento Sorted Oracles adapter) may handle values updating in a different way + * @param dataFeedIdsArray Array of the data feeds identifiers (it will always be all data feed ids) + * @param values The reported values that should be validated and reported + */ + function _validateAndUpdateDataFeedsValues(bytes32[] memory dataFeedIdsArray, uint256[] memory values) internal virtual; + + /** + * @dev This function reverts if not enough time passed since the latest update + */ + function _assertMinIntervalBetweenUpdatesPassed() private view { + uint256 currentBlockTimestamp = getBlockTimestamp(); + uint256 blockTimestampFromLatestUpdate = getBlockTimestampFromLatestUpdate(); + uint256 minIntervalBetweenUpdates = getMinIntervalBetweenUpdates(); + if (currentBlockTimestamp < blockTimestampFromLatestUpdate + minIntervalBetweenUpdates) { + revert MinIntervalBetweenUpdatesHasNotPassedYet( + currentBlockTimestamp, + blockTimestampFromLatestUpdate, + minIntervalBetweenUpdates + ); + } + } + + /** + * @notice Returns minimal required interval (usually in seconds) between subsequent updates + * @dev You can override this function to change the required interval between udpates. + * Please do not set it to 0, as it may open many attack vectors + * @return interval The required interval between updates + */ + function getMinIntervalBetweenUpdates() public view virtual returns (uint256) { + return MIN_INTERVAL_BETWEEN_UPDATES; + } + + /** + * @notice Reverts if the proposed timestamp of data packages it too old or too new + * comparing to the block.timestamp. It also ensures that the proposed timestamp is newer + * Then the one from the previous update + * @param dataPackagesTimestamp The proposed timestamp (usually in milliseconds) + */ + function validateProposedDataPackagesTimestamp(uint256 dataPackagesTimestamp) public view { + _preventUpdateWithOlderDataPackages(dataPackagesTimestamp); + validateDataPackagesTimestampOnce(dataPackagesTimestamp); + } + + + /** + * @notice Reverts if the proposed timestamp of data packages it too old or too new + * comparing to the current block timestamp + * @param dataPackagesTimestamp The proposed timestamp (usually in milliseconds) + */ + function validateDataPackagesTimestampOnce(uint256 dataPackagesTimestamp) public view virtual { + uint256 receivedTimestampSeconds = dataPackagesTimestamp / 1000; + + (uint256 maxDataAheadSeconds, uint256 maxDataDelaySeconds) = getAllowedTimestampDiffsInSeconds(); + + uint256 blockTimestamp = getBlockTimestamp(); + + if (blockTimestamp < receivedTimestampSeconds) { + if ((receivedTimestampSeconds - blockTimestamp) > maxDataAheadSeconds) { + revert RedstoneDefaultsLib.TimestampFromTooLongFuture(receivedTimestampSeconds, blockTimestamp); + } + } else if ((blockTimestamp - receivedTimestampSeconds) > maxDataDelaySeconds) { + revert RedstoneDefaultsLib.TimestampIsTooOld(receivedTimestampSeconds, blockTimestamp); + } + } + + /** + * @dev This function can be overriden, e.g. to use block.number instead of block.timestamp + * It can be useful in some L2 chains, as sometimes their different blocks can have the same timestamp + * @return timestamp Timestamp or Block number or any other number that can identify time in the context + * of the given blockchain + */ + function getBlockTimestamp() public view virtual returns (uint256) { + return block.timestamp; + } + + /** + * @dev Helpful function for getting values for timestamp validation + * @return maxDataAheadSeconds Max allowed number of seconds ahead of block.timrstamp + * @return maxDataDelaySeconds Max allowed number of seconds for data delay + */ + function getAllowedTimestampDiffsInSeconds() public view virtual returns (uint256 maxDataAheadSeconds, uint256 maxDataDelaySeconds) { + maxDataAheadSeconds = RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_AHEAD_SECONDS; + maxDataDelaySeconds = RedstoneDefaultsLib.DEFAULT_MAX_DATA_TIMESTAMP_DELAY_SECONDS; + } + + /** + * @dev Reverts if proposed data packages are not newer than the ones used previously + * @param dataPackagesTimestamp Timestamp od the data packages (usually in milliseconds) + */ + function _preventUpdateWithOlderDataPackages(uint256 dataPackagesTimestamp) internal view { + uint256 dataTimestampFromLatestUpdate = getDataTimestampFromLatestUpdate(); + + if (dataPackagesTimestamp <= dataTimestampFromLatestUpdate) { + revert DataTimestampShouldBeNewerThanBefore( + dataPackagesTimestamp, + dataTimestampFromLatestUpdate + ); + } + } + + /** + * @notice Returns data timestamp from the latest update + * @dev It's virtual, because its implementation can sometimes be different + * (e.g. SinglePriceFeedAdapterWithClearing) + * @return lastDataTimestamp Timestamp of the latest reported data packages + */ + function getDataTimestampFromLatestUpdate() public view virtual returns (uint256 lastDataTimestamp) { + (lastDataTimestamp, ) = getTimestampsFromLatestUpdate(); + } + + /** + * @notice Returns block timestamp of the latest successful update + * @return blockTimestamp The block timestamp of the latest successful update + */ + function getBlockTimestampFromLatestUpdate() public view returns (uint256 blockTimestamp) { + (, blockTimestamp) = getTimestampsFromLatestUpdate(); + } + + /** + * @dev Returns 2 timestamps packed into a single uint256 number + * @return packedTimestamps a single uin256 number with 2 timestamps + */ + function getPackedTimestampsFromLatestUpdate() public view returns (uint256 packedTimestamps) { + assembly { + packedTimestamps := sload(LATEST_UPDATE_TIMESTAMPS_STORAGE_LOCATION) + } + } + + /** + * @notice Returns timestamps of the latest successful update + * @return dataTimestamp timestamp (usually in milliseconds) from the signed data packages + * @return blockTimestamp timestamp of the block when the update has happened + */ + function getTimestampsFromLatestUpdate() public view virtual returns (uint128 dataTimestamp, uint128 blockTimestamp) { + return _unpackTimestamps(getPackedTimestampsFromLatestUpdate()); + } + + + /** + * @dev A helpful function to unpack 2 timestamps from one uin256 number + * @param packedTimestamps a single uin256 number + * @return dataTimestamp fetched from left 128 bits + * @return blockTimestamp fetched from right 128 bits + */ + function _unpackTimestamps(uint256 packedTimestamps) internal pure returns (uint128 dataTimestamp, uint128 blockTimestamp) { + dataTimestamp = uint128(packedTimestamps >> 128); // left 128 bits + blockTimestamp = uint128(packedTimestamps); // right 128 bits + } + + + /** + * @dev Logic of saving timestamps of the current update + * By default, it stores packed timestamps in one storage slot (32 bytes) + * to minimise gas costs + * But it can be overriden (e.g. in SinglePriceFeedAdapter) + * @param dataPackagesTimestamp . + */ + function _saveTimestampsOfCurrentUpdate(uint256 dataPackagesTimestamp) internal virtual { + uint256 blockTimestamp = getBlockTimestamp(); + + if (blockTimestamp > MAX_NUMBER_FOR_128_BITS) { + revert BlockTimestampIsTooBig(blockTimestamp); + } + + if (dataPackagesTimestamp > MAX_NUMBER_FOR_128_BITS) { + revert DataTimestampIsTooBig(dataPackagesTimestamp); + } + + assembly { + let timestamps := or(shl(BITS_COUNT_IN_16_BYTES, dataPackagesTimestamp), blockTimestamp) + sstore(LATEST_UPDATE_TIMESTAMPS_STORAGE_LOCATION, timestamps) + } + } + + /** + * @notice Returns the latest properly reported value of the data feed + * @param dataFeedId The identifier of the requested data feed + * @return value The latest value of the given data feed + */ + function getValueForDataFeed(bytes32 dataFeedId) public view returns (uint256) { + getDataFeedIndex(dataFeedId); // will revert if data feed id is not supported + + // "unsafe" here means "without validation" + uint256 valueForDataFeed = getValueForDataFeedUnsafe(dataFeedId); + + validateDataFeedValueOnRead(dataFeedId, valueForDataFeed); + return valueForDataFeed; + } + + /** + * @notice Returns the latest properly reported values for several data feeds + * @param dataFeedIds The array of identifiers for the requested feeds + * @return values Values of the requested data feeds in the corresponding order + */ + function getValuesForDataFeeds(bytes32[] memory dataFeedIds) public view returns (uint256[] memory) { + uint256[] memory values = getValuesForDataFeedsUnsafe(dataFeedIds); + for (uint256 i = 0; i < dataFeedIds.length;) { + bytes32 dataFeedId = dataFeedIds[i]; + getDataFeedIndex(dataFeedId); // will revert if data feed id is not supported + validateDataFeedValueOnRead(dataFeedId, values[i]); + unchecked { i++; } // reduces gas costs + } + return values; + } + + + + /** + * @dev Reverts if proposed value for the proposed data feed id is invalid + * Is called on every NOT *unsafe method which reads dataFeed + * By default, it just checks if the value is not equal to 0, but it can be extended + * @param dataFeedId The data feed identifier + * @param valueForDataFeed Proposed value for the data feed + */ + function validateDataFeedValueOnRead(bytes32 dataFeedId, uint256 valueForDataFeed) public view virtual { + if (valueForDataFeed == 0) { + revert DataFeedValueCannotBeZero(dataFeedId); + } + } + + /** + * @dev Reverts if proposed value for the proposed data feed id is invalid + * Is called on every NOT *unsafe method which writes dataFeed + * By default, it does nothing + * @param dataFeedId The data feed identifier + * @param valueForDataFeed Proposed value for the data feed + */ + function validateDataFeedValueOnWrite(bytes32 dataFeedId, uint256 valueForDataFeed) public view virtual { + if (valueForDataFeed == 0) { + revert DataFeedValueCannotBeZero(dataFeedId); + } + } + + /** + * @dev [HIGH RISK] Returns the latest value for a given data feed without validation + * Important! Using this function instead of `getValueForDataFeed` may cause + * significant risk for your smart contracts + * @param dataFeedId The data feed identifier + * @return dataFeedValue Unvalidated value of the latest successful update + */ + function getValueForDataFeedUnsafe(bytes32 dataFeedId) public view virtual returns (uint256); + + /** + * @notice [HIGH RISK] Returns the latest properly reported values for several data feeds without validation + * Important! Using this function instead of `getValuesForDataFeeds` may cause + * significant risk for your smart contracts + * @param requestedDataFeedIds The array of identifiers for the requested feeds + * @return values Unvalidated values of the requested data feeds in the corresponding order + */ + function getValuesForDataFeedsUnsafe(bytes32[] memory requestedDataFeedIds) public view virtual returns (uint256[] memory values) { + values = new uint256[](requestedDataFeedIds.length); + for (uint256 i = 0; i < requestedDataFeedIds.length;) { + values[i] = getValueForDataFeedUnsafe(requestedDataFeedIds[i]); + unchecked { i++; } // reduces gas costs + } + return values; + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/LowestGasRead.sol b/src/on-chain-relayer/contracts/custom-integrations/LowestGasRead.sol new file mode 100644 index 0000000..f2bd029 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/LowestGasRead.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +abstract contract LowestGasRead { + /** + * @dev [HIGH RISK] Returns the latest value for a given data feed without validation + * Important! Using this function instead of `getValueForDataFeed` may cause + * significant risk for your smart contracts + * @return dataFeedValue Unvalidated value of the latest successful update + */ + function getBtcValueWithLowestGas() external view returns (uint256 dataFeedValue) { + assembly { + // THIS VALUE HAS TO BE PRE COMPUTED // precompute keccak256(abi.encode("BTC", 0x4dd0c77efa6f6d590c97573d8c70b714546e7311202ff7c11c484cc841d91bfc)) + dataFeedValue := sload(0xf497211eccb68cc78a757a9caed87152a70e6da38b5f59e20a3feb628cda40b8) + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/ethena-balancer-ratio-provider/MergedAdapterWithoutRoundsSusdeRateProviderBase.sol b/src/on-chain-relayer/contracts/custom-integrations/ethena-balancer-ratio-provider/MergedAdapterWithoutRoundsSusdeRateProviderBase.sol new file mode 100644 index 0000000..1342822 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/ethena-balancer-ratio-provider/MergedAdapterWithoutRoundsSusdeRateProviderBase.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {MergedPriceFeedAdapterWithoutRoundsPrimaryProd} from "../../price-feeds/data-services/MergedPriceFeedAdapterWithoutRoundsPrimaryProd.sol"; +import {DeviationLib} from "../../libs/DeviationLib.sol"; + +contract MergedAdapterWithoutRoundsSusdeRateProviderBase is + MergedPriceFeedAdapterWithoutRoundsPrimaryProd +{ + uint256 private constant MIN_UPDATE_INTERVAL = 12 hours; + uint256 private constant PRECISION = 10000; + uint256 private constant MAX_ALLOWED_DEVIATION_PERCENT = 2 * PRECISION; + + error UpdaterNotAuthorised(address signer); + error ProposedValueIsDeviatedTooMuch( + uint256 latestValue, + uint256 proposedNewValue, + uint256 deviation, + uint256 maxAllowedDeviationPercent + ); + + function decimals() public pure override returns (uint8) { + return 18; + } + + function getDataFeedId() public pure virtual override returns (bytes32) { + return bytes32("sUSDe_RATE_PROVIDER"); + } + + function getRate() public view virtual returns (uint256) { + return SafeCast.toUint256(latestAnswer()); + } + + function description() public view virtual override returns (string memory) { + return "RedStone Ethena Balancer Rate Provider sUSDe Price Feed"; + } + + function validateDataFeedValueOnWrite(bytes32 dataFeedId, uint256 valueForDataFeed) public view virtual override { + if (valueForDataFeed == 0) { + revert DataFeedValueCannotBeZero(dataFeedId); + } + _validateDeviationFromLatestAnswer(valueForDataFeed, dataFeedId); + } + + function getMinIntervalBetweenUpdates() + public + view + virtual + override + returns (uint256) + { + return MIN_UPDATE_INTERVAL; + } + + function _validateDeviationFromLatestAnswer( + uint256 proposedValue, + bytes32 dataFeedId + ) internal view virtual { + uint256 latestAnswer = getValueForDataFeedUnsafe(dataFeedId); + if (latestAnswer != 0) { + uint256 deviation = DeviationLib.calculateAbsDeviation( + proposedValue, + latestAnswer, + PRECISION + ); + if (deviation > MAX_ALLOWED_DEVIATION_PERCENT) { + revert ProposedValueIsDeviatedTooMuch( + latestAnswer, + proposedValue, + deviation, + MAX_ALLOWED_DEVIATION_PERCENT + ); + } + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/fluid/EthUsdcRedstoneAdapterForFluidOracle.sol b/src/on-chain-relayer/contracts/custom-integrations/fluid/EthUsdcRedstoneAdapterForFluidOracle.sol new file mode 100644 index 0000000..7290cd1 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/fluid/EthUsdcRedstoneAdapterForFluidOracle.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {IFluidOracle} from "./IFluidOracle.sol"; +import {PriceFeedsAdapterWithoutRounds} from "../../price-feeds/without-rounds/PriceFeedsAdapterWithoutRounds.sol"; + +abstract contract EthUsdcRedstoneAdapterForFluidOracle is + IFluidOracle, + PriceFeedsAdapterWithoutRounds +{ + // precompute keccak256(abi.encode("ETH/USDC", 0x4dd0c77efa6f6d590c97573d8c70b714546e7311202ff7c11c484cc841d91bfc)) + bytes32 constant PRICE_LOCATION_IN_STORAGE = + 0x02967e833d2ce9c403dca2db59409302fd3c621b131bafcc7adc11d77518462c; + + bytes32 constant private ETH_USDC_ID = bytes32("ETH/USDC"); + + error UpdaterNotAuthorised(address signer); + + function getDataFeedIds() public pure override returns (bytes32[] memory dataFeedIds) { + dataFeedIds = new bytes32[](1); + dataFeedIds[0] = ETH_USDC_ID; + } + + function getDataFeedIndex(bytes32 dataFeedId) public view override virtual returns (uint256) { + if (dataFeedId == ETH_USDC_ID) { return 0; } + revert DataFeedIdNotFound(dataFeedId); + } + + function getExchangeRate() + external + view + override + returns (uint256 exchangeRate_) + { + assembly { + exchangeRate_ := sload(PRICE_LOCATION_IN_STORAGE) + } + } + +} \ No newline at end of file diff --git a/src/on-chain-relayer/contracts/custom-integrations/fluid/FluidRedstoneAdapterReader.sol b/src/on-chain-relayer/contracts/custom-integrations/fluid/FluidRedstoneAdapterReader.sol new file mode 100644 index 0000000..00280a1 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/fluid/FluidRedstoneAdapterReader.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {IFluidOracle} from "./IFluidOracle.sol"; + +/** + * This contract ilustrates the cheapest possible way to read from FluidOracleRedstoneAdapter contract + * Gas reduced in comparison to FluidOracleRedstoneAdapter(address).readExchangeRate - less ~2230 gas (total cost = ~5321 (with call included)) + */ +contract FluidRedstoneAdapterReader { + bytes4 constant READ_EXCHANGE_RATE_SELECTOR = 0xe6aa216c; + + function getRedstoneExchangeRate(address oracleAddress) public virtual returns(uint256 exchangeRate) { + assembly { + let freeSlot := mload(0x40) + + mstore(freeSlot, READ_EXCHANGE_RATE_SELECTOR) + + let success := staticcall( + 5000, // estimated gas cost for this function + oracleAddress, + freeSlot, + 0x04, + freeSlot, + 0x20 + ) + + switch success + case 0 { + revert(freeSlot, 0x40) + } + default { + exchangeRate := mload(freeSlot) + } + + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/fluid/IFluidOracle.sol b/src/on-chain-relayer/contracts/custom-integrations/fluid/IFluidOracle.sol new file mode 100644 index 0000000..b07e503 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/fluid/IFluidOracle.sol @@ -0,0 +1,15 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.14; + +interface IFluidOracle { + /// @notice Get the exchange rate between the underlying asset and the peg asset + /// @return exchangeRate_ The exchange rate, scaled by 1e18 + function getExchangeRate() external view returns (uint256 exchangeRate_); +} + +/// @title FluidOracle +/// @notice Base contract that any Oracle must implement +abstract contract FluidOracle is IFluidOracle { + /// @inheritdoc IFluidOracle + function getExchangeRate() external view virtual returns (uint256 exchangeRate_); +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/layerbank/ILToken.sol b/src/on-chain-relayer/contracts/custom-integrations/layerbank/ILToken.sol new file mode 100644 index 0000000..19d0e57 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/layerbank/ILToken.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +interface ILToken { + function underlying() external view returns (address); +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/layerbank/IPriceCalculator.sol b/src/on-chain-relayer/contracts/custom-integrations/layerbank/IPriceCalculator.sol new file mode 100644 index 0000000..866860b --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/layerbank/IPriceCalculator.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.14; + +interface IPriceCalculator { + struct ReferenceData { + uint256 lastData; + uint256 lastUpdated; + } + + function priceOf(address asset) external view returns (uint256); + + function pricesOf( + address[] memory assets + ) external view returns (uint256[] memory); + + function priceOfETH() external view returns (uint256); + + function getUnderlyingPrice(address gToken) external view returns (uint256); + + function getUnderlyingPrices( + address[] memory gTokens + ) external view returns (uint256[] memory); +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterBase.sol b/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterBase.sol new file mode 100644 index 0000000..2bbe9e9 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterBase.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {ILToken} from "./ILToken.sol"; +import {IPriceCalculator} from "./IPriceCalculator.sol"; +import {PriceFeedsAdapterWithRoundsPrimaryProd} from "../../price-feeds/data-services/PriceFeedsAdapterWithRoundsPrimaryProd.sol"; + +abstract contract LayerBankOracleAdapterBase is PriceFeedsAdapterWithRoundsPrimaryProd, IPriceCalculator { + error UnsupportedAsset(address asset); + error DataIsStale(uint256 lastUpdateTime); + + /** + * @dev Maps gToken address to dataFeedId + * @param asset asset address + * @return dataFeedId + */ + function getDataFeedIdForAsset(address asset) public view virtual returns(bytes32); + + /** + * @dev by default redstone value feeds returns values fromatted as 8 decimals + * This function can be used to override this default per dataFeedId. + * @param dataFeedId data feed id + * @param valueFromRedstonePayload value for data feed id by default 8 decimals + * @return uint256 scaled value from redstone payload + */ + function convertDecimals(bytes32 dataFeedId, uint256 valueFromRedstonePayload) public view virtual returns (uint256); + + /** + * @dev asserts that value is not stale + */ + function requireNonStaleData() public view virtual; + + /** + * @param gToken gToken address + * @return address underlying asset address + */ + function getUnderlyingAsset(address gToken) public view virtual returns(address) { + return ILToken(gToken).underlying(); + } + + /** + * @dev [IMPORTANT] This function does not assert that value is not stale! + * @param asset address of asset + * @return uint256 value decimals are scaled according to convertDecimals function + */ + function _uncheckedPriceOf(address asset) internal view virtual returns (uint256) { + bytes32 dataFeedId = getDataFeedIdForAsset(asset); + uint256 valueFromRedstonePayload = getValueForDataFeed(dataFeedId); + return convertDecimals(dataFeedId, valueFromRedstonePayload); + } + + /** + * @dev return value of single asset + * @param asset asset address + * @return uint256 value decimals are scaled according to convertDecimals function + */ + function priceOf(address asset) public view virtual returns (uint256) { + requireNonStaleData(); + return _uncheckedPriceOf(asset); + } + + /** + * @dev return values for many assets + * @param assets list of assets addresses + * @return values values are scaled according to convertDecimals function + */ + function pricesOf( + address[] memory assets + ) external view returns (uint256[] memory values) { + requireNonStaleData(); + values = new uint256[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + values[i] = _uncheckedPriceOf(assets[i]); + } + } + + /** + * @dev return value of gToken underyling asset + * @param gToken address of gToken contract + * @return uint256 value is scaled according to convertDecimals function + */ + function getUnderlyingPrice(address gToken) public view returns (uint256) { + return priceOf(getUnderlyingAsset(gToken)); + } + + /** + * @dev return values of gTokens underyling assets + * @param gTokens addresses of gToken contracts + * @return values values are scaled according to convertDecimals function + */ + function getUnderlyingPrices( + address[] memory gTokens + ) public view returns (uint256[] memory values) { + values = new uint256[](gTokens.length); + for (uint256 i = 0; i < gTokens.length; i++) { + values[i] = getUnderlyingPrice(gTokens[i]); + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterV1.sol b/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterV1.sol new file mode 100644 index 0000000..ddc77ef --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/layerbank/LayerBankOracleAdapterV1.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {LayerBankOracleAdapterBase} from "./LayerBankOracleAdapterBase.sol"; + +contract LayerBankOracleAdapterV1 is LayerBankOracleAdapterBase { + + uint256 internal constant MAX_ALLOWED_DATA_STALENESS = 10 hours; + uint256 internal constant DEFAULT_DECIMAL_SCALER = 1e10; + + bytes32 internal constant ETH_ID = bytes32("ETH"); + address internal constant ETH_ASSET = address(0); + + bytes32 internal constant USDC_ID = bytes32("USDC"); + address internal constant USDC_ASSET = 0xb73603C5d87fA094B7314C74ACE2e64D165016fb; + + bytes32 internal constant TIA_ID = bytes32("TIA"); + address internal constant TIA_ASSET = 0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa; + + bytes32 internal constant LAB_M_ID = bytes32("LAB.m"); + address internal constant LAB_M_ASSET = 0x20A512dbdC0D006f46E6cA11329034Eb3d18c997; + + bytes32 internal constant WST_ETH_ID = bytes32("wstETH"); + address internal constant WST_ETH_ASSET = 0x2FE3AD97a60EB7c79A976FC18Bb5fFD07Dd94BA5; + + bytes32 internal constant STONE_ID = bytes32("STONE"); + address internal constant STONE_ASSET = 0xEc901DA9c68E90798BbBb74c11406A32A70652C3; + + bytes32 internal constant W_USDM_ID = bytes32("wUSDM"); + address internal constant W_USDM_ASSET = 0xbdAd407F77f44F7Da6684B416b1951ECa461FB07; + + bytes32 internal constant MANTA_ID = bytes32("MANTA"); + address internal constant MANTA_ASSET = 0x95CeF13441Be50d20cA4558CC0a27B601aC544E5; + + function getDataFeedIdForAsset(address asset) public view virtual override returns(bytes32) { + if (asset == USDC_ASSET) { + return USDC_ID; + } else if (asset == TIA_ASSET) { + return TIA_ID; + } else if (asset == LAB_M_ASSET) { + return LAB_M_ID; + } else if (asset == WST_ETH_ASSET) { + return WST_ETH_ID; + } else if (asset == STONE_ASSET) { + return STONE_ID; + } else if (asset == W_USDM_ASSET) { + return W_USDM_ID; + } else if (asset == MANTA_ASSET) { + return MANTA_ID; + } else if (asset == ETH_ASSET) { + return ETH_ID; + } else { + revert UnsupportedAsset(asset); + } + } + + function getDataFeedIds() public view virtual override returns (bytes32[] memory dataFeedIds) { + dataFeedIds = new bytes32[](8); + dataFeedIds[0] = ETH_ID; + dataFeedIds[1] = USDC_ID; + dataFeedIds[2] = TIA_ID; + dataFeedIds[3] = LAB_M_ID; + dataFeedIds[4] = WST_ETH_ID; + dataFeedIds[5] = STONE_ID; + dataFeedIds[6] = W_USDM_ID; + dataFeedIds[7] = MANTA_ID; + } + + function convertDecimals(bytes32 dataFeedId, uint256 valueFromRedstonePayload) public view virtual override returns(uint256) { + dataFeedId; // Currently, this arg is unused, but it be required for new tokens + return valueFromRedstonePayload * DEFAULT_DECIMAL_SCALER; + } + + function priceOfETH() public view returns (uint256) { + return priceOf(ETH_ASSET); + } + + function requireNonStaleData() public view virtual override { + uint256 latestUpdateTime = getBlockTimestampFromLatestUpdate(); + uint256 curTime = getBlockTimestamp(); + if (latestUpdateTime < curTime && (curTime - latestUpdateTime) > MAX_ALLOWED_DATA_STALENESS) { + revert DataIsStale(latestUpdateTime); + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/ISortedOracles.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/ISortedOracles.sol new file mode 100644 index 0000000..4442aff --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/ISortedOracles.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import {SortedLinkedListWithMedian} from "./linkedlists/SortedLinkedListWithMedian.sol"; + +/** + * @title Simplified interface of the SortedOracles contract + * @author The Mento team (modified by the Redstone team) + * @dev Some functions were removed to simplify implementation + * of the mock SortedOracles contract. Interfaces of the functions + * below are identical with the original ISortedOracles interface + */ +interface ISortedOracles { + function report(address, uint256, address, address) external; + + function removeExpiredReports(address, uint256) external; + + function getRates( + address + ) + external + view + returns ( + address[] memory, + uint256[] memory, + SortedLinkedListWithMedian.MedianRelation[] memory + ); + + function numTimestamps(address) external view returns (uint256); + + function medianRate(address) external view returns (uint256, uint256); +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/MentoAdapterBase.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/MentoAdapterBase.sol new file mode 100644 index 0000000..aba3362 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/MentoAdapterBase.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ISortedOracles} from "./ISortedOracles.sol"; +import {RedstoneAdapterBase} from "../../core/RedstoneAdapterBase.sol"; + +/** + * @title Redstone oracles adapter for the Mento SortedOracles contract + * @author The Redstone Oracles team + * @dev This contract should be whitelisted as an oracle client in the + * SortedOracles contract. It allows anyone to push signed oracle data + * to report them in the Mento SortedOracles contract. It is ownable, + * the owner can manage delivered data feeds and corresponding token + * addresses. + * + */ +abstract contract MentoAdapterBase is RedstoneAdapterBase, Initializable { + error TokenNotFoundForIndex(uint256 tokenIndex); + + struct DataFeedDetails { + bytes32 dataFeedId; + address tokenAddress; + } + + // RedStone provides values with 8 decimals + // Mento sorted oracles expect 24 decimals (24 - 8 = 16) + uint256 internal constant PRICE_MULTIPLIER = 1e16; + + // 68 = 4 (fun selector) + 32 (proposedTimestamp) + 32 (size of one array element) + uint256 internal constant INITIAL_CALLDATA_OFFSET = 68; + uint256 internal constant LOCATION_IN_SORTED_LIST_BYTE_SIZE = 64; + + struct LocationInSortedLinkedList { + address lesserKey; + address greaterKey; + } + + /** + * @dev Helpful function for upgradable contracts + */ + function initialize() public initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + // This function must be overriden + function getSortedOracles() + public + view + virtual + returns (ISortedOracles sortedOracles); + + /** + * @notice Used for getting proposed values from RedStone's data packages + * @param dataFeedIds An array of data feed identifiers + * @return values The normalized values for corresponding data feeds + */ + function getNormalizedOracleValuesFromTxCalldata( + bytes32[] calldata dataFeedIds + ) public view returns (uint256[] memory) { + uint256[] memory values = getOracleNumericValuesFromTxMsg(dataFeedIds); + for (uint256 i = 0; i < values.length; ) { + values[i] = normalizeRedstoneValueForMento(values[i]); + unchecked { + i++; + } // reduces gas costs + } + return values; + } + + /** + * @notice Helpful function to simplify the mento relayer implementation + */ + function updatePriceValuesAndCleanOldReports( + uint256 proposedTimestamp, + LocationInSortedLinkedList[] calldata locationsInSortedLinkedLists + ) external { + updatePriceValues(proposedTimestamp, locationsInSortedLinkedLists); + removeAllExpiredReports(); + } + + function removeAllExpiredReports() public { + uint256 tokensLength = getDataFeedsCount(); + ISortedOracles sortedOracles = getSortedOracles(); + for (uint256 tokenIndex = 0; tokenIndex < tokensLength; ) { + (, address tokenAddress) = getTokenDetailsAtIndex(tokenIndex); + uint256 curNumberOfReports = sortedOracles.numTimestamps(tokenAddress); + if (curNumberOfReports > 0) { + sortedOracles.removeExpiredReports( + tokenAddress, + curNumberOfReports - 1 + ); + } + unchecked { + tokenIndex++; + } // reduces gas costs + } + } + + function normalizeRedstoneValueForMento( + uint256 valueFromRedstone + ) public pure returns (uint256) { + return PRICE_MULTIPLIER * valueFromRedstone; + } + + function convertMentoValueToRedstoneValue( + uint256 mentoValue + ) public pure returns (uint256) { + return mentoValue / PRICE_MULTIPLIER; + } + + /** + * @notice Extracts Redstone's oracle values from calldata, verifying signatures + * and timestamps, and reports it to the SortedOracles contract + * @param proposedTimestamp Timestamp that should be lesser or equal to each + * timestamp from the signed data packages in calldata + * @param locationsInSortedLinkedLists The array of locations in linked list for reported values + */ + function updatePriceValues( + uint256 proposedTimestamp, + LocationInSortedLinkedList[] calldata locationsInSortedLinkedLists + ) public { + locationsInSortedLinkedLists; // This argument is used later (extracted from calldata) + updateDataFeedsValues(proposedTimestamp); + } + + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIds, + uint256[] memory values + ) internal override { + LocationInSortedLinkedList[] + memory locationsInSortedList = extractLinkedListLocationsFromCalldata(); + for (uint256 dataFeedIndex = 0; dataFeedIndex < dataFeedIds.length; ) { + (, address tokenAddress) = getTokenDetailsAtIndex(dataFeedIndex); + uint256 priceValue = normalizeRedstoneValueForMento( + values[dataFeedIndex] + ); + LocationInSortedLinkedList memory location = locationsInSortedList[ + dataFeedIndex + ]; + + getSortedOracles().report( + tokenAddress, + priceValue, + location.lesserKey, + location.greaterKey + ); + + unchecked { + dataFeedIndex++; + } // reduces gas costs + } + } + + function extractLinkedListLocationsFromCalldata() + private + pure + returns (LocationInSortedLinkedList[] memory locationsInSortedList) + { + uint256 calldataOffset = INITIAL_CALLDATA_OFFSET; + uint256 arrayLength = abi.decode( + msg.data[calldataOffset:calldataOffset + STANDARD_SLOT_BS], + (uint256) + ); + + calldataOffset += STANDARD_SLOT_BS; + + locationsInSortedList = new LocationInSortedLinkedList[](arrayLength); + for (uint256 i = 0; i < arrayLength; ) { + locationsInSortedList[i] = abi.decode( + msg.data[calldataOffset:calldataOffset + + LOCATION_IN_SORTED_LIST_BYTE_SIZE], + (LocationInSortedLinkedList) + ); + calldataOffset += LOCATION_IN_SORTED_LIST_BYTE_SIZE; + unchecked { + i++; + } // reduces gas costs + } + } + + function getDataFeedIds() public view override returns (bytes32[] memory) { + uint256 dataFeedsCount = getDataFeedsCount(); + bytes32[] memory dataFeedIds = new bytes32[](dataFeedsCount); + for (uint256 dataFeedIndex = 0; dataFeedIndex < dataFeedsCount; ) { + (dataFeedIds[dataFeedIndex], ) = getTokenDetailsAtIndex(dataFeedIndex); + unchecked { + dataFeedIndex++; + } // reduces gas costs + } + + return dataFeedIds; + } + + // This function is used by mento relayer + function getDataFeeds() public view returns (DataFeedDetails[] memory) { + uint256 dataFeedsCount = getDataFeedsCount(); + DataFeedDetails[] memory dataFeeds = new DataFeedDetails[](dataFeedsCount); + for (uint256 dataFeedIndex = 0; dataFeedIndex < dataFeedsCount; ) { + (bytes32 dataFeedId, address tokenAddress) = getTokenDetailsAtIndex( + dataFeedIndex + ); + dataFeeds[dataFeedIndex] = DataFeedDetails({ + dataFeedId: dataFeedId, + tokenAddress: tokenAddress + }); + unchecked { + dataFeedIndex++; + } // reduces gas costs + } + return dataFeeds; + } + + // This function must be overriden in the child contract + function getTokenDetailsAtIndex( + uint256 tokenIndex + ) public view virtual returns (bytes32 dataFeedId, address tokenAddress); + + // This function must be overriden in the child contract + function getDataFeedsCount() public view virtual returns (uint256); + + function getTokenIndexByDataFeedId( + bytes32 dataFeedId + ) public view virtual returns (uint256) { + uint256 dataFeedsCount = getDataFeedsCount(); + for (uint256 dataFeedIndex = 0; dataFeedIndex < dataFeedsCount; ) { + (bytes32 dataFeedIdAtIndex, ) = getTokenDetailsAtIndex(dataFeedIndex); + if (dataFeedId == dataFeedIdAtIndex) { + return dataFeedIndex; + } + unchecked { + dataFeedIndex++; + } // reduces gas costs + } + + revert DataFeedIdNotFound(dataFeedId); + } + + // [HIGH RISK] Using this function directly may cause significant risk + function getValueForDataFeedUnsafe( + bytes32 dataFeedId + ) public view override returns (uint256) { + uint256 tokenIndex = getTokenIndexByDataFeedId(dataFeedId); + (, address tokenAddress) = getTokenDetailsAtIndex(tokenIndex); + (uint256 medianRate, ) = getSortedOracles().medianRate(tokenAddress); + return convertMentoValueToRedstoneValue(medianRate); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/MockSortedOracles.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/MockSortedOracles.sol new file mode 100644 index 0000000..bda3fd3 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/MockSortedOracles.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import {SafeMath} from "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import {ISortedOracles} from "./ISortedOracles.sol"; +import {AddressSortedLinkedListWithMedian} from "./linkedlists/AddressSortedLinkedListWithMedian.sol"; +import {SortedLinkedListWithMedian} from "./linkedlists/SortedLinkedListWithMedian.sol"; + +/** + * @title Simplified mock version of the SortedOracles contract + * @author The Mento team (the code modified by the RedStone team) + * @dev It is used for testing the Mento adapter contract + */ +contract MockSortedOracles is ISortedOracles { + using SafeMath for uint256; + using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; + + uint256 private constant FIXED1_UINT = 1e24; + + uint256 public reportExpirySeconds; + + // Maps a rateFeedID to a sorted list of report values. + mapping(address => SortedLinkedListWithMedian.List) private rates; + // Maps a rateFeedID to a sorted list of report timestamps. + mapping(address => SortedLinkedListWithMedian.List) private timestamps; + + /** + * @notice Removes a report that is expired. + * @param token The rateFeedId of the report to be removed. + * @param n The number of expired reports to remove, at most (deterministic upper gas bound). + */ + function removeExpiredReports(address token, uint256 n) external { + require( + token != address(0) && n < timestamps[token].getNumElements(), + "token addr null or trying to remove too many reports" + ); + for (uint256 i = 0; i < n; i = i.add(1)) { + (bool isExpired, address oldestAddress) = isOldestReportExpired(token); + if (isExpired) { + removeReport(token, oldestAddress); + } else { + break; + } + } + } + + /** + * @notice Check if last report is expired. + * @param token The rateFeedId of the reports to be checked. + * @return bool A bool indicating if the last report is expired. + * @return address Oracle address of the last report. + */ + function isOldestReportExpired(address token) public view returns (bool, address) { + // solhint-disable-next-line reason-string + require(token != address(0)); + address oldest = timestamps[token].getTail(); + uint256 timestamp = timestamps[token].getValue(oldest); + // solhint-disable-next-line not-rely-on-time + if (block.timestamp.sub(timestamp) >= getTokenReportExpirySeconds(token)) { + return (true, oldest); + } + return (false, oldest); + } + + /** + * @notice Updates an oracle value and the median. + * @param token The rateFeedId for the rate that is being reported. + * @param value The number of stable asset that equate to one unit of collateral asset, for the + * specified rateFeedId, expressed as a fixidity value. + * @param lesserKey The element which should be just left of the new oracle value. + * @param greaterKey The element which should be just right of the new oracle value. + * @dev Note that only one of `lesserKey` or `greaterKey` needs to be correct to reduce friction. + */ + function report(address token, uint256 value, address lesserKey, address greaterKey) external { + // uint256 originalMedian = rates[token].getMedianValue(); + if (rates[token].contains(msg.sender)) { + rates[token].update(msg.sender, value, lesserKey, greaterKey); + + // Rather than update the timestamp, we remove it and re-add it at the + // head of the list later. The reason for this is that we need to handle + // a few different cases: + // 1. This oracle is the only one to report so far. lesserKey = address(0) + // 2. Other oracles have reported since this one's last report. lesserKey = getHead() + // 3. Other oracles have reported, but the most recent is this one. + // lesserKey = key immediately after getHead() + // + // However, if we just remove this timestamp, timestamps[token].getHead() + // does the right thing in all cases. + timestamps[token].remove(msg.sender); + } else { + rates[token].insert(msg.sender, value, lesserKey, greaterKey); + } + timestamps[token].insert( + msg.sender, + // solhint-disable-next-line not-rely-on-time + block.timestamp, + timestamps[token].getHead(), + address(0) + ); + } + + /** + * @notice Gets all elements from the doubly linked list. + * @param token The rateFeedId for which the collateral asset exchange rate is being reported. + * @return keys Keys of nn unpacked list of elements from largest to smallest. + * @return values Values of an unpacked list of elements from largest to smallest. + * @return relations Relations of an unpacked list of elements from largest to smallest. + */ + function getRates( + address token + ) + external + view + returns ( + address[] memory, + uint256[] memory, + SortedLinkedListWithMedian.MedianRelation[] memory + ) + { + return rates[token].getElements(); + } + + /** + * @notice Returns the number of rates that are currently stored for a specifed rateFeedId. + * @param token The rateFeedId for which to retrieve the number of rates. + * @return uint256 The number of reported oracle rates stored for the given rateFeedId. + */ + function numRates(address token) public view returns (uint256) { + return rates[token].getNumElements(); + } + + /** + * @notice Returns the median of the currently stored rates for a specified rateFeedId. + * @param token The rateFeedId of the rates for which the median value is being retrieved. + * @return uint256 The median exchange rate for rateFeedId. + * @return fixidity + */ + function medianRate(address token) external view returns (uint256, uint256) { + return (rates[token].getMedianValue(), numRates(token) == 0 ? 0 : FIXED1_UINT); + } + + /** + * @notice Returns the number of timestamps. + * @param token The rateFeedId for which the collateral asset exchange rate is being reported. + * @return uint256 The number of oracle report timestamps for the specified rateFeedId. + */ + function numTimestamps(address token) public view returns (uint256) { + return timestamps[token].getNumElements(); + } + + /** + * @notice Checks if a report exists for a specified rateFeedId from a given oracle. + * @param token The rateFeedId to be checked. + * @param oracle The oracle whose report should be checked. + * @return bool True if a report exists, false otherwise. + */ + function reportExists(address token, address oracle) internal view returns (bool) { + return rates[token].contains(oracle) && timestamps[token].contains(oracle); + } + + function getTokenReportExpirySeconds(address token) public view returns (uint256) { + token; + return reportExpirySeconds; + } + + /** + * @notice Removes an oracle value and updates the median. + * @param token The rateFeedId for which the collateral asset exchange rate is being reported. + * @param oracle The oracle whose value should be removed. + * @dev This can be used to delete elements for oracles that have been removed. + * However, a > 1 elements reports list should always be maintained + */ + function removeReport(address token, address oracle) private { + if (numTimestamps(token) == 1 && reportExists(token, oracle)) return; + rates[token].remove(oracle); + timestamps[token].remove(oracle); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/data-services/MentoAdapterPrimaryProd.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/data-services/MentoAdapterPrimaryProd.sol new file mode 100644 index 0000000..9f72ded --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/data-services/MentoAdapterPrimaryProd.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {MentoAdapterBase} from "../MentoAdapterBase.sol"; + +abstract contract MentoAdapterPrimaryProd is MentoAdapterBase { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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); + } + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressLinkedList.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressLinkedList.sol new file mode 100644 index 0000000..8e1fc52 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressLinkedList.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./LinkedList.sol"; + +/** + * @title Maintains a doubly linked list keyed by address. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + * Following the `next` pointers will lead you to the head, rather than the tail. + */ +library AddressLinkedList { + using LinkedList for LinkedList.List; + using SafeMath for uint256; + + function toBytes(address a) public pure returns (bytes32) { + return bytes32(uint256(uint160(a)) << 96); + } + + function toAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b) >> 96)); + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param previousKey The key of the element that comes before the element to insert. + * @param nextKey The key of the element that comes after the element to insert. + */ + function insert( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.insert(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Inserts an element at the end of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(LinkedList.List storage list, address key) public { + list.insert(toBytes(key), bytes32(0), list.tail); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(LinkedList.List storage list, address key) public { + list.remove(toBytes(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param previousKey The key of the element that comes before the updated element. + * @param nextKey The key of the element that comes after the updated element. + */ + function update( + LinkedList.List storage list, + address key, + address previousKey, + address nextKey + ) public { + list.update(toBytes(key), toBytes(previousKey), toBytes(nextKey)); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(LinkedList.List storage list, address key) public view returns (bool) { + return list.elements[toBytes(key)].exists; + } + + /** + * @notice Returns the N greatest elements of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(LinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.numElements); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedList.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedList.sol new file mode 100644 index 0000000..3d96e27 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedList.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/Math.sol"; +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./SortedLinkedList.sol"; + +/** + * @title Maintains a sorted list of unsigned ints keyed by address. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + */ +library AddressSortedLinkedList { + using SafeMath for uint256; + using SortedLinkedList for SortedLinkedList.List; + + function toBytes(address a) public pure returns (bytes32) { + return bytes32(uint256(uint160(a)) << 96); + } + + function toAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b) >> 96)); + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param value The element value. + * @param lesserKey The key of the element less than the element to insert. + * @param greaterKey The key of the element greater than the element to insert. + */ + function insert( + SortedLinkedList.List storage list, + address key, + uint256 value, + address lesserKey, + address greaterKey + ) public { + list.insert(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(SortedLinkedList.List storage list, address key) public { + list.remove(toBytes(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param value The element value. + * @param lesserKey The key of the element will be just left of `key` after the update. + * @param greaterKey The key of the element will be just right of `key` after the update. + * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. + */ + function update( + SortedLinkedList.List storage list, + address key, + uint256 value, + address lesserKey, + address greaterKey + ) public { + list.update(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(SortedLinkedList.List storage list, address key) public view returns (bool) { + return list.contains(toBytes(key)); + } + + /** + * @notice Returns the value for a particular key in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return The element value. + */ + function getValue( + SortedLinkedList.List storage list, + address key + ) public view returns (uint256) { + return list.getValue(toBytes(key)); + } + + /** + * @notice Gets all elements from the doubly linked list. + * @return Array of all keys in the list. + * @return Values corresponding to keys, which will be ordered largest to smallest. + */ + function getElements( + SortedLinkedList.List storage list + ) public view returns (address[] memory, uint256[] memory) { + bytes32[] memory byteKeys = list.getKeys(); + address[] memory keys = new address[](byteKeys.length); + uint256[] memory values = new uint256[](byteKeys.length); + for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + values[i] = list.values[byteKeys[i]]; + } + return (keys, values); + } + + /** + * @notice Returns the minimum of `max` and the number of elements in the list > threshold. + * @param list A storage pointer to the underlying list. + * @param threshold The number that the element must exceed to be included. + * @param max The maximum number returned by this function. + * @return The minimum of `max` and the number of elements in the list > threshold. + */ + function numElementsGreaterThan( + SortedLinkedList.List storage list, + uint256 threshold, + uint256 max + ) public view returns (uint256) { + uint256 revisedMax = Math.min(max, list.list.numElements); + bytes32 key = list.list.head; + for (uint256 i = 0; i < revisedMax; i = i.add(1)) { + if (list.getValue(key) < threshold) { + return i; + } + key = list.list.elements[key].previousKey; + } + return revisedMax; + } + + /** + * @notice Returns the N greatest elements of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the greatest elements. + */ + function headN( + SortedLinkedList.List storage list, + uint256 n + ) public view returns (address[] memory) { + bytes32[] memory byteKeys = list.headN(n); + address[] memory keys = new address[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(SortedLinkedList.List storage list) public view returns (address[] memory) { + return headN(list, list.list.numElements); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedListWithMedian.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedListWithMedian.sol new file mode 100644 index 0000000..97fe27a --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/AddressSortedLinkedListWithMedian.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./SortedLinkedListWithMedian.sol"; + +/** + * @title Maintains a sorted list of unsigned ints keyed by address. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + */ +library AddressSortedLinkedListWithMedian { + using SafeMath for uint256; + using SortedLinkedListWithMedian for SortedLinkedListWithMedian.List; + + function toBytes(address a) public pure returns (bytes32) { + return bytes32(uint256(uint160(a)) << 96); + } + + function toAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b) >> 96)); + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param value The element value. + * @param lesserKey The key of the element less than the element to insert. + * @param greaterKey The key of the element greater than the element to insert. + */ + function insert( + SortedLinkedListWithMedian.List storage list, + address key, + uint256 value, + address lesserKey, + address greaterKey + ) public { + list.insert(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(SortedLinkedListWithMedian.List storage list, address key) public { + list.remove(toBytes(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param value The element value. + * @param lesserKey The key of the element will be just left of `key` after the update. + * @param greaterKey The key of the element will be just right of `key` after the update. + * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. + */ + function update( + SortedLinkedListWithMedian.List storage list, + address key, + uint256 value, + address lesserKey, + address greaterKey + ) public { + list.update(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains( + SortedLinkedListWithMedian.List storage list, + address key + ) public view returns (bool) { + return list.contains(toBytes(key)); + } + + /** + * @notice Returns the value for a particular key in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return The element value. + */ + function getValue( + SortedLinkedListWithMedian.List storage list, + address key + ) public view returns (uint256) { + return list.getValue(toBytes(key)); + } + + /** + * @notice Returns the median value of the sorted list. + * @param list A storage pointer to the underlying list. + * @return The median value. + */ + function getMedianValue( + SortedLinkedListWithMedian.List storage list + ) public view returns (uint256) { + return list.getValue(list.median); + } + + /** + * @notice Returns the key of the first element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the first element in the list. + */ + function getHead(SortedLinkedListWithMedian.List storage list) external view returns (address) { + return toAddress(list.getHead()); + } + + /** + * @notice Returns the key of the median element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the median element in the list. + */ + function getMedian( + SortedLinkedListWithMedian.List storage list + ) external view returns (address) { + return toAddress(list.getMedian()); + } + + /** + * @notice Returns the key of the last element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the last element in the list. + */ + function getTail(SortedLinkedListWithMedian.List storage list) external view returns (address) { + return toAddress(list.getTail()); + } + + /** + * @notice Returns the number of elements in the list. + * @param list A storage pointer to the underlying list. + * @return The number of elements in the list. + */ + function getNumElements( + SortedLinkedListWithMedian.List storage list + ) external view returns (uint256) { + return list.getNumElements(); + } + + /** + * @notice Gets all elements from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return Array of all keys in the list. + * @return Values corresponding to keys, which will be ordered largest to smallest. + * @return Array of relations to median of corresponding list elements. + */ + function getElements( + SortedLinkedListWithMedian.List storage list + ) + public + view + returns ( + address[] memory, + uint256[] memory, + SortedLinkedListWithMedian.MedianRelation[] memory + ) + { + bytes32[] memory byteKeys = list.getKeys(); + address[] memory keys = new address[](byteKeys.length); + uint256[] memory values = new uint256[](byteKeys.length); + SortedLinkedListWithMedian.MedianRelation[] memory relations = + new SortedLinkedListWithMedian.MedianRelation[](keys.length); + for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { + keys[i] = toAddress(byteKeys[i]); + values[i] = list.getValue(byteKeys[i]); + relations[i] = list.relation[byteKeys[i]]; + } + return (keys, values, relations); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/IntegerSortedLinkedList.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/IntegerSortedLinkedList.sol new file mode 100644 index 0000000..407619a --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/IntegerSortedLinkedList.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./SortedLinkedList.sol"; + +/** + * @title Maintains a sorted list of unsigned ints keyed by uint256. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + */ +library IntegerSortedLinkedList { + using SafeMath for uint256; + using SortedLinkedList for SortedLinkedList.List; + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param value The element value. + * @param lesserKey The key of the element less than the element to insert. + * @param greaterKey The key of the element greater than the element to insert. + */ + function insert( + SortedLinkedList.List storage list, + uint256 key, + uint256 value, + uint256 lesserKey, + uint256 greaterKey + ) public { + list.insert(bytes32(key), value, bytes32(lesserKey), bytes32(greaterKey)); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(SortedLinkedList.List storage list, uint256 key) public { + list.remove(bytes32(key)); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param value The element value. + * @param lesserKey The key of the element will be just left of `key` after the update. + * @param greaterKey The key of the element will be just right of `key` after the update. + * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. + */ + function update( + SortedLinkedList.List storage list, + uint256 key, + uint256 value, + uint256 lesserKey, + uint256 greaterKey + ) public { + list.update(bytes32(key), value, bytes32(lesserKey), bytes32(greaterKey)); + } + + /** + * @notice Inserts an element at the end of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(SortedLinkedList.List storage list, uint256 key) public { + list.push(bytes32(key)); + } + + /** + * @notice Removes N elements from the head of the list and returns their keys. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to pop. + * @return The keys of the popped elements. + */ + function popN(SortedLinkedList.List storage list, uint256 n) public returns (uint256[] memory) { + bytes32[] memory byteKeys = list.popN(n); + uint256[] memory keys = new uint256[](byteKeys.length); + for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { + keys[i] = uint256(byteKeys[i]); + } + return keys; + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(SortedLinkedList.List storage list, uint256 key) public view returns (bool) { + return list.contains(bytes32(key)); + } + + /** + * @notice Returns the value for a particular key in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return The element value. + */ + function getValue( + SortedLinkedList.List storage list, + uint256 key + ) public view returns (uint256) { + return list.getValue(bytes32(key)); + } + + /** + * @notice Gets all elements from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return Array of all keys in the list. + * @return Values corresponding to keys, which will be ordered largest to smallest. + */ + function getElements( + SortedLinkedList.List storage list + ) public view returns (uint256[] memory, uint256[] memory) { + bytes32[] memory byteKeys = list.getKeys(); + uint256[] memory keys = new uint256[](byteKeys.length); + uint256[] memory values = new uint256[](byteKeys.length); + for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { + keys[i] = uint256(byteKeys[i]); + values[i] = list.values[byteKeys[i]]; + } + return (keys, values); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/LinkedList.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/LinkedList.sol new file mode 100644 index 0000000..65a0e8a --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/LinkedList.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; + +/** + * @title Maintains a doubly linked list keyed by bytes32. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + * Following the `next` pointers will lead you to the head, rather than the tail. + */ +library LinkedList { + using SafeMath for uint256; + + struct Element { + bytes32 previousKey; + bytes32 nextKey; + bool exists; + } + + struct List { + bytes32 head; + bytes32 tail; + uint256 numElements; + mapping(bytes32 => Element) elements; + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param previousKey The key of the element that comes before the element to insert. + * @param nextKey The key of the element that comes after the element to insert. + */ + function insert(List storage list, bytes32 key, bytes32 previousKey, bytes32 nextKey) internal { + require(key != bytes32(0), "Key must be defined"); + require(!contains(list, key), "Can't insert an existing element"); + require( + previousKey != key && nextKey != key, + "Key cannot be the same as previousKey or nextKey" + ); + + Element storage element = list.elements[key]; + element.exists = true; + + if (list.numElements == 0) { + list.tail = key; + list.head = key; + } else { + require( + previousKey != bytes32(0) || nextKey != bytes32(0), + "Either previousKey or nextKey must be defined" + ); + + element.previousKey = previousKey; + element.nextKey = nextKey; + + if (previousKey != bytes32(0)) { + require( + contains(list, previousKey), + "If previousKey is defined, it must exist in the list" + ); + Element storage previousElement = list.elements[previousKey]; + require(previousElement.nextKey == nextKey, "previousKey must be adjacent to nextKey"); + previousElement.nextKey = key; + } else { + list.tail = key; + } + + if (nextKey != bytes32(0)) { + require(contains(list, nextKey), "If nextKey is defined, it must exist in the list"); + Element storage nextElement = list.elements[nextKey]; + require(nextElement.previousKey == previousKey, "previousKey must be adjacent to nextKey"); + nextElement.previousKey = key; + } else { + list.head = key; + } + } + + list.numElements = list.numElements.add(1); + } + + /** + * @notice Inserts an element at the tail of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(List storage list, bytes32 key) internal { + insert(list, key, bytes32(0), list.tail); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(List storage list, bytes32 key) internal { + Element storage element = list.elements[key]; + require(key != bytes32(0) && contains(list, key), "key not in list"); + if (element.previousKey != bytes32(0)) { + Element storage previousElement = list.elements[element.previousKey]; + previousElement.nextKey = element.nextKey; + } else { + list.tail = element.nextKey; + } + + if (element.nextKey != bytes32(0)) { + Element storage nextElement = list.elements[element.nextKey]; + nextElement.previousKey = element.previousKey; + } else { + list.head = element.previousKey; + } + + delete list.elements[key]; + list.numElements = list.numElements.sub(1); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param previousKey The key of the element that comes before the updated element. + * @param nextKey The key of the element that comes after the updated element. + */ + function update(List storage list, bytes32 key, bytes32 previousKey, bytes32 nextKey) internal { + require( + key != bytes32(0) && key != previousKey && key != nextKey && contains(list, key), + "key on in list" + ); + remove(list, key); + insert(list, key, previousKey, nextKey); + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(List storage list, bytes32 key) internal view returns (bool) { + return list.elements[key].exists; + } + + /** + * @notice Returns the keys of the N elements at the head of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the N elements at the head of the list. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(List storage list, uint256 n) internal view returns (bytes32[] memory) { + require(n <= list.numElements, "not enough elements"); + bytes32[] memory keys = new bytes32[](n); + bytes32 key = list.head; + for (uint256 i = 0; i < n; i = i.add(1)) { + keys[i] = key; + key = list.elements[key].previousKey; + } + return keys; + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(List storage list) internal view returns (bytes32[] memory) { + return headN(list, list.numElements); + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedList.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedList.sol new file mode 100644 index 0000000..d725c94 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedList.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./LinkedList.sol"; + +/** + * @title Maintains a sorted list of unsigned ints keyed by bytes32. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + */ +library SortedLinkedList { + using SafeMath for uint256; + using LinkedList for LinkedList.List; + + struct List { + LinkedList.List list; + mapping(bytes32 => uint256) values; + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param value The element value. + * @param lesserKey The key of the element less than the element to insert. + * @param greaterKey The key of the element greater than the element to insert. + */ + function insert( + List storage list, + bytes32 key, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) internal { + require( + key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key), + "invalid key" + ); + require( + (lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0, + "greater and lesser key zero" + ); + require(contains(list, lesserKey) || lesserKey == bytes32(0), "invalid lesser key"); + require(contains(list, greaterKey) || greaterKey == bytes32(0), "invalid greater key"); + (lesserKey, greaterKey) = getLesserAndGreater(list, value, lesserKey, greaterKey); + list.list.insert(key, lesserKey, greaterKey); + list.values[key] = value; + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(List storage list, bytes32 key) internal { + list.list.remove(key); + list.values[key] = 0; + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param value The element value. + * @param lesserKey The key of the element will be just left of `key` after the update. + * @param greaterKey The key of the element will be just right of `key` after the update. + * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. + */ + function update( + List storage list, + bytes32 key, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) internal { + remove(list, key); + insert(list, key, value, lesserKey, greaterKey); + } + + /** + * @notice Inserts an element at the tail of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(List storage list, bytes32 key) internal { + insert(list, key, 0, bytes32(0), list.list.tail); + } + + /** + * @notice Removes N elements from the head of the list and returns their keys. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to pop. + * @return The keys of the popped elements. + */ + function popN(List storage list, uint256 n) internal returns (bytes32[] memory) { + require(n <= list.list.numElements, "not enough elements"); + bytes32[] memory keys = new bytes32[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + bytes32 key = list.list.head; + keys[i] = key; + remove(list, key); + } + return keys; + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(List storage list, bytes32 key) internal view returns (bool) { + return list.list.contains(key); + } + + /** + * @notice Returns the value for a particular key in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return The element value. + */ + function getValue(List storage list, bytes32 key) internal view returns (uint256) { + return list.values[key]; + } + + /** + * @notice Gets all elements from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return Array of all keys in the list. + * @return Values corresponding to keys, which will be ordered largest to smallest. + */ + function getElements( + List storage list + ) internal view returns (bytes32[] memory, uint256[] memory) { + bytes32[] memory keys = getKeys(list); + uint256[] memory values = new uint256[](keys.length); + for (uint256 i = 0; i < keys.length; i = i.add(1)) { + values[i] = list.values[keys[i]]; + } + return (keys, values); + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(List storage list) internal view returns (bytes32[] memory) { + return list.list.getKeys(); + } + + /** + * @notice Returns first N greatest elements of the list. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to return. + * @return The keys of the first n elements. + * @dev Reverts if n is greater than the number of elements in the list. + */ + function headN(List storage list, uint256 n) internal view returns (bytes32[] memory) { + return list.list.headN(n); + } + + /** + * @notice Returns the keys of the elements greaterKey than and less than the provided value. + * @param list A storage pointer to the underlying list. + * @param value The element value. + * @param lesserKey The key of the element which could be just left of the new value. + * @param greaterKey The key of the element which could be just right of the new value. + * @return correctLesserValue The correct lesserKey keys. + * @return correctGreaterValue The correct greaterKey keys. + */ + function getLesserAndGreater( + List storage list, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) private view returns (bytes32 correctLesserValue, bytes32 correctGreaterValue) { + // Check for one of the following conditions and fail if none are met: + // 1. The value is less than the current lowest value + // 2. The value is greater than the current greatest value + // 3. The value is just greater than the value for `lesserKey` + // 4. The value is just less than the value for `greaterKey` + if (lesserKey == bytes32(0) && isValueBetween(list, value, lesserKey, list.list.tail)) { + return (lesserKey, list.list.tail); + } else if ( + greaterKey == bytes32(0) && isValueBetween(list, value, list.list.head, greaterKey) + ) { + return (list.list.head, greaterKey); + } else if ( + lesserKey != bytes32(0) && + isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey) + ) { + return (lesserKey, list.list.elements[lesserKey].nextKey); + } else if ( + greaterKey != bytes32(0) && + isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) + ) { + return (list.list.elements[greaterKey].previousKey, greaterKey); + } else { + require(false, "get lesser and greater failure"); + } + } + + /** + * @notice Returns whether or not a given element is between two other elements. + * @param list A storage pointer to the underlying list. + * @param value The element value. + * @param lesserKey The key of the element whose value should be lesserKey. + * @param greaterKey The key of the element whose value should be greaterKey. + * @return True if the given element is between the two other elements. + */ + function isValueBetween( + List storage list, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) private view returns (bool) { + bool isLesser = lesserKey == bytes32(0) || list.values[lesserKey] <= value; + bool isGreater = greaterKey == bytes32(0) || list.values[greaterKey] >= value; + return isLesser && isGreater; + } +} diff --git a/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedListWithMedian.sol b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedListWithMedian.sol new file mode 100644 index 0000000..e7cddf2 --- /dev/null +++ b/src/on-chain-relayer/contracts/custom-integrations/mento/linkedlists/SortedLinkedListWithMedian.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.14; + +import "@openzeppelin/contracts/utils/math/SafeMath.sol"; +import "./LinkedList.sol"; +import "./SortedLinkedList.sol"; + +/** + * @title Maintains a sorted list of unsigned ints keyed by bytes32. + * @author The Mento team (the code modified by the RedStone team) + * @dev The code has been slightly modified to be compatible with ^0.8.0 version + */ +library SortedLinkedListWithMedian { + using SafeMath for uint256; + using SortedLinkedList for SortedLinkedList.List; + + enum MedianAction { + None, + Lesser, + Greater + } + + enum MedianRelation { + Undefined, + Lesser, + Greater, + Equal + } + + struct List { + SortedLinkedList.List list; + bytes32 median; + mapping(bytes32 => MedianRelation) relation; + } + + /** + * @notice Inserts an element into a doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + * @param value The element value. + * @param lesserKey The key of the element less than the element to insert. + * @param greaterKey The key of the element greater than the element to insert. + */ + function insert( + List storage list, + bytes32 key, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) internal { + list.list.insert(key, value, lesserKey, greaterKey); + LinkedList.Element storage element = list.list.list.elements[key]; + + MedianAction action = MedianAction.None; + if (list.list.list.numElements == 1) { + list.median = key; + list.relation[key] = MedianRelation.Equal; + } else if (list.list.list.numElements % 2 == 1) { + // When we have an odd number of elements, and the element that we inserted is less than + // the previous median, we need to slide the median down one element, since we had previously + // selected the greater of the two middle elements. + if ( + element.previousKey == bytes32(0) || + list.relation[element.previousKey] == MedianRelation.Lesser + ) { + action = MedianAction.Lesser; + list.relation[key] = MedianRelation.Lesser; + } else { + list.relation[key] = MedianRelation.Greater; + } + } else { + // When we have an even number of elements, and the element that we inserted is greater than + // the previous median, we need to slide the median up one element, since we always select + // the greater of the two middle elements. + if ( + element.nextKey == bytes32(0) || list.relation[element.nextKey] == MedianRelation.Greater + ) { + action = MedianAction.Greater; + list.relation[key] = MedianRelation.Greater; + } else { + list.relation[key] = MedianRelation.Lesser; + } + } + updateMedian(list, action); + } + + /** + * @notice Removes an element from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to remove. + */ + function remove(List storage list, bytes32 key) internal { + MedianAction action = MedianAction.None; + if (list.list.list.numElements == 0) { + list.median = bytes32(0); + } else if (list.list.list.numElements % 2 == 0) { + // When we have an even number of elements, we always choose the higher of the two medians. + // Thus, if the element we're removing is greaterKey than or equal to the median we need to + // slide the median left by one. + if ( + list.relation[key] == MedianRelation.Greater || list.relation[key] == MedianRelation.Equal + ) { + action = MedianAction.Lesser; + } + } else { + // When we don't have an even number of elements, we just choose the median value. + // Thus, if the element we're removing is less than or equal to the median, we need to slide + // median right by one. + if ( + list.relation[key] == MedianRelation.Lesser || list.relation[key] == MedianRelation.Equal + ) { + action = MedianAction.Greater; + } + } + updateMedian(list, action); + + list.list.remove(key); + } + + /** + * @notice Updates an element in the list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @param value The element value. + * @param lesserKey The key of the element will be just left of `key` after the update. + * @param greaterKey The key of the element will be just right of `key` after the update. + * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. + */ + function update( + List storage list, + bytes32 key, + uint256 value, + bytes32 lesserKey, + bytes32 greaterKey + ) internal { + remove(list, key); + insert(list, key, value, lesserKey, greaterKey); + } + + /** + * @notice Inserts an element at the tail of the doubly linked list. + * @param list A storage pointer to the underlying list. + * @param key The key of the element to insert. + */ + function push(List storage list, bytes32 key) internal { + insert(list, key, 0, bytes32(0), list.list.list.tail); + } + + /** + * @notice Removes N elements from the head of the list and returns their keys. + * @param list A storage pointer to the underlying list. + * @param n The number of elements to pop. + * @return The keys of the popped elements. + */ + function popN(List storage list, uint256 n) internal returns (bytes32[] memory) { + require(n <= list.list.list.numElements, "not enough elements"); + bytes32[] memory keys = new bytes32[](n); + for (uint256 i = 0; i < n; i = i.add(1)) { + bytes32 key = list.list.list.head; + keys[i] = key; + remove(list, key); + } + return keys; + } + + /** + * @notice Returns whether or not a particular key is present in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return Whether or not the key is in the sorted list. + */ + function contains(List storage list, bytes32 key) internal view returns (bool) { + return list.list.contains(key); + } + + /** + * @notice Returns the value for a particular key in the sorted list. + * @param list A storage pointer to the underlying list. + * @param key The element key. + * @return The element value. + */ + function getValue(List storage list, bytes32 key) internal view returns (uint256) { + return list.list.values[key]; + } + + /** + * @notice Returns the median value of the sorted list. + * @param list A storage pointer to the underlying list. + * @return The median value. + */ + function getMedianValue(List storage list) internal view returns (uint256) { + return getValue(list, list.median); + } + + /** + * @notice Returns the key of the first element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the first element in the list. + */ + function getHead(List storage list) internal view returns (bytes32) { + return list.list.list.head; + } + + /** + * @notice Returns the key of the median element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the median element in the list. + */ + function getMedian(List storage list) internal view returns (bytes32) { + return list.median; + } + + /** + * @notice Returns the key of the last element in the list. + * @param list A storage pointer to the underlying list. + * @return The key of the last element in the list. + */ + function getTail(List storage list) internal view returns (bytes32) { + return list.list.list.tail; + } + + /** + * @notice Returns the number of elements in the list. + * @param list A storage pointer to the underlying list. + * @return The number of elements in the list. + */ + function getNumElements(List storage list) internal view returns (uint256) { + return list.list.list.numElements; + } + + /** + * @notice Gets all elements from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return Array of all keys in the list. + * @return Values corresponding to keys, which will be ordered largest to smallest. + * @return Array of relations to median of corresponding list elements. + */ + function getElements( + List storage list + ) internal view returns (bytes32[] memory, uint256[] memory, MedianRelation[] memory) { + bytes32[] memory keys = getKeys(list); + uint256[] memory values = new uint256[](keys.length); + MedianRelation[] memory relations = new MedianRelation[](keys.length); + for (uint256 i = 0; i < keys.length; i = i.add(1)) { + values[i] = list.list.values[keys[i]]; + relations[i] = list.relation[keys[i]]; + } + return (keys, values, relations); + } + + /** + * @notice Gets all element keys from the doubly linked list. + * @param list A storage pointer to the underlying list. + * @return All element keys from head to tail. + */ + function getKeys(List storage list) internal view returns (bytes32[] memory) { + return list.list.getKeys(); + } + + /** + * @notice Moves the median pointer right or left of its current value. + * @param list A storage pointer to the underlying list. + * @param action Which direction to move the median pointer. + */ + function updateMedian(List storage list, MedianAction action) private { + LinkedList.Element storage previousMedian = list.list.list.elements[list.median]; + if (action == MedianAction.Lesser) { + list.relation[list.median] = MedianRelation.Greater; + list.median = previousMedian.previousKey; + } else if (action == MedianAction.Greater) { + list.relation[list.median] = MedianRelation.Lesser; + list.median = previousMedian.nextKey; + } + list.relation[list.median] = MedianRelation.Equal; + } +} diff --git a/src/on-chain-relayer/contracts/erc7412/IERC7412.sol b/src/on-chain-relayer/contracts/erc7412/IERC7412.sol new file mode 100644 index 0000000..1a6c974 --- /dev/null +++ b/src/on-chain-relayer/contracts/erc7412/IERC7412.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title ERC-7412 Off-Chain Data Retrieval Contract + */ +interface IERC7412 { + /** + * @dev Emitted when an oracle is requested to provide data. Upon receipt of this error, a wallet client + * should automatically resolve the requested oracle data and call fulfillOracleQuery. + * @param oracleContract The address of the oracle contract (which is also the fulfillment contract). + * @param oracleQuery The query to be sent to the off-chain interface. + */ + error OracleDataRequired(address oracleContract, bytes oracleQuery); + + /** + * @dev Emitted when the recently posted oracle data requires a fee to be paid. Upon receipt of this error, + * a wallet client should attach the requested feeAmount to the most recently posted oracle data transaction + */ + error FeeRequired(uint feeAmount); + + /** + * @dev Returns a human-readable identifier of the oracle contract. This should map to a URL and API + * key on the client side. + * @return The oracle identifier. + */ + function oracleId() view external returns (bytes32); + + /** + * @dev Upon resolving the oracle query, the client should call this function to post the data to the + * blockchain. + * @param signedOffchainData The data that was returned from the off-chain interface, signed by the oracle. + */ + function fulfillOracleQuery(bytes calldata signedOffchainData) payable external; +} diff --git a/src/on-chain-relayer/contracts/erc7412/RedstonePrimaryProdWithoutRoundsERC7412.sol b/src/on-chain-relayer/contracts/erc7412/RedstonePrimaryProdWithoutRoundsERC7412.sol new file mode 100644 index 0000000..3273a7c --- /dev/null +++ b/src/on-chain-relayer/contracts/erc7412/RedstonePrimaryProdWithoutRoundsERC7412.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.14; + +import {IERC7412} from './IERC7412.sol'; +import {MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd} from '../price-feeds/data-services/MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd.sol'; + + /** + * @title Implementation of a price feeds adapter and price feed and ERC7412 + * @author The Redstone Oracles team + * @dev This contract is abstract, the following functions should be + * implemented in the actual contract before deployment: + * - getDataFeedId + * - getTTL + */ + abstract contract RedstonePrimaryProdWithoutRoundsERC7412 is IERC7412, MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd { + bytes32 constant ORACLE_ID = bytes32("REDSTONE"); + uint256 constant MAX_DATA_AHEAD_SECONDS = 120; + uint256 constant MAX_DATA_DELAY_SECONDS = 120; + + function getTTL() view internal virtual returns (uint256); + + function oracleId() pure external returns (bytes32) { + return ORACLE_ID; + } + + function getAllowedTimestampDiffsInSeconds() public view override virtual returns (uint256 maxDataAheadSeconds, uint256 maxDataDelaySeconds) { + return (MAX_DATA_AHEAD_SECONDS, MAX_DATA_DELAY_SECONDS); + } + + /** + * @dev If price was updated recently we return success. + * This allow smooth UX when two users independently tries to update price in same block + */ + function updateDataFeedsValues(uint256 dataPackagesTimestamp) override public virtual { + uint256 lastTimestamp = getBlockTimestampFromLatestUpdate(); + if(block.timestamp - lastTimestamp < MIN_INTERVAL_BETWEEN_UPDATES) { + return; + } + + super.updateDataFeedsValues(dataPackagesTimestamp); + } + + function validateDataFeedValueOnRead(bytes32 dataFeedId, uint256 value) public view override virtual { + uint256 lastTimestamp = getBlockTimestampFromLatestUpdate(); + if (block.timestamp - lastTimestamp > getTTL()) { + revert OracleDataRequired( + address(this), + abi.encode(getDataFeedId(), getUniqueSignersThreshold(), getDataServiceId()) + ); + } + + super.validateDataFeedValueOnRead(dataFeedId, value); + } + + function fulfillOracleQuery(bytes calldata signedOffchainData) payable external { + (uint256 dataTimestamp) = abi.decode(signedOffchainData, (uint256)); + updateDataFeedsValues(dataTimestamp); + } +} diff --git a/src/on-chain-relayer/contracts/libs/DeviationLib.sol b/src/on-chain-relayer/contracts/libs/DeviationLib.sol new file mode 100644 index 0000000..8977c19 --- /dev/null +++ b/src/on-chain-relayer/contracts/libs/DeviationLib.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +library DeviationLib { + function calculateAbsDeviation( + uint256 proposedValue, + uint256 originalValue, + uint256 precision + ) internal pure returns (uint256) { + int256 originalValueAsInt = SafeCast.toInt256(originalValue); + return + abs( + ((SafeCast.toInt256(proposedValue) - originalValueAsInt) * + 100 * + SafeCast.toInt256(precision)) / originalValueAsInt + ); + } + + function abs(int256 x) private pure returns (uint256) { + return SafeCast.toUint256(x >= 0 ? x : -x); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/MergedPriceFeedAdapterCommon.sol b/src/on-chain-relayer/contracts/price-feeds/MergedPriceFeedAdapterCommon.sol new file mode 100644 index 0000000..e47fb7f --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/MergedPriceFeedAdapterCommon.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.14; +import {IRedstoneAdapter} from "../core/IRedstoneAdapter.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +abstract contract MergedPriceFeedAdapterCommon { + event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); + + error CannotUpdateMoreThanOneDataFeed(); + + function getPriceFeedAdapter() public view virtual returns (IRedstoneAdapter) { + return IRedstoneAdapter(address(this)); + } + + function aggregator() public view virtual returns (address) { + return address(getPriceFeedAdapter()); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/PriceFeedBase.sol b/src/on-chain-relayer/contracts/price-feeds/PriceFeedBase.sol new file mode 100644 index 0000000..bad9e52 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/PriceFeedBase.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.14; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IRedstoneAdapter} from "../core/IRedstoneAdapter.sol"; +import {IPriceFeed} from "./interfaces/IPriceFeed.sol"; + +/** + * @title Main logic of the price feed contract + * @author The Redstone Oracles team + * @dev Implementation of common functions for the PriceFeed contract + * that queries data from the specified PriceFeedAdapter + * + * It can be used by projects that have already implemented with Chainlink-like + * price feeds and would like to minimise changes in their existing codebase. + * + * If you are flexible, it's much better (and cheaper in terms of gas) to query + * the PriceFeedAdapter contract directly + */ +abstract contract PriceFeedBase is IPriceFeed, Initializable { + uint256 internal constant INT256_MAX = uint256(type(int256).max); + + error UnsafeUintToIntConversion(uint256 value); + + /** + * @dev Helpful function for upgradable contracts + */ + function initialize() public virtual initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + /** + * @notice Returns data feed identifier for the PriceFeed contract + * @return dataFeedId The identifier of the data feed + */ + function getDataFeedId() public view virtual returns (bytes32); + + /** + * @notice Returns the address of the price feed adapter + * @return address The address of the price feed adapter + */ + function getPriceFeedAdapter() public view virtual returns (IRedstoneAdapter); + + + /** + * @notice Returns the number of decimals for the price feed + * @dev By default, RedStone uses 8 decimals for data feeds + * @return decimals The number of decimals in the price feed values + */ + function decimals() public virtual pure override returns (uint8) { + return 8; + } + + + /** + * @notice Description of the Price Feed + * @return description + */ + function description() public view virtual override returns (string memory) { + return "Redstone Price Feed"; + } + + /** + * @notice Version of the Price Feed + * @dev Currently it has no specific motivation and was added + * only to be compatible with the Chainlink interface + * @return version + */ + function version() public virtual pure override returns (uint256) { + return 1; + } + + + /** + * @notice Returns details of the latest successful update round + * @dev It uses few helpful functions to abstract logic of getting + * latest round id and value + * @return roundId The number of the latest round + * @return answer The latest reported value + * @return startedAt Block timestamp when the latest successful round started + * @return updatedAt Block timestamp of the latest successful round + * @return answeredInRound The number of the latest round + */ + function latestRoundData() + public + view + override + virtual + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + roundId = latestRound(); + answer = latestAnswer(); + + uint256 blockTimestamp = getPriceFeedAdapter().getBlockTimestampFromLatestUpdate(); + + // These values are equal after chainlink’s OCR update + startedAt = blockTimestamp; + updatedAt = blockTimestamp; + + // We want to be compatible with Chainlink's interface + // And in our case the roundId is always equal to answeredInRound + answeredInRound = roundId; + } + + /** + * @notice Old Chainlink function for getting the latest successfully reported value + * @return latestAnswer The latest successfully reported value + */ + function latestAnswer() public virtual view returns (int256) { + bytes32 dataFeedId = getDataFeedId(); + + uint256 uintAnswer = getPriceFeedAdapter().getValueForDataFeed(dataFeedId); + + if (uintAnswer > INT256_MAX) { + revert UnsafeUintToIntConversion(uintAnswer); + } + + return int256(uintAnswer); + } + + /** + * @notice Old Chainlink function for getting the number of latest round + * @return latestRound The number of the latest update round + */ + function latestRound() public view virtual returns (uint80); +} diff --git a/src/on-chain-relayer/contracts/price-feeds/PriceFeedsAdapterBase.sol b/src/on-chain-relayer/contracts/price-feeds/PriceFeedsAdapterBase.sol new file mode 100644 index 0000000..18468e8 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/PriceFeedsAdapterBase.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {RedstoneAdapterBase} from "../core/RedstoneAdapterBase.sol"; + +/** + * @title Common logic of the price feeds adapter contracts + * @author The Redstone Oracles team + */ +abstract contract PriceFeedsAdapterBase is RedstoneAdapterBase, Initializable { + + /** + * @dev Helpful function for upgradable contracts + */ + function initialize() public virtual initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + /** + * @dev This function is virtual and may contain additional logic in the derived contract + * E.g. it can check if the updating conditions are met (e.g. if at least one + * value is deviated enough) + * @param dataFeedIdsArray Array of all data feeds identifiers + * @param values The reported values that are validated and reported + */ + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIdsArray, + uint256[] memory values + ) internal virtual override { + for (uint256 i = 0; i < dataFeedIdsArray.length;) { + _validateAndUpdateDataFeedValue(dataFeedIdsArray[i], values[i]); + unchecked { i++; } // reduces gas costs + } + } + + /** + * @dev Helpful virtual function for handling value validation and saving in derived + * Price Feed Adapters contracts + * @param dataFeedId The data feed identifier + * @param dataFeedValue Proposed value for the data feed + */ + function _validateAndUpdateDataFeedValue(bytes32 dataFeedId, uint256 dataFeedValue) internal virtual; +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithRoundsPrimaryProd.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithRoundsPrimaryProd.sol new file mode 100644 index 0000000..4fb70f8 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithRoundsPrimaryProd.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {MergedPriceFeedAdapterWithRounds} from "../with-rounds/MergedPriceFeedAdapterWithRounds.sol"; + +abstract contract MergedPriceFeedAdapterWithRoundsPrimaryProd is MergedPriceFeedAdapterWithRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithoutRoundsPrimaryProd.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithoutRoundsPrimaryProd.sol new file mode 100644 index 0000000..2e47bcd --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedPriceFeedAdapterWithoutRoundsPrimaryProd.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {MergedPriceFeedAdapterWithoutRounds} from "../without-rounds/MergedPriceFeedAdapterWithoutRounds.sol"; + +abstract contract MergedPriceFeedAdapterWithoutRoundsPrimaryProd is MergedPriceFeedAdapterWithoutRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd.sol new file mode 100644 index 0000000..cc9a846 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {MergedSinglePriceFeedAdapterWithoutRounds} from "../without-rounds/MergedSinglePriceFeedAdapterWithoutRounds.sol"; + +abstract contract MergedSinglePriceFeedAdapterWithoutRoundsPrimaryProd is MergedSinglePriceFeedAdapterWithoutRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 3; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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 getDataServiceId() public view override virtual returns (string memory) { + return "redstone-primary-prod"; + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsMainDemo.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsMainDemo.sol new file mode 100644 index 0000000..d764902 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsMainDemo.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterWithRounds} from "../with-rounds/PriceFeedsAdapterWithRounds.sol"; + +abstract contract PriceFeedsAdapterWithRoundsMainDemo is PriceFeedsAdapterWithRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 1; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + if (signerAddress == 0x0C39486f770B26F5527BBBf942726537986Cd7eb) { + return 0; + } else { + revert SignerNotAuthorised(signerAddress); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsPrimaryProd.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsPrimaryProd.sol new file mode 100644 index 0000000..079c81f --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithRoundsPrimaryProd.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterWithRounds} from "../with-rounds/PriceFeedsAdapterWithRounds.sol"; + +abstract contract PriceFeedsAdapterWithRoundsPrimaryProd is PriceFeedsAdapterWithRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsMainDemo.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsMainDemo.sol new file mode 100644 index 0000000..1ca91f5 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsMainDemo.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterWithoutRounds} from "../without-rounds/PriceFeedsAdapterWithoutRounds.sol"; + +abstract contract PriceFeedsAdapterWithoutRoundsMainDemo is PriceFeedsAdapterWithoutRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 1; + } + + function getAuthorisedSignerIndex(address signerAddress) + public + view + virtual + override + returns (uint8) + { + if (signerAddress == 0x0C39486f770B26F5527BBBf942726537986Cd7eb) { + return 0; + } else { + revert SignerNotAuthorised(signerAddress); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsPrimaryProd.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsPrimaryProd.sol new file mode 100644 index 0000000..e15620f --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/PriceFeedsAdapterWithoutRoundsPrimaryProd.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterWithoutRounds} from "../without-rounds/PriceFeedsAdapterWithoutRounds.sol"; + +abstract contract PriceFeedsAdapterWithoutRoundsPrimaryProd is PriceFeedsAdapterWithoutRounds { + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + 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); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapter.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapter.sol new file mode 100644 index 0000000..de32c4e --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapter.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SinglePriceFeedAdapter} from "../without-rounds/SinglePriceFeedAdapter.sol"; + +contract VSTPriceFeedsAdapter is SinglePriceFeedAdapter { + uint256 internal constant BIT_MASK_TO_CHECK_CIRCUIT_BREAKER_FLAG = 0x0000000000000000000000000100000000000000000000000000000000000000; + + error InvalidSignersCount(uint256 signersCount); + error CircuitBreakerTripped(); + + function getDataFeedId() public pure override returns (bytes32) { + return bytes32("VST"); + } + + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 2; // 2 out of 3 + } + + function aggregateValues(uint256[] memory values) public pure override returns (uint256) { + if (values.length != 2) { + revert InvalidSignersCount(values.length); + } + + _checkCircuitBreaker(values[0]); + _checkCircuitBreaker(values[1]); + + return (values[0] + values[1]) / 2; + } + + function _checkCircuitBreaker(uint256 value) internal pure { + if (value & BIT_MASK_TO_CHECK_CIRCUIT_BREAKER_FLAG > 0) { + revert CircuitBreakerTripped(); + } + } + + function getAuthorisedSignerIndex( + address signerAddress + ) public view virtual override returns (uint8) { + if (signerAddress == 0xf7a873ff07E1d021ae808a28e6862f821148c789) { + return 0; + } else if (signerAddress == 0x827Cc644d3f33d55075354875A961aC8B9EB7Cc8) { + return 1; + } else if (signerAddress == 0x1C31b3eA83F48A6E550938d295893514A9e99Eca) { + return 2; + } else { + revert SignerNotAuthorised(signerAddress); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapterWithoutRoundsMainDemo.sol b/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapterWithoutRoundsMainDemo.sol new file mode 100644 index 0000000..44e658e --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/data-services/VSTPriceFeedsAdapterWithoutRoundsMainDemo.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SinglePriceFeedAdapter} from "../without-rounds/SinglePriceFeedAdapter.sol"; + +contract VSTPriceFeedsAdapterWithoutRoundsMainDemo is SinglePriceFeedAdapter { + function getDataFeedId() public pure override returns (bytes32) { + return bytes32("VST"); + } + + function getUniqueSignersThreshold() public view virtual override returns (uint8) { + return 1; + } + + function getAuthorisedSignerIndex(address signerAddress) + public + view + virtual + override + returns (uint8) + { + if (signerAddress == 0x0C39486f770B26F5527BBBf942726537986Cd7eb) { + return 0; + } else { + revert SignerNotAuthorised(signerAddress); + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeed.sol b/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeed.sol new file mode 100644 index 0000000..5bc60af --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeed.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import {IPriceFeedLegacy} from "./IPriceFeedLegacy.sol"; + +/** + * @title Complete price feed interface + * @author The Redstone Oracles team + * @dev All required public functions that must be implemented + * by each Redstone PriceFeed contract + */ +interface IPriceFeed is IPriceFeedLegacy, AggregatorV3Interface { + /** + * @notice Returns data feed identifier for the PriceFeed contract + * @return dataFeedId The identifier of the data feed + */ + function getDataFeedId() external view returns (bytes32); +} diff --git a/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeedLegacy.sol b/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeedLegacy.sol new file mode 100644 index 0000000..e45c009 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/interfaces/IPriceFeedLegacy.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +/** + * @title Interface with the old Chainlink Price Feed functions + * @author The Redstone Oracles team + * @dev There are some projects (e.g. gmx-contracts) that still + * rely on some legacy functions + */ +interface IPriceFeedLegacy { + /** + * @notice Old Chainlink function for getting the number of latest round + * @return latestRound The number of the latest update round + */ + function latestRound() external view returns (uint80); + + + /** + * @notice Old Chainlink function for getting the latest successfully reported value + * @return latestAnswer The latest successfully reported value + */ + function latestAnswer() external view returns (int256); +} diff --git a/src/on-chain-relayer/contracts/price-feeds/with-rounds/MergedPriceFeedAdapterWithRounds.sol b/src/on-chain-relayer/contracts/price-feeds/with-rounds/MergedPriceFeedAdapterWithRounds.sol new file mode 100644 index 0000000..20138cf --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/with-rounds/MergedPriceFeedAdapterWithRounds.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedBase, PriceFeedWithRounds} from "./PriceFeedWithRounds.sol"; +import {PriceFeedsAdapterBase, PriceFeedsAdapterWithRounds} from "./PriceFeedsAdapterWithRounds.sol"; +import {IRedstoneAdapter} from "../../core/IRedstoneAdapter.sol"; +import {MergedPriceFeedAdapterCommon} from "../MergedPriceFeedAdapterCommon.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +abstract contract MergedPriceFeedAdapterWithRounds is + MergedPriceFeedAdapterCommon, + PriceFeedWithRounds, + PriceFeedsAdapterWithRounds +{ + function initialize() public virtual override(PriceFeedBase, PriceFeedsAdapterBase) initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + function getPriceFeedAdapter() public view virtual override(MergedPriceFeedAdapterCommon, PriceFeedBase) returns (IRedstoneAdapter) { + return super.getPriceFeedAdapter(); + } + + function getDataFeedIds() public view virtual override returns (bytes32[] memory dataFeedIds) { + dataFeedIds = new bytes32[](1); + dataFeedIds[0] = getDataFeedId(); + } + + function getDataFeedIndex(bytes32 dataFeedId) public view virtual override returns (uint256) { + if (dataFeedId == getDataFeedId()) { + return 0; + } else { + revert DataFeedIdNotFound(dataFeedId); + } + } + + function _emitEventAfterSingleValueUpdate(uint256 newValue) internal virtual { + emit AnswerUpdated(SafeCast.toInt256(newValue), getLatestRoundId(), block.timestamp); + } + + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIdsArray, + uint256[] memory values + ) internal virtual override { + if (dataFeedIdsArray.length != 1 || values.length != 1) { + revert CannotUpdateMoreThanOneDataFeed(); + } + PriceFeedsAdapterWithRounds._validateAndUpdateDataFeedsValues(dataFeedIdsArray, values); + _emitEventAfterSingleValueUpdate(values[0]); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedWithRounds.sol b/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedWithRounds.sol new file mode 100644 index 0000000..f757858 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedWithRounds.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterWithRounds} from "./PriceFeedsAdapterWithRounds.sol"; +import {PriceFeedBase} from "../PriceFeedBase.sol"; + +/** + * @title Implementation of a price feed contract with rounds support + * @author The Redstone Oracles team + * @dev This contract is abstract. The actual contract instance + * must implement the following functions: + * - getDataFeedId + * - getPriceFeedAdapter + */ +abstract contract PriceFeedWithRounds is PriceFeedBase { + uint256 internal constant UINT80_MAX = uint256(type(uint80).max); + + error UnsafeUint256ToUint80Conversion(uint256 value); + + function getPriceFeedAdapterWithRounds() public view returns(PriceFeedsAdapterWithRounds) { + return PriceFeedsAdapterWithRounds(address(getPriceFeedAdapter())); + } + + /** + * @notice Old Chainlink function for getting the number of latest round + * @return latestRound The number of the latest successful round + */ + function latestRound() public view override returns (uint80) { + uint256 latestRoundUint256 = getPriceFeedAdapterWithRounds().getLatestRoundId(); + + if (latestRoundUint256 > UINT80_MAX) { + revert UnsafeUint256ToUint80Conversion(latestRoundUint256); + } + + return uint80(latestRoundUint256); + } + + /** + * @notice Returns details for the given round + * @param roundId Requested round identifier + */ + function getRoundData(uint80 requestedRoundId) public view override returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + (uint256 dataFeedValue, uint128 roundDataTimestamp, uint128 roundBlockTimestamp) = getPriceFeedAdapterWithRounds().getRoundDataFromAdapter( + getDataFeedId(), + requestedRoundId + ); + roundId = requestedRoundId; + + if (dataFeedValue > INT256_MAX) { + revert UnsafeUintToIntConversion(dataFeedValue); + } + + answer = int256(dataFeedValue); + startedAt = roundDataTimestamp / 1000; // convert to seconds + updatedAt = roundBlockTimestamp; + + // We want to be compatible with Chainlink's interface + // And in our case the roundId is always equal to answeredInRound + answeredInRound = requestedRoundId; + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedsAdapterWithRounds.sol b/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedsAdapterWithRounds.sol new file mode 100644 index 0000000..3086212 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/with-rounds/PriceFeedsAdapterWithRounds.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterBase} from "../PriceFeedsAdapterBase.sol"; + +/** + * @title Price feeds adapter contract with rounds support + * @author The Redstone Oracles team + * @dev This contract is abstract. The actual contract instance + * must implement the following functions: + * - getDataFeedIds + * - getUniqueSignersThreshold + * - getAuthorisedSignerIndex + * + * We also recommend to override `getDataFeedIndex` function with hardcoded + * values, as it can significantly reduce gas usage + */ +abstract contract PriceFeedsAdapterWithRounds is PriceFeedsAdapterBase { + bytes32 constant VALUES_MAPPING_STORAGE_LOCATION = 0x4dd0c77efa6f6d590c97573d8c70b714546e7311202ff7c11c484cc841d91bfc; // keccak256("RedStone.oracleValuesMapping"); + bytes32 constant ROUND_TIMESTAMPS_MAPPING_STORAGE_LOCATION = 0x207e00944d909d1224f0c253d58489121d736649f8393199f55eecf4f0cf3eb0; // keccak256("RedStone.roundTimestampMapping"); + bytes32 constant LATEST_ROUND_ID_STORAGE_LOCATION = 0xc68d7f1ee07d8668991a8951e720010c9d44c2f11c06b5cac61fbc4083263938; // keccak256("RedStone.latestRoundId"); + + error RoundNotFound(uint256 roundId); + + /** + * @dev Saved new round data to the storage + * @param dataFeedIdsArray Array of all data feeds identifiers + * @param values The reported values that are validated and reported + */ + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIdsArray, + uint256[] memory values + ) internal virtual override { + _incrementLatestRoundId(); + _updatePackedTimestampsForLatestRound(); + + for (uint256 i = 0; i < dataFeedIdsArray.length;) { + _validateAndUpdateDataFeedValue(dataFeedIdsArray[i], values[i]); + unchecked { i++; } // reduces gas costs + } + } + + /** + * @dev Helpful virtual function for handling value validation and updating + * @param dataFeedId The data feed identifier + * @param dataFeedValue Proposed value for the data feed + */ + function _validateAndUpdateDataFeedValue(bytes32 dataFeedId, uint256 dataFeedValue) internal virtual override { + validateDataFeedValueOnWrite(dataFeedId, dataFeedValue); + bytes32 locationInStorage = _getValueLocationInStorage(dataFeedId, getLatestRoundId()); + assembly { + sstore(locationInStorage, dataFeedValue) + } + } + + /** + * @dev [HIGH RISK] Returns the value for a given data feed from the latest round + * without validation. Important! Using this function instead of `getValueForDataFeed` + * may cause significant risk for your smart contracts + * @param dataFeedId The data feed identifier + * @return dataFeedValue Unvalidated value of the latest successful update + */ + function getValueForDataFeedUnsafe(bytes32 dataFeedId) public view override returns (uint256 dataFeedValue) { + return getValueForDataFeedAndRound(dataFeedId, getLatestRoundId()); + } + + /** + * @dev [HIGH RISK] Returns value for the requested data feed from the given round + * without validation. + * @param dataFeedId The data feed identifier + * @param roundId The number of the requested round + * @return dataFeedValue value for the requested data feed from the given round + */ + function getValueForDataFeedAndRound(bytes32 dataFeedId, uint256 roundId) public view returns (uint256 dataFeedValue) { + bytes32 locationInStorage = _getValueLocationInStorage(dataFeedId, roundId); + assembly { + dataFeedValue := sload(locationInStorage) + } + } + + + /** + * @notice Returns data from the latest successful round + * @return latestRoundId + * @return latestRoundDataTimestamp + * @return latestRoundBlockTimestamp + */ + function getLatestRoundParams() public view returns ( uint256 latestRoundId, uint128 latestRoundDataTimestamp, uint128 latestRoundBlockTimestamp) { + latestRoundId = getLatestRoundId(); + uint256 packedRoundTimestamps = getPackedTimestampsForRound(latestRoundId); + (latestRoundDataTimestamp, latestRoundBlockTimestamp) = _unpackTimestamps( + packedRoundTimestamps + ); + } + + + /** + * @notice Returns details for the given round and data feed + * @param dataFeedId Requested data feed + * @param roundId Requested round identifier + * @return dataFeedValue + * @return roundDataTimestamp + * @return roundBlockTimestamp + */ + function getRoundDataFromAdapter(bytes32 dataFeedId, uint256 roundId) public view returns (uint256 dataFeedValue, uint128 roundDataTimestamp, uint128 roundBlockTimestamp) { + if (roundId > getLatestRoundId() || roundId == 0) { + revert RoundNotFound(roundId); + } + + dataFeedValue = getValueForDataFeedAndRound(dataFeedId, roundId); + validateDataFeedValueOnRead(dataFeedId, dataFeedValue); + uint256 packedRoundTimestamps = getPackedTimestampsForRound(roundId); + (roundDataTimestamp, roundBlockTimestamp) = _unpackTimestamps(packedRoundTimestamps); + } + + + /** + * @dev Helpful function for getting storage location for requested value + * @param dataFeedId Requested data feed identifier + * @param roundId Requested round number + * @return locationInStorage + */ + function _getValueLocationInStorage(bytes32 dataFeedId, uint256 roundId) private pure returns (bytes32) { + return keccak256(abi.encode(dataFeedId, roundId, VALUES_MAPPING_STORAGE_LOCATION)); + } + + + /** + * @dev Helpful function for getting storage location for round timestamps + * @param roundId Requested round number + * @return locationInStorage + */ + function _getRoundTimestampsLocationInStorage(uint256 roundId) private pure returns (bytes32) { + return keccak256(abi.encode(roundId, ROUND_TIMESTAMPS_MAPPING_STORAGE_LOCATION)); + } + + + /** + * @notice Returns latest successful round number + * @return latestRoundId + */ + function getLatestRoundId() public view returns (uint256 latestRoundId) { + assembly { + latestRoundId := sload(LATEST_ROUND_ID_STORAGE_LOCATION) + } + } + + /** + * @dev Helpful function for incrementing the latest round number by 1 in + * the contract storage + */ + function _incrementLatestRoundId() private { + uint256 latestRoundId = getLatestRoundId(); + assembly { + sstore(LATEST_ROUND_ID_STORAGE_LOCATION, add(latestRoundId, 1)) + } + } + + /** + * @notice Returns timestamps related to the given round packed into one number + * @param roundId Requested round number + * @return roundTimestamps + */ + function getPackedTimestampsForRound(uint256 roundId) public view returns (uint256 roundTimestamps) { + bytes32 locationInStorage = _getRoundTimestampsLocationInStorage(roundId); + assembly { + roundTimestamps := sload(locationInStorage) + } + } + + + /** + * @dev Saves packed timestamps (data and block.timestamp) in the contract storage + */ + function _updatePackedTimestampsForLatestRound() private { + uint256 packedTimestamps = getPackedTimestampsFromLatestUpdate(); + uint256 latestRoundId = getLatestRoundId(); + bytes32 locationInStorage = _getRoundTimestampsLocationInStorage(latestRoundId); + assembly { + sstore(locationInStorage, packedTimestamps) + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedPriceFeedAdapterWithoutRounds.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedPriceFeedAdapterWithoutRounds.sol new file mode 100644 index 0000000..1ee201a --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedPriceFeedAdapterWithoutRounds.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterBase, PriceFeedsAdapterWithoutRounds} from "./PriceFeedsAdapterWithoutRounds.sol"; +import {PriceFeedBase, PriceFeedWithoutRounds} from "./PriceFeedWithoutRounds.sol"; +import {IRedstoneAdapter} from "../../core/IRedstoneAdapter.sol"; +import {MergedPriceFeedAdapterCommon} from "../MergedPriceFeedAdapterCommon.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +abstract contract MergedPriceFeedAdapterWithoutRounds is + MergedPriceFeedAdapterCommon, + PriceFeedsAdapterWithoutRounds, + PriceFeedWithoutRounds +{ + + function initialize() public override(PriceFeedBase, PriceFeedsAdapterBase) initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + function getPriceFeedAdapter() public view virtual override(MergedPriceFeedAdapterCommon, PriceFeedBase) returns (IRedstoneAdapter) { + return super.getPriceFeedAdapter(); + } + + function getDataFeedIds() public view virtual override returns (bytes32[] memory dataFeedIds) { + dataFeedIds = new bytes32[](1); + dataFeedIds[0] = getDataFeedId(); + } + + function getDataFeedIndex(bytes32 dataFeedId) public view virtual override returns (uint256) { + if (dataFeedId == getDataFeedId()) { + return 0; + } else { + revert DataFeedIdNotFound(dataFeedId); + } + } + + function _emitEventAfterSingleValueUpdate(uint256 newValue) internal virtual { + emit AnswerUpdated(SafeCast.toInt256(newValue), latestRound(), block.timestamp); + } + + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIdsArray, + uint256[] memory values + ) internal virtual override { + if (dataFeedIdsArray.length != 1 || values.length != 1) { + revert CannotUpdateMoreThanOneDataFeed(); + } + _validateAndUpdateDataFeedValue(dataFeedIdsArray[0], values[0]); + _emitEventAfterSingleValueUpdate(values[0]); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedSinglePriceFeedAdapterWithoutRounds.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedSinglePriceFeedAdapterWithoutRounds.sol new file mode 100644 index 0000000..cee8a87 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/MergedSinglePriceFeedAdapterWithoutRounds.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.14; + +import {SinglePriceFeedAdapter} from "./SinglePriceFeedAdapter.sol"; +import {PriceFeedsAdapterBase} from "./PriceFeedsAdapterWithoutRounds.sol"; +import {PriceFeedBase, PriceFeedWithoutRounds} from "./PriceFeedWithoutRounds.sol"; +import {IRedstoneAdapter} from "../../core/IRedstoneAdapter.sol"; +import {MergedPriceFeedAdapterCommon} from "../MergedPriceFeedAdapterCommon.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +abstract contract MergedSinglePriceFeedAdapterWithoutRounds is + MergedPriceFeedAdapterCommon, + SinglePriceFeedAdapter, + PriceFeedWithoutRounds +{ + + function getDataFeedId() public view virtual override(SinglePriceFeedAdapter, PriceFeedBase) returns (bytes32); + + function initialize() public override(PriceFeedBase, PriceFeedsAdapterBase) initializer { + // We don't have storage variables, but we keep this function + // Because it is used for contract setup in upgradable contracts + } + + function getPriceFeedAdapter() public view virtual override(MergedPriceFeedAdapterCommon, PriceFeedBase) returns (IRedstoneAdapter) { + return super.getPriceFeedAdapter(); + } + + function _emitEventAfterSingleValueUpdate(uint256 newValue) internal virtual { + emit AnswerUpdated(SafeCast.toInt256(newValue), latestRound(), block.timestamp); + } + + function _validateAndUpdateDataFeedsValues( + bytes32[] memory dataFeedIdsArray, + uint256[] memory values + ) internal virtual override { + if (dataFeedIdsArray.length != 1 || values.length != 1) { + revert CannotUpdateMoreThanOneDataFeed(); + } + _validateAndUpdateDataFeedValue(dataFeedIdsArray[0], values[0]); + _emitEventAfterSingleValueUpdate(values[0]); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedWithoutRounds.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedWithoutRounds.sol new file mode 100644 index 0000000..3291f8b --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedWithoutRounds.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.14; + +import {PriceFeedBase} from "../PriceFeedBase.sol"; + +/** + * @title Implementation of a price feed contract without rounds support + * @author The Redstone Oracles team + * @dev This contract is abstract. The actual contract instance + * must implement the following functions: + * - getDataFeedId + * - getPriceFeedAdapter + */ +abstract contract PriceFeedWithoutRounds is PriceFeedBase { + uint80 constant DEFAULT_ROUND = 1; + + error GetRoundDataCanBeOnlyCalledWithLatestRound(uint80 requestedRoundId); + + /** + * @dev We always return 1, since we do not support rounds in this contract + */ + function latestRound() public pure override returns (uint80) { + return DEFAULT_ROUND; + } + + /** + * @dev There are possible use cases that some contracts don't need values from old rounds + * but still rely on `getRoundData` or `latestRounud` functions + */ + function getRoundData(uint80 requestedRoundId) public view override returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { + if (requestedRoundId != latestRound()) { + revert GetRoundDataCanBeOnlyCalledWithLatestRound(requestedRoundId); + } + return latestRoundData(); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedsAdapterWithoutRounds.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedsAdapterWithoutRounds.sol new file mode 100644 index 0000000..7d1c55d --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/PriceFeedsAdapterWithoutRounds.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterBase} from "../PriceFeedsAdapterBase.sol"; + +/** + * @title Implementation of a price feeds adapter without rounds support + * @author The Redstone Oracles team + * @dev This contract is abstract, the following functions should be + * implemented in the actual contract before deployment: + * - getDataFeedIds + * - getUniqueSignersThreshold + * - getAuthorisedSignerIndex + * + * We also recommend to override `getDataFeedIndex` function with hardcoded + * values, as it can significantly reduce gas usage + */ +abstract contract PriceFeedsAdapterWithoutRounds is PriceFeedsAdapterBase { + bytes32 constant VALUES_MAPPING_STORAGE_LOCATION = 0x4dd0c77efa6f6d590c97573d8c70b714546e7311202ff7c11c484cc841d91bfc; // keccak256("RedStone.oracleValuesMapping"); + + /** + * @dev Helpful virtual function for handling value validation and saving + * @param dataFeedId The data feed identifier + * @param dataFeedValue Proposed value for the data feed + */ + function _validateAndUpdateDataFeedValue(bytes32 dataFeedId, uint256 dataFeedValue) internal override virtual { + validateDataFeedValueOnWrite(dataFeedId, dataFeedValue); + bytes32 locationInStorage = _getValueLocationInStorage(dataFeedId); + assembly { + sstore(locationInStorage, dataFeedValue) + } + } + + /** + * @dev [HIGH RISK] Returns the latest value for a given data feed without validation + * Important! Using this function instead of `getValueForDataFeed` may cause + * significant risk for your smart contracts + * @param dataFeedId The data feed identifier + * @return dataFeedValue Unvalidated value of the latest successful update + */ + function getValueForDataFeedUnsafe(bytes32 dataFeedId) public view virtual override returns (uint256 dataFeedValue) { + bytes32 locationInStorage = _getValueLocationInStorage(dataFeedId); + assembly { + dataFeedValue := sload(locationInStorage) + } + } + + /** + * @dev Helpful function for getting storage location for the requested data feed + * @param dataFeedId Requested data feed identifier + * @return locationInStorage + */ + function _getValueLocationInStorage(bytes32 dataFeedId) private pure returns (bytes32) { + return keccak256(abi.encode(dataFeedId, VALUES_MAPPING_STORAGE_LOCATION)); + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapter.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapter.sol new file mode 100644 index 0000000..e4470d9 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapter.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {PriceFeedsAdapterBase} from "../PriceFeedsAdapterBase.sol"; + +/** + * @title Price feed adapter for one specific data feed without rounds support + * @author The Redstone Oracles team + * @dev This version works only with a single data feed. It's abstract and + * the following functions should be implemented in the actual contract + * before deployment: + * - getDataFeedId + * - getUniqueSignersThreshold + * - getAuthorisedSignerIndex + * + * This contract stores the value along with timestamps in a single storage slot + * 32 bytes = 6 bytes (Data timestamp ) + 6 bytes (Block timestamp) + 20 bytes (Value) + */ +abstract contract SinglePriceFeedAdapter is PriceFeedsAdapterBase { + + bytes32 internal constant DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION = 0x632f4a585e47073d66129e9ebce395c9b39d8a1fc5b15d4d7df2e462fb1fccfa; // keccak256("RedStone.singlePriceFeedAdapter"); + uint256 internal constant MAX_VALUE_WITH_20_BYTES = 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff; + uint256 internal constant BIT_MASK_TO_CLEAR_LAST_20_BYTES = 0xffffffffffffffffffffffff0000000000000000000000000000000000000000; + uint256 internal constant MAX_NUMBER_FOR_48_BITS = 0x0000000000000000000000000000000000000000000000000000ffffffffffff; + + error DataFeedValueTooBig(uint256 valueForDataFeed); + + /** + * @notice Returns the only data feed identifer supported by the adapter + * @dev This function should be overriden in the derived contracts, + * but `getDataFeedIds` and `getDataFeedIndex` should not (and can not) + * @return dataFeedId The only data feed identifer supported by the adapter + */ + function getDataFeedId() public view virtual returns (bytes32); + + /** + * @notice Returns identifiers of all data feeds supported by the Adapter contract + * In this case - an array with only one element + * @return dataFeedIds + */ + function getDataFeedIds() public view virtual override returns (bytes32[] memory dataFeedIds) { + dataFeedIds = new bytes32[](1); + dataFeedIds[0] = getDataFeedId(); + } + + /** + * @dev Returns 0 if dataFeedId is the one, otherwise reverts + * @param dataFeedId The identifier of the requested data feed + */ + function getDataFeedIndex(bytes32 dataFeedId) public virtual view override returns(uint256) { + if (dataFeedId == getDataFeedId()) { + return 0; + } else { + revert DataFeedIdNotFound(dataFeedId); + } + } + + /** + * @dev Reverts if proposed value for the proposed data feed id is invalid + * By default, it checks if the value is not equal to 0 and if it fits to 20 bytes + * Because other 12 bytes are used for storing the packed timestamps + * @param dataFeedId The data feed identifier + * @param valueForDataFeed Proposed value for the data feed + */ + function validateDataFeedValueOnWrite(bytes32 dataFeedId, uint256 valueForDataFeed) public view virtual override { + super.validateDataFeedValueOnWrite(dataFeedId, valueForDataFeed); + if (valueForDataFeed > MAX_VALUE_WITH_20_BYTES) { + revert DataFeedValueTooBig(valueForDataFeed); + } + } + + /** + * @dev [HIGH RISK] Returns the latest value for a given data feed without validation + * Important! Using this function instead of `getValueForDataFeed` may cause + * significant risk for your smart contracts + * @return dataFeedValue Unvalidated value of the latest successful update + */ + function getValueForDataFeedUnsafe(bytes32) public view virtual override returns (uint256 dataFeedValue) { + uint160 dataFeedValueCompressed; + assembly { + dataFeedValueCompressed := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + } + return dataFeedValueCompressed; + } + + /** + * @notice Returns timestamps of the latest successful update + * @dev Timestamps here use only 6 bytes each and are packed together with the value + * @return dataTimestamp timestamp (usually in milliseconds) from the signed data packages + * @return blockTimestamp timestamp of the block when the update has happened + */ + function getTimestampsFromLatestUpdate() public view virtual override returns (uint128 dataTimestamp, uint128 blockTimestamp) { + uint256 latestUpdateDetails; + assembly { + latestUpdateDetails := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + } + dataTimestamp = uint128(latestUpdateDetails >> 208); // first 48 bits + blockTimestamp = uint128((latestUpdateDetails << 48) >> 208); // next 48 bits + } + + /** + * @dev Validates and saves the value in the contract storage + * It uses only 20 right bytes of the corresponding storage slot + * @param dataFeedId The data feed identifier + * @param dataFeedValue Proposed value for the data feed + */ + function _validateAndUpdateDataFeedValue(bytes32 dataFeedId, uint256 dataFeedValue) internal virtual override { + validateDataFeedValueOnWrite(dataFeedId, dataFeedValue); + assembly { + let curValueFromStorage := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + curValueFromStorage := and(curValueFromStorage, BIT_MASK_TO_CLEAR_LAST_20_BYTES) // clear dataFeedValue bits + curValueFromStorage := or(curValueFromStorage, dataFeedValue) + sstore(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION, curValueFromStorage) + } + } + + /** + * @dev Helpful function that packs and saves timestamps in the 12 left bytes of the + * storage slot reserved for storing details about the latest update + * @param dataPackagesTimestamp Timestamp from the signed data packages, + * extracted from the RedStone payload in calldata + */ + function _saveTimestampsOfCurrentUpdate(uint256 dataPackagesTimestamp) internal virtual override { + uint256 blockTimestamp = getBlockTimestamp(); + + if (dataPackagesTimestamp > MAX_NUMBER_FOR_48_BITS) { + revert DataTimestampIsTooBig(dataPackagesTimestamp); + } + + if (blockTimestamp > MAX_NUMBER_FOR_48_BITS) { + revert BlockTimestampIsTooBig(blockTimestamp); + } + + uint256 timestampsPacked = dataPackagesTimestamp << 208; // 48 first bits for dataPackagesTimestamp + timestampsPacked |= (blockTimestamp << 160); // 48 next bits for blockTimestamp + assembly { + let latestUpdateDetails := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + latestUpdateDetails := and(latestUpdateDetails, MAX_VALUE_WITH_20_BYTES) // clear timestamp bits + sstore(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION, or(latestUpdateDetails, timestampsPacked)) + } + } +} diff --git a/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapterWithClearing.sol b/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapterWithClearing.sol new file mode 100644 index 0000000..8490462 --- /dev/null +++ b/src/on-chain-relayer/contracts/price-feeds/without-rounds/SinglePriceFeedAdapterWithClearing.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SinglePriceFeedAdapter} from "./SinglePriceFeedAdapter.sol"; + +/** + * @title [HIGH RISK] Price feed adapter for one specific data feed without + * rounds support, with storage clearing feature + * @author The Redstone Oracles team + * @dev This contract has a significant security risk, as it allows to + * update oracle data with older timestamps then the previous one. It can + * open many opportunities for attackers to manipulate the values and use it + * for arbitrage. Use it only if you know what you are doing very well + */ +abstract contract SinglePriceFeedAdapterWithClearing is SinglePriceFeedAdapter { + + bytes32 internal constant TEMP_DATA_TIMESTAMP_STORAGE_LOCATION = 0x9ba2e81f7980c774323961547312ae2319fc1970bb8ec60c86c869e9a1c1c0d2; // keccak256("RedStone.tempDataTimestampStorageLocation"); + uint256 internal constant MAX_VALUE_WITH_26_BYTES = 0x000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffff; + uint256 internal constant BIT_MASK_TO_CLEAR_LAST_26_BYTES = 0xffffffffffff0000000000000000000000000000000000000000000000000000; + + function validateDataFeedValueOnWrite(bytes32 dataFeedId, uint256 valueForDataFeed) public view virtual override { + super.validateDataFeedValueOnWrite(dataFeedId, valueForDataFeed); + if (valueForDataFeed > MAX_VALUE_WITH_26_BYTES) { + revert DataFeedValueTooBig(valueForDataFeed); + } + } + + function getValueForDataFeedUnsafe(bytes32) public view override virtual returns (uint256 dataFeedValue) { + uint208 dataFeedValueCompressed; + assembly { + dataFeedValueCompressed := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + } + return uint256(dataFeedValueCompressed); + } + + function getTimestampsFromLatestUpdate() public view override virtual returns (uint128 dataTimestamp, uint128 blockTimestamp) { + uint256 latestUpdateDetails; + assembly { + latestUpdateDetails := sload(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION) + } + blockTimestamp = uint128(latestUpdateDetails >> 208); // first 48 bits + dataTimestamp = blockTimestamp * 1000; // It's a hack, because we don't store dataTimestamp in storage in this version of adapter + } + + function getDataTimestampFromLatestUpdate() public view virtual override returns (uint256 lastDataTimestamp) { + assembly { + lastDataTimestamp := sload(TEMP_DATA_TIMESTAMP_STORAGE_LOCATION) + } + } + + function _validateAndUpdateDataFeedValue(bytes32 dataFeedId, uint256 dataFeedValue) virtual internal override { + validateDataFeedValueOnWrite(dataFeedId, dataFeedValue); + uint256 blockTimestampCompressedAndShifted = getBlockTimestamp() << 208; // Move value to the first 48 bits + assembly { + // Save timestamp and data feed value + let timestampAndValue := or(blockTimestampCompressedAndShifted, dataFeedValue) + sstore(DATA_FROM_LATEST_UPDATE_STORAGE_LOCATION, timestampAndValue) + + // Clear temp data timestamp, it refunds 19.9k gas + sstore(TEMP_DATA_TIMESTAMP_STORAGE_LOCATION, 0) + } + } + + function _saveTimestampsOfCurrentUpdate(uint256 dataPackagesTimestamp) virtual internal override { + assembly { + sstore(TEMP_DATA_TIMESTAMP_STORAGE_LOCATION, dataPackagesTimestamp) + } + } +} diff --git a/src/on-chain-relayer/contracts/sample/SampleDeviationLib.sol b/src/on-chain-relayer/contracts/sample/SampleDeviationLib.sol new file mode 100644 index 0000000..fd8759e --- /dev/null +++ b/src/on-chain-relayer/contracts/sample/SampleDeviationLib.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.14; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +library SampleDeviationLib { + function calculateAbsDeviation( + uint256 proposedValue, + uint256 originalValue, + uint256 precision + ) external pure returns (uint256) { + int256 originalValueAsInt = SafeCast.toInt256(originalValue); + return + abs( + ((SafeCast.toInt256(proposedValue) - originalValueAsInt) * + 100 * + SafeCast.toInt256(precision)) / originalValueAsInt + ); + } + + function abs(int256 x) private pure returns (uint256) { + return SafeCast.toUint256(x >= 0 ? x : -x); + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}