Skip to content

feat: fixed rate feed #59

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

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
57 changes: 57 additions & 0 deletions contracts/oracles/ConstantPriceFeed.sol
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, 2024.
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;

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

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

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

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

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");
}

/// @notice Serialized price feed parameters
function serialize() external view override returns (bytes memory) {
return abi.encode(token, pool, priceFeed, lowerBound, upperBound);
}

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

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);
}
}
38 changes: 0 additions & 38 deletions contracts/oracles/curve/CurveUSDPriceFeed.sol

This file was deleted.

9 changes: 5 additions & 4 deletions contracts/test/suites/PriceFeedDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {BPTStablePriceFeed} from "../../oracles/balancer/BPTStablePriceFeed.sol"
import {BPTWeightedPriceFeed} from "../../oracles/balancer/BPTWeightedPriceFeed.sol";
import {CurveCryptoLPPriceFeed} from "../../oracles/curve/CurveCryptoLPPriceFeed.sol";
import {CurveStableLPPriceFeed} from "../../oracles/curve/CurveStableLPPriceFeed.sol";
import {CurveUSDPriceFeed} from "../../oracles/curve/CurveUSDPriceFeed.sol";
import {CurveTWAPPriceFeed} from "../../oracles/curve/CurveTWAPPriceFeed.sol";
import {ERC4626PriceFeed} from "../../oracles/erc4626/ERC4626PriceFeed.sol";
import {WstETHPriceFeed} from "../../oracles/lido/WstETHPriceFeed.sol";
import {RedstonePriceFeed} from "../../oracles/updatable/RedstonePriceFeed.sol";
Expand Down Expand Up @@ -316,10 +316,11 @@ contract PriceFeedDeployer is Test, PriceFeedDataLive {

address pool = supportedContracts.addressOf(crvUSDPriceFeeds[i].pool);
address underlying = tokenTestSuite.addressOf(crvUSDPriceFeeds[i].underlying);

address pf = address(
new CurveUSDPriceFeed(
owner,
ICurvePool(pool).get_virtual_price() * 99 / 100,
new CurveTWAPPriceFeed(
0,
1e18,
token,
pool,
_getDeployedFeed(underlying, crvUSDPriceFeeds[i].reserve),
Expand Down
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);
}

/// @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);
}
}
91 changes: 91 additions & 0 deletions contracts/test/unit/curve/CurveTWAPPriceFeed.unit.t.sol
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
);
}
}
Loading
Loading