-
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 1 commit
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2025. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| 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 { | ||
| /// @notice Contract version | ||
| uint256 public constant override version = 3_10; | ||
|
|
||
| /// @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; | ||
|
|
||
| /// @notice Price feed description | ||
| string public description; | ||
|
|
||
| /// @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) nonZeroAddress(_token) { | ||
| if (_price <= 0) revert IncorrectPriceException(); | ||
|
|
||
| token = _token; | ||
| price = _price; | ||
|
|
||
| string memory tokenSymbol = IERC20Metadata(_token).symbol(); | ||
| description = string.concat(tokenSymbol, " / USD constant price feed"); | ||
lekhovitsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// @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,109 @@ | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2025. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| 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 {Address} from "@openzeppelin/contracts/utils/Address.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 Address for address; | ||
lekhovitsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// @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 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 Price feed description | ||
| string public description; | ||
|
|
||
| /// @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 _token Address of the coin 1 in the pool | ||
| /// @param _pool Address of the curve pool | ||
| /// @param _priceFeed Address of the coin 0 price feed | ||
| /// @param _stalenessPeriod Staleness period for the coin 0 price feed | ||
| constructor( | ||
| uint256 _lowerBound, | ||
| uint256 _upperBound, | ||
| address _token, | ||
| address _pool, | ||
| address _priceFeed, | ||
| uint32 _stalenessPeriod | ||
| ) 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; | ||
|
|
||
| string memory tokenSymbol = IERC20Metadata(_token).symbol(); | ||
| description = string.concat(tokenSymbol, " / USD Curve TWAP price feed"); | ||
lekhovitsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// @notice Serialized price feed parameters | ||
| function serialize() external view override returns (bytes memory) { | ||
| return abi.encode(token, pool, priceFeed, lowerBound, upperBound); | ||
lekhovitsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// @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 = ICurvePool(pool).price_oracle(); | ||
lekhovitsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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
| 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); | ||
| } | ||
|
|
||
| /// @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); | ||
|
|
||
| // Test with zero price | ||
| vm.expectRevert(IncorrectPriceException.selector); | ||
| new ConstantPriceFeed(token, 0); | ||
|
|
||
| // Test with negative price | ||
| vm.expectRevert(IncorrectPriceException.selector); | ||
| new ConstantPriceFeed(token, -1); | ||
| } | ||
| } |
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,91 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| // Gearbox Protocol. Generalized leverage for DeFi protocols | ||
| // (c) Gearbox Foundation, 2024. | ||
| pragma solidity ^0.8.23; | ||
|
|
||
| import {PriceFeedUnitTestHelper} from "../PriceFeedUnitTestHelper.sol"; | ||
|
|
||
| import {CurvePoolMock} from "../../mocks/curve/CurvePoolMock.sol"; | ||
|
|
||
| import {ICurvePool} from "../../../interfaces/curve/ICurvePool.sol"; | ||
| import {CurveTWAPPriceFeed} from "../../../oracles/curve/CurveTWAPPriceFeed.sol"; | ||
| import {WAD} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol"; | ||
| import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
|
||
| contract CurveTWAPPriceFeedUnitTest is PriceFeedUnitTestHelper { | ||
| CurveTWAPPriceFeed priceFeed; | ||
| CurvePoolMock curvePool; | ||
| address token; | ||
| uint256 lowerBound; | ||
| uint256 upperBound; | ||
|
|
||
| function setUp() public { | ||
| _setUp(); | ||
|
|
||
| token = makeAddr("token"); | ||
| vm.mockCall(token, abi.encodeCall(IERC20Metadata.symbol, ()), abi.encode("TOKEN")); | ||
|
|
||
| curvePool = new CurvePoolMock(); | ||
| curvePool.hack_price_oracle(1.03 ether); | ||
|
|
||
| lowerBound = 1.02 ether; | ||
| upperBound = 1.05 ether; | ||
|
|
||
| priceFeed = new CurveTWAPPriceFeed( | ||
| lowerBound, upperBound, token, address(curvePool), address(underlyingPriceFeed), 1 days | ||
| ); | ||
| } | ||
|
|
||
| /// @notice U:[CTWAP-1]: Price feed works as expected | ||
| function test_U_CTWAP_1_price_feed_works_as_expected() public { | ||
| // constructor | ||
| assertEq(priceFeed.token(), token, "Incorrect token address"); | ||
| assertEq(priceFeed.pool(), address(curvePool), "Incorrect pool address"); | ||
| assertEq(priceFeed.priceFeed(), address(underlyingPriceFeed), "Incorrect price feed"); | ||
| assertEq(priceFeed.lowerBound(), lowerBound, "Incorrect lower bound"); | ||
| assertEq(priceFeed.upperBound(), upperBound, "Incorrect upper bound"); | ||
| assertEq(priceFeed.description(), "TOKEN / USD Curve TWAP price feed", "Incorrect description"); | ||
|
|
||
| // latestRoundData | ||
| vm.expectCall(address(curvePool), abi.encodeCall(ICurvePool.price_oracle, ())); | ||
| (, int256 price,,,) = priceFeed.latestRoundData(); | ||
| assertEq(price, int256((1.03 ether * 2e8) / WAD), "Incorrect price"); | ||
| } | ||
|
|
||
| /// @notice U:[CTWAP-2]: Price feed handles exchange rate bounds properly | ||
| function test_U_CTWAP_2_price_feed_handles_exchange_rate_bounds() public { | ||
| // Test when exchange rate is below lower bound | ||
| curvePool.hack_price_oracle(1.01 ether); // Below lower bound | ||
| vm.expectRevert(CurveTWAPPriceFeed.CurveOracleOutOfBoundsException.selector); | ||
| priceFeed.latestRoundData(); | ||
|
|
||
| // Test when exchange rate equals lower bound | ||
| curvePool.hack_price_oracle(lowerBound); | ||
| (, int256 price,,,) = priceFeed.latestRoundData(); | ||
| assertEq(price, int256((lowerBound * 2e8) / WAD), "Incorrect price at lower bound"); | ||
|
|
||
| // Test when exchange rate equals upper bound | ||
| curvePool.hack_price_oracle(upperBound); | ||
| (, int256 price2,,,) = priceFeed.latestRoundData(); | ||
| assertEq(price2, int256((upperBound * 2e8) / WAD), "Incorrect price at upper bound"); | ||
|
|
||
| // Test when exchange rate exceeds upper bound (should be capped) | ||
| curvePool.hack_price_oracle(1.06 ether); // Above upper bound | ||
| (, int256 price3,,,) = priceFeed.latestRoundData(); | ||
| assertEq(price3, int256((upperBound * 2e8) / WAD), "Incorrect price when above upper bound"); | ||
| } | ||
|
|
||
| /// @notice U:[CTWAP-3]: Price feed constructor validates bounds correctly | ||
| function test_U_CTWAP_3_constructor_validates_bounds() public { | ||
| // Test upper bound < lower bound | ||
| vm.expectRevert(CurveTWAPPriceFeed.UpperBoundTooLowException.selector); | ||
| new CurveTWAPPriceFeed( | ||
| lowerBound, | ||
| lowerBound - 1, // Upper less than lower | ||
| token, | ||
| address(curvePool), | ||
| address(underlyingPriceFeed), | ||
| 1 days | ||
| ); | ||
| } | ||
| } |
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.