Skip to content

Commit c573e4a

Browse files
authored
Merge pull request #60 from Gearbox-protocol/curve-twap-feed
feat: add curve twap price feed and constant price feed
2 parents fc8d3a0 + 9d49350 commit c573e4a

File tree

10 files changed

+525
-133
lines changed

10 files changed

+525
-133
lines changed

contracts/interfaces/curve/ICurvePool.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ interface ICurvePool {
77
function get_virtual_price() external view returns (uint256);
88

99
function price_oracle() external view returns (uint256);
10+
11+
function price_oracle(uint256 index) external view returns (uint256);
1012
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2025.
4+
pragma solidity ^0.8.23;
5+
6+
import {LibString} from "@solady/utils/LibString.sol";
7+
import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol";
8+
import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol";
9+
import {IncorrectPriceException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
10+
11+
/// @title Constant price feed
12+
/// @notice A simple price feed that returns a constant value set in the constructor
13+
contract ConstantPriceFeed is IPriceFeed, SanityCheckTrait {
14+
using LibString for string;
15+
using LibString for bytes32;
16+
17+
/// @notice Contract version
18+
uint256 public constant override version = 3_10;
19+
20+
/// @notice Contract type
21+
bytes32 public constant override contractType = "PRICE_FEED::CONSTANT";
22+
23+
/// @notice Answer precision (always 8 decimals for USD price feeds)
24+
uint8 public constant override decimals = 8;
25+
26+
/// @notice Indicates that price oracle can skip checks for this price feed's answers
27+
bool public constant override skipPriceCheck = true;
28+
29+
/// @notice The constant price value to return
30+
int256 public immutable price;
31+
32+
bytes32 internal descriptionTicker;
33+
34+
/// @notice Constructor
35+
/// @param _price The constant price value to return (with 8 decimals)
36+
/// @param _descriptionTicker Short form description
37+
constructor(int256 _price, string memory _descriptionTicker) {
38+
if (_price <= 0) revert IncorrectPriceException();
39+
40+
price = _price;
41+
descriptionTicker = _descriptionTicker.toSmallString();
42+
}
43+
44+
/// @notice Price feed description
45+
function description() external view override returns (string memory) {
46+
return string.concat(descriptionTicker.fromSmallString(), " constant price feed");
47+
}
48+
49+
/// @notice Serialized price feed parameters
50+
function serialize() external view override returns (bytes memory) {
51+
return abi.encode(price);
52+
}
53+
54+
/// @notice Returns the constant USD price of the token with 8 decimals
55+
function latestRoundData() external view override returns (uint80, int256 answer, uint256, uint256, uint80) {
56+
return (0, price, 0, block.timestamp, 0);
57+
}
58+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2025.
4+
pragma solidity ^0.8.23;
5+
6+
import {LibString} from "@solady/utils/LibString.sol";
7+
import {ICurvePool} from "../../interfaces/curve/ICurvePool.sol";
8+
import {IPriceFeed} from "@gearbox-protocol/core-v3/contracts/interfaces/base/IPriceFeed.sol";
9+
import {WAD} from "@gearbox-protocol/core-v3/contracts/libraries/Constants.sol";
10+
import {PriceFeedValidationTrait} from "@gearbox-protocol/core-v3/contracts/traits/PriceFeedValidationTrait.sol";
11+
import {SanityCheckTrait} from "@gearbox-protocol/core-v3/contracts/traits/SanityCheckTrait.sol";
12+
13+
/// @title Curve TWAP price feed
14+
/// @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
15+
/// multiplied by the price of coin 0.
16+
contract CurveTWAPPriceFeed is IPriceFeed, PriceFeedValidationTrait, SanityCheckTrait {
17+
using LibString for string;
18+
using LibString for bytes32;
19+
20+
/// @notice Thrown when the curve oracle value is out of bounds
21+
error CurveOracleOutOfBoundsException();
22+
23+
/// @notice Thrown when the upper bound is less than the lower bound passed in constructor
24+
error UpperBoundTooLowException();
25+
26+
/// @notice Thrown when the the price feed fails to retrieve the price oracle value from known function signatures
27+
error UnknownCurveOracleSignatureException();
28+
29+
/// @notice Contract version
30+
uint256 public constant override version = 3_10;
31+
32+
/// @notice Contract type
33+
bytes32 public constant override contractType = "PRICE_FEED::CURVE_TWAP";
34+
35+
/// @notice Answer precision (always 8 decimals for USD price feeds)
36+
uint8 public constant override decimals = 8;
37+
38+
/// @notice Indicates that price oracle can skip checks for this price feed's answers
39+
bool public constant override skipPriceCheck = true;
40+
41+
/// @notice Token address
42+
address public immutable token;
43+
44+
/// @notice Curve pool address
45+
address public immutable pool;
46+
47+
/// @notice Coin 0 price feed address
48+
address public immutable priceFeed;
49+
50+
/// @notice Staleness period for the coin 0 price feed
51+
uint32 public immutable stalenessPeriod;
52+
53+
/// @notice Flag indicating if price feed checks can be skipped
54+
bool public immutable skipCheck;
55+
56+
/// @notice Lower bound for the exchange rate
57+
uint256 public immutable lowerBound;
58+
59+
/// @notice Upper bound for the exchange rate
60+
uint256 public immutable upperBound;
61+
62+
/// @notice Whether the returned exchange rate is coin1/coin0 (Curve returns coin0/coin1 by default)
63+
bool public immutable oneOverZero;
64+
65+
/// @dev Short form description
66+
bytes32 internal descriptionTicker;
67+
68+
/// @notice Constructor
69+
/// @param _lowerBound Lower bound for the pool exchange rate
70+
/// @param _upperBound Upper bound for the pool exchange rate (must be greater than lower bound)
71+
/// @param _oneOverZero Whether the returned exchange rate is coin1/coin0 (Curve returns coin0/coin1 by default)
72+
/// @param _token Address of the base asset
73+
/// @param _pool Address of the curve pool
74+
/// @param _priceFeed Address of the underlying asset price feed
75+
/// @param _stalenessPeriod Staleness period for the underlying asset price feed
76+
/// @param _descriptionTicker Short form description
77+
constructor(
78+
uint256 _lowerBound,
79+
uint256 _upperBound,
80+
bool _oneOverZero,
81+
address _token,
82+
address _pool,
83+
address _priceFeed,
84+
uint32 _stalenessPeriod,
85+
string memory _descriptionTicker
86+
) nonZeroAddress(_token) nonZeroAddress(_pool) nonZeroAddress(_priceFeed) {
87+
if (_upperBound <= _lowerBound) revert UpperBoundTooLowException();
88+
89+
token = _token;
90+
pool = _pool;
91+
priceFeed = _priceFeed;
92+
stalenessPeriod = _stalenessPeriod;
93+
skipCheck = _validatePriceFeedMetadata(_priceFeed, _stalenessPeriod);
94+
lowerBound = _lowerBound;
95+
upperBound = _upperBound;
96+
oneOverZero = _oneOverZero;
97+
98+
descriptionTicker = _descriptionTicker.toSmallString();
99+
}
100+
101+
/// @notice Price feed description
102+
function description() external view override returns (string memory) {
103+
return string.concat(descriptionTicker.fromSmallString(), " Curve TWAP price feed");
104+
}
105+
106+
/// @notice Serialized price feed parameters
107+
function serialize() external view override returns (bytes memory) {
108+
return abi.encode(token, pool, lowerBound, upperBound);
109+
}
110+
111+
/// @dev Retrieves the price oracle value from the curve pool
112+
/// Supports TwoCryptoOptimized and CurveStableNG pools
113+
/// Returns 1 / rate if the requested rate is coin1/coin0
114+
function _getExchangeRate() internal view returns (uint256 rate) {
115+
try ICurvePool(pool).price_oracle() returns (uint256 _rate) {
116+
rate = _rate;
117+
} catch {
118+
try ICurvePool(pool).price_oracle(0) returns (uint256 _rate) {
119+
rate = _rate;
120+
} catch {
121+
revert UnknownCurveOracleSignatureException();
122+
}
123+
}
124+
125+
rate = oneOverZero ? WAD * WAD / rate : rate;
126+
}
127+
128+
/// @notice Returns USD price of the token token with 8 decimals
129+
function latestRoundData() external view override returns (uint80, int256 answer, uint256, uint256, uint80) {
130+
uint256 exchangeRate = _getExchangeRate();
131+
132+
if (exchangeRate < lowerBound) revert CurveOracleOutOfBoundsException();
133+
if (exchangeRate > upperBound) exchangeRate = upperBound;
134+
135+
int256 underlyingPrice = _getValidatedPrice(priceFeed, stalenessPeriod, skipCheck);
136+
137+
answer = int256((exchangeRate * uint256(underlyingPrice)) / WAD);
138+
139+
return (0, answer, 0, 0, 0);
140+
}
141+
}

contracts/test/mocks/curve/CurvePoolMock.sol

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,33 @@ import {ICurvePool} from "../../../interfaces/curve/ICurvePool.sol";
77

88
contract CurvePoolMock is ICurvePool {
99
uint256 public override get_virtual_price;
10-
uint256 public override price_oracle;
10+
uint256 internal price_oracle_value;
11+
12+
bool public withIndex = false;
1113

1214
function hack_virtual_price(uint256 new_virtual_price) external {
1315
get_virtual_price = new_virtual_price;
1416
}
1517

1618
function hack_price_oracle(uint256 new_price_oracle) external {
17-
price_oracle = new_price_oracle;
19+
price_oracle_value = new_price_oracle;
20+
}
21+
22+
function hack_withIndex(bool new_withIndex) external {
23+
withIndex = new_withIndex;
24+
}
25+
26+
function price_oracle() external view returns (uint256) {
27+
if (withIndex) {
28+
revert();
29+
}
30+
return price_oracle_value;
31+
}
32+
33+
function price_oracle(uint256 index) external view returns (uint256) {
34+
if (!withIndex || index != 0) {
35+
revert();
36+
}
37+
return price_oracle_value;
1838
}
1939
}

0 commit comments

Comments
 (0)