From 51e63e5aeaedbb85097d194d956b2f969be3b21a Mon Sep 17 00:00:00 2001 From: Mikhailo Shabodyash Date: Mon, 1 Jul 2024 13:38:48 +0300 Subject: [PATCH 1/3] feat: new pricefeeds --- .../pricefeeds/RateBasedScalingPriceFeed.sol | 90 +++++++++++++++++++ .../pricefeeds/rsETHScalingPriceFeed.sol | 87 ++++++++++++++++++ contracts/vendor/kelp/IrsETHOracle.sol | 6 ++ 3 files changed, 183 insertions(+) create mode 100644 contracts/pricefeeds/RateBasedScalingPriceFeed.sol create mode 100644 contracts/pricefeeds/rsETHScalingPriceFeed.sol create mode 100644 contracts/vendor/kelp/IrsETHOracle.sol diff --git a/contracts/pricefeeds/RateBasedScalingPriceFeed.sol b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol new file mode 100644 index 000000000..502f5b084 --- /dev/null +++ b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "../IPriceFeed.sol"; + +interface IRateProvider { + function getRate() external view returns (uint256); +} + +/** + * @title Scaling price feed for rate based oracles + * @notice A custom price feed that scales up or down the price received from an underlying price feed and returns the result + * @author Compound + */ +contract RateBasedScalingPriceFeed is IPriceFeed { + /** Custom errors **/ + error InvalidInt256(); + + /// @notice Version of the price feed + uint public constant override version = 1; + + /// @notice Description of the price feed + string public description; + + /// @notice Number of decimals for returned prices + uint8 public immutable override decimals; + + /// @notice Underlying price feed where prices are fetched from + address public immutable underlyingPriceFeed; + + /// @notice Whether or not the price should be upscaled + bool internal immutable shouldUpscale; + + /// @notice The amount to upscale or downscale the price by + int256 internal immutable rescaleFactor; + + /** + * @notice Construct a new scaling price feed + * @param underlyingPriceFeed_ The address of the underlying price feed to fetch prices from + * @param decimals_ The number of decimals for the returned prices + **/ + constructor(address underlyingPriceFeed_, uint8 decimals_, uint8 underlyingDecimals_, string memory description_) { + underlyingPriceFeed = underlyingPriceFeed_; + decimals = decimals_; + description = description_; + + uint8 priceFeedDecimals = underlyingDecimals_; + // Note: Solidity does not allow setting immutables in if/else statements + shouldUpscale = priceFeedDecimals < decimals_ ? true : false; + rescaleFactor = (shouldUpscale + ? signed256(10 ** (decimals_ - priceFeedDecimals)) + : signed256(10 ** (priceFeedDecimals - decimals_)) + ); + } + + /** + * @notice Price for the latest round + * @return roundId Round id from the underlying price feed + * @return answer Latest price for the asset in terms of ETH + * @return startedAt Timestamp when the round was started; passed on from underlying price feed + * @return updatedAt Timestamp when the round was last updated; passed on from underlying price feed + * @return answeredInRound Round id in which the answer was computed; passed on from underlying price feed + **/ + function latestRoundData() override external view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + uint256 rate = IRateProvider(underlyingPriceFeed).getRate(); + return (roundId, scalePrice(int256(rate)), startedAt, updatedAt, answeredInRound); + } + + function signed256(uint256 n) internal pure returns (int256) { + if (n > uint256(type(int256).max)) revert InvalidInt256(); + return int256(n); + } + + function scalePrice(int256 price) internal view returns (int256) { + int256 scaledPrice; + if (shouldUpscale) { + scaledPrice = price * rescaleFactor; + } else { + scaledPrice = price / rescaleFactor; + } + return scaledPrice; + } +} \ No newline at end of file diff --git a/contracts/pricefeeds/rsETHScalingPriceFeed.sol b/contracts/pricefeeds/rsETHScalingPriceFeed.sol new file mode 100644 index 000000000..506115d47 --- /dev/null +++ b/contracts/pricefeeds/rsETHScalingPriceFeed.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "../vendor/kelp/IrsETHOracle.sol"; +import "../IPriceFeed.sol"; + +/** + * @title Scaling price feed for rsETH + * @notice A custom price feed that scales up or down the price received from an underlying Kelp price feed and returns the result + * @author Compound + */ +contract rsETHScalingPriceFeed is IPriceFeed { + /** Custom errors **/ + error InvalidInt256(); + + /// @notice Version of the price feed + uint public constant override version = 1; + + /// @notice Description of the price feed + string public description; + + /// @notice Number of decimals for returned prices + uint8 public immutable override decimals; + + /// @notice Underlying Kelp price feed where prices are fetched from + address public immutable underlyingPriceFeed; + + /// @notice Whether or not the price should be upscaled + bool internal immutable shouldUpscale; + + /// @notice The amount to upscale or downscale the price by + int256 internal immutable rescaleFactor; + + /** + * @notice Construct a new scaling price feed + * @param underlyingPriceFeed_ The address of the underlying price feed to fetch prices from + * @param decimals_ The number of decimals for the returned prices + **/ + constructor(address underlyingPriceFeed_, uint8 decimals_, string memory description_) { + underlyingPriceFeed = underlyingPriceFeed_; + decimals = decimals_; + description = description_; + + uint8 kelpPriceFeedDecimals = 18; + // Note: Solidity does not allow setting immutables in if/else statements + shouldUpscale = kelpPriceFeedDecimals < decimals_ ? true : false; + rescaleFactor = (shouldUpscale + ? signed256(10 ** (decimals_ - kelpPriceFeedDecimals)) + : signed256(10 ** (kelpPriceFeedDecimals - decimals_)) + ); + } + + /** + * @notice Price for the latest round + * @return roundId Round id from the underlying price feed + * @return answer Latest price for the asset in terms of ETH + * @return startedAt Timestamp when the round was started; passed on from underlying price feed + * @return updatedAt Timestamp when the round was last updated; passed on from underlying price feed + * @return answeredInRound Round id in which the answer was computed; passed on from underlying price feed + **/ + function latestRoundData() override external view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + int256 price = int256(IrsETHOracle(underlyingPriceFeed).rsETHPrice()); + return (roundId, scalePrice(price), startedAt, updatedAt, answeredInRound); + } + + function signed256(uint256 n) internal pure returns (int256) { + if (n > uint256(type(int256).max)) revert InvalidInt256(); + return int256(n); + } + + function scalePrice(int256 price) internal view returns (int256) { + int256 scaledPrice; + if (shouldUpscale) { + scaledPrice = price * rescaleFactor; + } else { + scaledPrice = price / rescaleFactor; + } + return scaledPrice; + } +} \ No newline at end of file diff --git a/contracts/vendor/kelp/IrsETHOracle.sol b/contracts/vendor/kelp/IrsETHOracle.sol new file mode 100644 index 000000000..7b462be7c --- /dev/null +++ b/contracts/vendor/kelp/IrsETHOracle.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +interface IrsETHOracle { + function rsETHPrice() external view returns (uint256); +} From 2c339853e1afe6d75f1f44fa689908fa23c74705 Mon Sep 17 00:00:00 2001 From: Mikhailo Shabodyash Date: Mon, 1 Jul 2024 14:11:48 +0300 Subject: [PATCH 2/3] fix: rename --- contracts/IRateProvider.sol | 6 ++++++ contracts/pricefeeds/RateBasedScalingPriceFeed.sol | 5 +---- ...calingPriceFeed.sol => RsETHScalingPriceFeed.sol} | 12 ++++++------ .../vendor/kelp/{IrsETHOracle.sol => ILRTOracle.sol} | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 contracts/IRateProvider.sol rename contracts/pricefeeds/{rsETHScalingPriceFeed.sol => RsETHScalingPriceFeed.sol} (88%) rename contracts/vendor/kelp/{IrsETHOracle.sol => ILRTOracle.sol} (83%) diff --git a/contracts/IRateProvider.sol b/contracts/IRateProvider.sol new file mode 100644 index 000000000..1a9fbcc10 --- /dev/null +++ b/contracts/IRateProvider.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +interface IRateProvider { + function getRate() external view returns (uint256); +} diff --git a/contracts/pricefeeds/RateBasedScalingPriceFeed.sol b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol index 502f5b084..1532a3f85 100644 --- a/contracts/pricefeeds/RateBasedScalingPriceFeed.sol +++ b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol @@ -3,10 +3,7 @@ pragma solidity 0.8.15; import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; import "../IPriceFeed.sol"; - -interface IRateProvider { - function getRate() external view returns (uint256); -} +import "../IRateProvider.sol"; /** * @title Scaling price feed for rate based oracles diff --git a/contracts/pricefeeds/rsETHScalingPriceFeed.sol b/contracts/pricefeeds/RsETHScalingPriceFeed.sol similarity index 88% rename from contracts/pricefeeds/rsETHScalingPriceFeed.sol rename to contracts/pricefeeds/RsETHScalingPriceFeed.sol index 506115d47..638a81803 100644 --- a/contracts/pricefeeds/rsETHScalingPriceFeed.sol +++ b/contracts/pricefeeds/RsETHScalingPriceFeed.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; -import "../vendor/kelp/IrsETHOracle.sol"; +import "../vendor/kelp/ILRTOracle.sol"; import "../IPriceFeed.sol"; /** @@ -42,12 +42,12 @@ contract rsETHScalingPriceFeed is IPriceFeed { decimals = decimals_; description = description_; - uint8 kelpPriceFeedDecimals = 18; + uint8 underlyingPriceFeedDecimals = 18; // Note: Solidity does not allow setting immutables in if/else statements - shouldUpscale = kelpPriceFeedDecimals < decimals_ ? true : false; + shouldUpscale = underlyingPriceFeedDecimals < decimals_ ? true : false; rescaleFactor = (shouldUpscale - ? signed256(10 ** (decimals_ - kelpPriceFeedDecimals)) - : signed256(10 ** (kelpPriceFeedDecimals - decimals_)) + ? signed256(10 ** (decimals_ - underlyingPriceFeedDecimals)) + : signed256(10 ** (underlyingPriceFeedDecimals - decimals_)) ); } @@ -66,7 +66,7 @@ contract rsETHScalingPriceFeed is IPriceFeed { uint256 updatedAt, uint80 answeredInRound ) { - int256 price = int256(IrsETHOracle(underlyingPriceFeed).rsETHPrice()); + int256 price = int256(ILRTOracle(underlyingPriceFeed).rsETHPrice()); return (roundId, scalePrice(price), startedAt, updatedAt, answeredInRound); } diff --git a/contracts/vendor/kelp/IrsETHOracle.sol b/contracts/vendor/kelp/ILRTOracle.sol similarity index 83% rename from contracts/vendor/kelp/IrsETHOracle.sol rename to contracts/vendor/kelp/ILRTOracle.sol index 7b462be7c..9b194a259 100644 --- a/contracts/vendor/kelp/IrsETHOracle.sol +++ b/contracts/vendor/kelp/ILRTOracle.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.15; -interface IrsETHOracle { +interface ILRTOracle { function rsETHPrice() external view returns (uint256); } From 80f08d17269868012ba7ee234ebc7a696f08c287 Mon Sep 17 00:00:00 2001 From: Mikhailo Shabodyash Date: Tue, 9 Jul 2024 11:39:39 +0300 Subject: [PATCH 3/3] fix: post audit fixes --- .../pricefeeds/RateBasedScalingPriceFeed.sol | 14 ++++++++++++-- contracts/pricefeeds/RsETHScalingPriceFeed.sol | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/contracts/pricefeeds/RateBasedScalingPriceFeed.sol b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol index 1532a3f85..34bf92096 100644 --- a/contracts/pricefeeds/RateBasedScalingPriceFeed.sol +++ b/contracts/pricefeeds/RateBasedScalingPriceFeed.sol @@ -13,9 +13,10 @@ import "../IRateProvider.sol"; contract RateBasedScalingPriceFeed is IPriceFeed { /** Custom errors **/ error InvalidInt256(); + error BadDecimals(); /// @notice Version of the price feed - uint public constant override version = 1; + uint public constant VERSION = 1; /// @notice Description of the price feed string public description; @@ -39,6 +40,7 @@ contract RateBasedScalingPriceFeed is IPriceFeed { **/ constructor(address underlyingPriceFeed_, uint8 decimals_, uint8 underlyingDecimals_, string memory description_) { underlyingPriceFeed = underlyingPriceFeed_; + if (decimals_ > 18) revert BadDecimals(); decimals = decimals_; description = description_; @@ -67,7 +69,7 @@ contract RateBasedScalingPriceFeed is IPriceFeed { uint80 answeredInRound ) { uint256 rate = IRateProvider(underlyingPriceFeed).getRate(); - return (roundId, scalePrice(int256(rate)), startedAt, updatedAt, answeredInRound); + return (1, scalePrice(signed256(rate)), block.timestamp, block.timestamp, 1); } function signed256(uint256 n) internal pure returns (int256) { @@ -84,4 +86,12 @@ contract RateBasedScalingPriceFeed is IPriceFeed { } return scaledPrice; } + + /** + * @notice Contract version + * @return The version of the price feed contract + **/ + function version() external pure returns (uint256) { + return VERSION; + } } \ No newline at end of file diff --git a/contracts/pricefeeds/RsETHScalingPriceFeed.sol b/contracts/pricefeeds/RsETHScalingPriceFeed.sol index 638a81803..106b1c130 100644 --- a/contracts/pricefeeds/RsETHScalingPriceFeed.sol +++ b/contracts/pricefeeds/RsETHScalingPriceFeed.sol @@ -13,9 +13,10 @@ import "../IPriceFeed.sol"; contract rsETHScalingPriceFeed is IPriceFeed { /** Custom errors **/ error InvalidInt256(); + error BadDecimals(); /// @notice Version of the price feed - uint public constant override version = 1; + uint public constant VERSION = 1; /// @notice Description of the price feed string public description; @@ -39,6 +40,7 @@ contract rsETHScalingPriceFeed is IPriceFeed { **/ constructor(address underlyingPriceFeed_, uint8 decimals_, string memory description_) { underlyingPriceFeed = underlyingPriceFeed_; + if (decimals_ > 18) revert BadDecimals(); decimals = decimals_; description = description_; @@ -66,8 +68,8 @@ contract rsETHScalingPriceFeed is IPriceFeed { uint256 updatedAt, uint80 answeredInRound ) { - int256 price = int256(ILRTOracle(underlyingPriceFeed).rsETHPrice()); - return (roundId, scalePrice(price), startedAt, updatedAt, answeredInRound); + int256 price = signed256(ILRTOracle(underlyingPriceFeed).rsETHPrice()); + return (1, scalePrice(price), block.timestamp, block.timestamp, 1); } function signed256(uint256 n) internal pure returns (int256) { @@ -84,4 +86,12 @@ contract rsETHScalingPriceFeed is IPriceFeed { } return scaledPrice; } + + /** + * @notice Contract version + * @return The version of the price feed contract + **/ + function version() external pure returns (uint256) { + return VERSION; + } } \ No newline at end of file