Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/interfaces/curve/ICurvePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ interface ICurvePool {
function get_virtual_price() external view returns (uint256);

function price_oracle() external view returns (uint256);

function price_oracle(uint256 index) external view returns (uint256);
}
64 changes: 64 additions & 0 deletions contracts/oracles/ConstantPriceFeed.sol
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;

/// @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;

/// @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);
}
}
142 changes: 142 additions & 0 deletions contracts/oracles/curve/CurveTWAPPriceFeed.sol
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";

/// @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();

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
/// 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();
}
}

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);
}
}
24 changes: 22 additions & 2 deletions contracts/test/mocks/curve/CurvePoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,33 @@ import {ICurvePool} from "../../../interfaces/curve/ICurvePool.sol";

contract CurvePoolMock is ICurvePool {
uint256 public override get_virtual_price;
uint256 public override price_oracle;
uint256 internal price_oracle_value;

bool public withIndex = false;

function hack_virtual_price(uint256 new_virtual_price) external {
get_virtual_price = new_virtual_price;
}

function hack_price_oracle(uint256 new_price_oracle) external {
price_oracle = new_price_oracle;
price_oracle_value = new_price_oracle;
}

function hack_withIndex(bool new_withIndex) external {
withIndex = new_withIndex;
}

function price_oracle() external view returns (uint256) {
if (withIndex) {
revert();
}
return price_oracle_value;
}

function price_oracle(uint256 index) external view returns (uint256) {
if (!withIndex || index != 0) {
revert();
}
return price_oracle_value;
}
}
71 changes: 71 additions & 0 deletions contracts/test/unit/ConstantPriceFeed.unit.t.sol
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");
}
}
Loading
Loading