-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add curve twap price feed and constant price feed #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2025. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import {LibString} from "@solady/utils/LibString.sol"; | ||
| import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; | ||
| import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import {IncorrectPriceException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; | ||
|
|
||
| /// @title Constant price feed | ||
| /// @notice A simple price feed that returns a constant value set in the constructor | ||
| contract ConstantPriceFeed is IPriceFeed, SanityCheckTrait { | ||
| using LibString for string; | ||
| using LibString for bytes32; | ||
| /// @notice Contract version | ||
|
|
||
| uint256 public constant override version = 3_10; | ||
Van0k marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// @notice Contract type | ||
| bytes32 public constant override contractType = "PRICE_FEED::CONSTANT"; | ||
|
|
||
| /// @notice Answer precision (always 8 decimals for USD price feeds) | ||
| uint8 public constant override decimals = 8; | ||
|
|
||
| /// @notice Indicates that price oracle can skip checks for this price feed's answers | ||
| bool public constant override skipPriceCheck = true; | ||
|
|
||
| /// @notice The token address this price feed is for | ||
| address public immutable token; | ||
Van0k marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// @notice The constant price value to return | ||
| int256 public immutable price; | ||
|
|
||
| bytes32 internal descriptionTicker; | ||
|
|
||
| /// @notice Constructor | ||
| /// @param _token The token address this price feed is for | ||
| /// @param _price The constant price value to return (with 8 decimals) | ||
| constructor(address _token, int256 _price, string memory _descriptionTicker) nonZeroAddress(_token) { | ||
| if (_price <= 0) revert IncorrectPriceException(); | ||
|
|
||
| token = _token; | ||
| price = _price; | ||
|
|
||
| descriptionTicker = _descriptionTicker.toSmallString(); | ||
| } | ||
|
|
||
| /// @notice Price feed description | ||
| function description() external view override returns (string memory) { | ||
| return string.concat(descriptionTicker.fromSmallString(), " constant price feed"); | ||
| } | ||
|
|
||
| /// @notice Serialized price feed parameters | ||
| function serialize() external view override returns (bytes memory) { | ||
| return abi.encode(token, price); | ||
| } | ||
|
|
||
| /// @notice Returns the constant USD price of the token with 8 decimals | ||
| function latestRoundData() external view override returns (uint80, int256 answer, uint256, uint256, uint80) { | ||
| return (0, price, 0, block.timestamp, 0); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2025. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import {LibString} from "@solady/utils/LibString.sol"; | ||
| import {ICurvePool} from "../../interfaces/curve/ICurvePool.sol"; | ||
| import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol"; | ||
| import {WAD} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol"; | ||
| import {PriceFeedValidationTrait} from "@gearbox-protocol/core-v3/contracts/traits/PriceFeedValidationTrait.sol"; | ||
| import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
Van0k marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// @title Curve TWAP price feed | ||
| /// @notice Computes price of coin 1 in a Curve pool in terms of units of coin 0, based on the pool's TWAP, which is then | ||
| /// multiplied by the price of coin 0. | ||
| contract CurveTWAPPriceFeed is IPriceFeed, PriceFeedValidationTrait, SanityCheckTrait { | ||
| using LibString for string; | ||
| using LibString for bytes32; | ||
|
|
||
| /// @notice Thrown when the curve oracle value is out of bounds | ||
| error CurveOracleOutOfBoundsException(); | ||
|
|
||
| /// @notice Thrown when the upper bound is less than the lower bound passed in constructor | ||
| error UpperBoundTooLowException(); | ||
|
|
||
| /// @notice Thrown when the the price feed fails to retrieve the price oracle value from known function signatures | ||
| error UnknownCurveOracleSignatureException(); | ||
|
|
||
| /// @notice Contract version | ||
| uint256 public constant override version = 3_10; | ||
|
|
||
| /// @notice Contract type | ||
| bytes32 public constant override contractType = "PRICE_FEED::CURVE_TWAP"; | ||
|
|
||
| /// @notice Answer precision (always 8 decimals for USD price feeds) | ||
| uint8 public constant override decimals = 8; | ||
|
|
||
| /// @notice Indicates that price oracle can skip checks for this price feed's answers | ||
| bool public constant override skipPriceCheck = true; | ||
|
|
||
| /// @notice token token address | ||
| address public immutable token; | ||
|
|
||
| /// @notice Curve pool address | ||
| address public immutable pool; | ||
|
|
||
| /// @notice Coin 0 price feed address | ||
| address public immutable priceFeed; | ||
|
|
||
| /// @notice Staleness period for the coin 0 price feed | ||
| uint32 public immutable stalenessPeriod; | ||
|
|
||
| /// @notice Flag indicating if price feed checks can be skipped | ||
| bool public immutable skipCheck; | ||
|
|
||
| /// @notice Lower bound for the exchange rate | ||
| uint256 public immutable lowerBound; | ||
|
|
||
| /// @notice Upper bound for the exchange rate | ||
| uint256 public immutable upperBound; | ||
|
|
||
| /// @notice Whether the returned exchange rate is coin1/coin0 (Curve returns coin0/coin1 by default) | ||
| bool public immutable oneOverZero; | ||
|
|
||
| /// @dev Short form description | ||
| bytes32 internal descriptionTicker; | ||
|
|
||
| /// @notice Constructor | ||
| /// @param _lowerBound Lower bound for the pool exchange rate | ||
| /// @param _upperBound Upper bound for the pool exchange rate (must be greater than lower bound) | ||
| /// @param _oneOverZero Whether the returned exchange rate is coin1/coin0 (Curve returns coin0/coin1 by default) | ||
| /// @param _token Address of the base asset | ||
| /// @param _pool Address of the curve pool | ||
| /// @param _priceFeed Address of the underlying asset price feed | ||
| /// @param _stalenessPeriod Staleness period for the underlying asset price feed | ||
| /// @param _descriptionTicker Short form description | ||
| constructor( | ||
| uint256 _lowerBound, | ||
| uint256 _upperBound, | ||
| bool _oneOverZero, | ||
| address _token, | ||
| address _pool, | ||
| address _priceFeed, | ||
| uint32 _stalenessPeriod, | ||
| string memory _descriptionTicker | ||
| ) nonZeroAddress(_token) nonZeroAddress(_pool) nonZeroAddress(_priceFeed) { | ||
| if (_upperBound < _lowerBound) revert UpperBoundTooLowException(); | ||
Van0k marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| token = _token; | ||
| pool = _pool; | ||
| priceFeed = _priceFeed; | ||
| stalenessPeriod = _stalenessPeriod; | ||
| skipCheck = _validatePriceFeedMetadata(_priceFeed, _stalenessPeriod); | ||
| lowerBound = _lowerBound; | ||
| upperBound = _upperBound; | ||
| oneOverZero = _oneOverZero; | ||
|
|
||
| descriptionTicker = _descriptionTicker.toSmallString(); | ||
| } | ||
|
|
||
| /// @notice Price feed description | ||
| function description() external view override returns (string memory) { | ||
| return string.concat(descriptionTicker.fromSmallString(), " Curve TWAP price feed"); | ||
| } | ||
|
|
||
| /// @notice Serialized price feed parameters | ||
| function serialize() external view override returns (bytes memory) { | ||
| return abi.encode(token, pool, lowerBound, upperBound); | ||
| } | ||
|
|
||
| /// @dev Retrieves the price oracle value from the curve pool | ||
| /// Supports TwoCryptoOptimized and CurveStableNG pools | ||
Van0k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// Returns 1 / rate if the requested rate is coin1/coin0 | ||
| function _getExchangeRate() internal view returns (uint256 rate) { | ||
| try ICurvePool(pool).price_oracle() returns (uint256 _rate) { | ||
| rate = _rate; | ||
| } catch { | ||
| try ICurvePool(pool).price_oracle(0) returns (uint256 _rate) { | ||
| rate = _rate; | ||
| } catch { | ||
| revert UnknownCurveOracleSignatureException(); | ||
| } | ||
| } | ||
lekhovitsky marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| rate = oneOverZero ? WAD * WAD / rate : rate; | ||
| } | ||
|
|
||
| /// @notice Returns USD price of the token token with 8 decimals | ||
| function latestRoundData() external view override returns (uint80, int256 answer, uint256, uint256, uint80) { | ||
| uint256 exchangeRate = _getExchangeRate(); | ||
|
|
||
| if (exchangeRate < lowerBound) revert CurveOracleOutOfBoundsException(); | ||
| if (exchangeRate > upperBound) exchangeRate = upperBound; | ||
|
|
||
| int256 underlyingPrice = _getValidatedPrice(priceFeed, stalenessPeriod, skipCheck); | ||
|
|
||
| answer = int256((exchangeRate * uint256(underlyingPrice)) / WAD); | ||
|
|
||
| return (0, answer, 0, 0, 0); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2024. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import {Test} from "forge-std/Test.sol"; | ||
| import {ConstantPriceFeed} from "../../oracles/ConstantPriceFeed.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
| import {IncorrectPriceException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol"; | ||
|
|
||
| contract ConstantPriceFeedUnitTest is Test { | ||
| ConstantPriceFeed priceFeed; | ||
| address token; | ||
| int256 constantPrice; | ||
|
|
||
| function setUp() public { | ||
| token = makeAddr("TOKEN"); | ||
| constantPrice = 12345678; // $1.23456789 with 8 decimals | ||
|
|
||
| // Mock token symbol call | ||
| vm.mockCall(token, abi.encodeCall(IERC20Metadata.symbol, ()), abi.encode("TKN")); | ||
|
|
||
| priceFeed = new ConstantPriceFeed(token, constantPrice, "TKN / USD"); | ||
| } | ||
|
|
||
| /// @notice U:[CPF-1]: Price feed initialization works as expected | ||
| function test_U_CPF_1_initialization() public { | ||
| assertEq(priceFeed.token(), token, "Incorrect token address"); | ||
| assertEq(priceFeed.price(), constantPrice, "Incorrect constant price"); | ||
| assertEq(priceFeed.description(), "TKN / USD constant price feed", "Incorrect description"); | ||
| assertEq(priceFeed.decimals(), 8, "Incorrect decimals"); | ||
| assertTrue(priceFeed.skipPriceCheck(), "skipPriceCheck should be true"); | ||
| assertEq(priceFeed.contractType(), "PRICE_FEED::CONSTANT", "Incorrect contract type"); | ||
| } | ||
|
|
||
| /// @notice U:[CPF-2]: Price feed returns constant price as expected | ||
| function test_U_CPF_2_latestRoundData() public { | ||
| (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = | ||
| priceFeed.latestRoundData(); | ||
|
|
||
| assertEq(roundId, 0, "Incorrect roundId"); | ||
| assertEq(answer, constantPrice, "Incorrect price answer"); | ||
| assertEq(startedAt, 0, "Incorrect startedAt"); | ||
| assertEq(updatedAt, block.timestamp, "Incorrect updatedAt"); | ||
| assertEq(answeredInRound, 0, "Incorrect answeredInRound"); | ||
| } | ||
|
|
||
| /// @notice U:[CPF-3]: Price feed serialization works as expected | ||
| function test_U_CPF_3_serialize() public { | ||
| bytes memory serialized = priceFeed.serialize(); | ||
| (address serializedToken, int256 serializedPrice) = abi.decode(serialized, (address, int256)); | ||
|
|
||
| assertEq(serializedToken, token, "Incorrect serialized token"); | ||
| assertEq(serializedPrice, constantPrice, "Incorrect serialized price"); | ||
| } | ||
|
|
||
| /// @notice U:[CPF-4]: Price feed constructor validates parameters | ||
| function test_U_CPF_4_constructor_validation() public { | ||
| // Test with zero address token | ||
| vm.expectRevert(); | ||
| new ConstantPriceFeed(address(0), constantPrice, "TKN / USD"); | ||
|
|
||
| // Test with zero price | ||
| vm.expectRevert(IncorrectPriceException.selector); | ||
| new ConstantPriceFeed(token, 0, "TKN / USD"); | ||
|
|
||
| // Test with negative price | ||
| vm.expectRevert(IncorrectPriceException.selector); | ||
| new ConstantPriceFeed(token, -1, "TKN / USD"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.