Skip to content
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

fix: bpt stable price feed fix + live test #33

Open
wants to merge 2 commits into
base: main
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
10 changes: 9 additions & 1 deletion contracts/interfaces/balancer/IBalancerStablePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
// (c) Gearbox Foundation, 2023.
pragma solidity ^0.8.17;

interface IBalancerStablePool {
interface IBalancerRateProvider {
function getRate() external view returns (uint256);
}

interface IBalancerStablePool is IBalancerRateProvider {
function getPoolId() external view returns (bytes32);

function getVault() external view returns (address);

function getRateProviders() external view returns (address[] memory);
}
2 changes: 2 additions & 0 deletions contracts/interfaces/balancer/IBalancerWeightedPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ interface IBalancerWeightedPool {
function getActualSupply() external view returns (uint256);

function getPoolId() external view returns (bytes32);

function getVault() external view returns (address);
}
65 changes: 59 additions & 6 deletions contracts/oracles/balancer/BPTStablePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,40 @@ import {LPPriceFeed} from "../LPPriceFeed.sol";
import {PriceFeedParams} from "../PriceFeedParams.sol";
import {WAD} from "@gearbox-protocol/core-v2/contracts/libraries/Constants.sol";
import {PriceFeedType} from "@gearbox-protocol/sdk-gov/contracts/PriceFeedType.sol";
import {IBalancerStablePool} from "../../interfaces/balancer/IBalancerStablePool.sol";
import {IBalancerStablePool, IBalancerRateProvider} from "../../interfaces/balancer/IBalancerStablePool.sol";
import {IBalancerVault} from "../../interfaces/balancer/IBalancerVault.sol";

/// @title Balancer stable pool token price feed
/// @dev Similarly to Curve stableswap, aggregate function is minimum of underlying tokens prices
contract BPTStablePriceFeed is LPPriceFeed {
uint256 public constant override version = 3_00;
PriceFeedType public constant override priceFeedType = PriceFeedType.BALANCER_STABLE_LP_ORACLE;

int256 constant TOKEN_RATE_NUMERATOR = 1e18;

uint8 public immutable numAssets;

address public immutable rateProvider0;
address public immutable priceFeed0;
uint32 public immutable stalenessPeriod0;
bool public immutable skipCheck0;

address public immutable rateProvider1;
address public immutable priceFeed1;
uint32 public immutable stalenessPeriod1;
bool public immutable skipCheck1;

address public immutable rateProvider2;
address public immutable priceFeed2;
uint32 public immutable stalenessPeriod2;
bool public immutable skipCheck2;

address public immutable rateProvider3;
address public immutable priceFeed3;
uint32 public immutable stalenessPeriod3;
bool public immutable skipCheck3;

address public immutable rateProvider4;
address public immutable priceFeed4;
uint32 public immutable stalenessPeriod4;
bool public immutable skipCheck4;
Expand All @@ -47,6 +55,14 @@ contract BPTStablePriceFeed is LPPriceFeed {
nonZeroAddress(priceFeeds[0].priceFeed) // U:[BAL-S-2]
nonZeroAddress(priceFeeds[1].priceFeed) // U:[BAL-S-2]
{
address[5] memory rateProviders = _getRateProviders(_balancerPool);

rateProvider0 = rateProviders[0];
rateProvider1 = rateProviders[1];
rateProvider2 = rateProviders[2];
rateProvider3 = rateProviders[3];
rateProvider4 = rateProviders[4];

priceFeed0 = priceFeeds[0].priceFeed;
priceFeed1 = priceFeeds[1].priceFeed;
priceFeed2 = priceFeeds[2].priceFeed;
Expand All @@ -70,28 +86,65 @@ contract BPTStablePriceFeed is LPPriceFeed {
_setLimiter(lowerBound); // U:[BAL-S-1]
}

function _getRateProviders(address _balancerPool) internal view returns (address[5] memory rateProviders) {
address vault = IBalancerStablePool(_balancerPool).getVault();
bytes32 poolId = IBalancerStablePool(_balancerPool).getPoolId();

(address[] memory tokens,,) = IBalancerVault(vault).getPoolTokens(poolId);
address[] memory _rateProviders = IBalancerStablePool(_balancerPool).getRateProviders();

uint256 len = tokens.length;
uint256 k = 0;
for (uint256 i; i < len;) {
if (tokens[i] != _balancerPool) {
rateProviders[k] = _rateProviders[i];
unchecked {
++k;
}
}

unchecked {
++i;
}
}
}

function getAggregatePrice() public view override returns (int256 answer) {
answer = _getValidatedPrice(priceFeed0, stalenessPeriod0, skipCheck0); // U:[BAL-S-2]
answer = _getAnswerOverTokenRate(rateProvider0, priceFeed0, stalenessPeriod0, skipCheck0); // U:[BAL-S-2]

int256 answerA = _getValidatedPrice(priceFeed1, stalenessPeriod1, skipCheck1);
int256 answerA = _getAnswerOverTokenRate(rateProvider1, priceFeed1, stalenessPeriod1, skipCheck1);
if (answerA < answer) answer = answerA; // U:[BAL-S-2]

if (numAssets > 2) {
answerA = _getValidatedPrice(priceFeed2, stalenessPeriod2, skipCheck2);
answerA = _getAnswerOverTokenRate(rateProvider2, priceFeed2, stalenessPeriod2, skipCheck2);
if (answerA < answer) answer = answerA; // U:[BAL-S-2]

if (numAssets > 3) {
answerA = _getValidatedPrice(priceFeed3, stalenessPeriod3, skipCheck3);
answerA = _getAnswerOverTokenRate(rateProvider3, priceFeed3, stalenessPeriod3, skipCheck3);
if (answerA < answer) answer = answerA; // U:[BAL-S-2]

if (numAssets > 4) {
answerA = _getValidatedPrice(priceFeed4, stalenessPeriod4, skipCheck4);
answerA = _getAnswerOverTokenRate(rateProvider4, priceFeed4, stalenessPeriod4, skipCheck4);
if (answerA < answer) answer = answerA; // U:[BAL-S-2]
}
}
}
}

function _getAnswerOverTokenRate(address rateProvider, address priceFeed, uint32 stalenessPeriod, bool skipCheck)
internal
view
returns (int256 answer)
{
answer = _getValidatedPrice(priceFeed, stalenessPeriod, skipCheck);

if (rateProvider != address(0)) {
answer = answer * TOKEN_RATE_NUMERATOR / int256(IBalancerRateProvider(rateProvider).getRate());
}

return answer;
}

function getLPExchangeRate() public view override returns (uint256) {
return IBalancerStablePool(lpToken).getRate(); // U:[BAL-S-1]
}
Expand Down
98 changes: 98 additions & 0 deletions contracts/test/live/balancer/BPTStablePriceFeed.eq.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// SPDX-License-Identifier: UNLICENSED
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2023.
pragma solidity ^0.8.17;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import {IPriceFeed} from "@gearbox-protocol/core-v2/contracts/interfaces/IPriceFeed.sol";
import {IBalancerWeightedPool} from "../../../interfaces/balancer/IBalancerWeightedPool.sol";
import {IBalancerVault} from "../../../interfaces/balancer/IBalancerVault.sol";

import {AddressProviderV3ACLMock} from
"@gearbox-protocol/core-v3/contracts/test/mocks/core/AddressProviderV3ACLMock.sol";
import {SupportedContracts} from "@gearbox-protocol/sdk-gov/contracts/SupportedContracts.sol";
import {TokenType} from "@gearbox-protocol/sdk-gov/contracts/Tokens.sol";
import {Tokens, TokensTestSuite} from "@gearbox-protocol/core-v3/contracts/test/suites/TokensTestSuite.sol";
import {NetworkDetector} from "@gearbox-protocol/sdk-gov/contracts/NetworkDetector.sol";
import {PriceFeedType} from "@gearbox-protocol/sdk-gov/contracts/PriceFeedType.sol";
import {PriceFeedConfig} from "@gearbox-protocol/core-v3/contracts/test/interfaces/ICreditConfig.sol";
import {PriceFeedDeployer} from "../../suites/PriceFeedDeployer.sol";

import {Test} from "forge-std/Test.sol";

contract BPTStablePriceFeed is Test {
PriceFeedDeployer public pfd;
uint256 chainId;

modifier liveTestOnly() {
if (chainId != 1337 && chainId != 31337) {
_;
}
}

function setUp() public {
NetworkDetector nd = new NetworkDetector();
chainId = nd.chainId();

if (chainId != 1337 && chainId != 31337) {
TokensTestSuite tokenTestSuite = new TokensTestSuite();

AddressProviderV3ACLMock addressProvider = new AddressProviderV3ACLMock();
SupportedContracts sc = new SupportedContracts(chainId);

pfd = new PriceFeedDeployer(chainId, address(addressProvider), tokenTestSuite, sc);
}
}

function test_live_BAL_EQ_01_Balancer_BPT_stable_pf_returns_price_equal_or_lower() public liveTestOnly {
uint256 len = pfd.priceFeedConfigLength();

for (uint256 i; i < len; ++i) {
(address token, address priceFeed,,) = pfd.priceFeedConfig(i);

PriceFeedType pft;

try IPriceFeed(priceFeed).priceFeedType() returns (PriceFeedType _pft) {
pft = _pft;
} catch {
pft = PriceFeedType.CHAINLINK_ORACLE;
}

if (
pft != PriceFeedType.BALANCER_STABLE_LP_ORACLE
|| pfd.tokenTestSuite().tokenTypes(pfd.tokenTestSuite().tokenIndexes(token))
!= TokenType.BALANCER_LP_TOKEN
) continue;

(, int256 pfPrice,,,) = IPriceFeed(priceFeed).latestRoundData();

bytes32 poolId = IBalancerWeightedPool(token).getPoolId();
address vault = IBalancerWeightedPool(token).getVault();

(address[] memory tokens, uint256[] memory balances,) = IBalancerVault(vault).getPoolTokens(poolId);

int256 computedPrice = 0;

for (uint256 j = 0; j < tokens.length; ++j) {
if (tokens[j] == token) continue;

(, int256 assetPrice,,,) = IPriceFeed(pfd.priceFeeds(tokens[j])).latestRoundData();

computedPrice += assetPrice * int256(balances[j]) / int256(10 ** ERC20(tokens[j]).decimals());
}

uint256 supply;

try IBalancerWeightedPool(token).getActualSupply() returns (uint256 _supply) {
supply = _supply;
} catch {
supply = ERC20(token).totalSupply();
}

computedPrice = computedPrice * 1e18 / int256(supply);

assertLe(pfPrice, computedPrice, "PF price higher than computed based on value of pool assets");
}
}
}
14 changes: 14 additions & 0 deletions contracts/test/mocks/balancer/BalancerRateProviderMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Foundation, 2023.
pragma solidity ^0.8.17;

import {IBalancerRateProvider} from "../../../interfaces/balancer/IBalancerStablePool.sol";

contract BalancerRateProviderMock is IBalancerRateProvider {
uint256 public override getRate;

constructor(uint256 rate) {
getRate = rate;
}
}
26 changes: 26 additions & 0 deletions contracts/test/mocks/balancer/BalancerStablePoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,38 @@
// (c) Gearbox Foundation, 2023.
pragma solidity ^0.8.17;

import {BalancerRateProviderMock} from "./BalancerRateProviderMock.sol";
import {IBalancerStablePool} from "../../../interfaces/balancer/IBalancerStablePool.sol";

contract BalancerStablePoolMock is IBalancerStablePool {
uint256 public override getRate;

mapping(address => uint256) public getTokenRate;

address[] public rateProviders;

address public getVault;
bytes32 public getPoolId;

constructor(address balancerVault, bytes32 poolId) {
getVault = balancerVault;
getPoolId = poolId;
}

function hackRate(uint256 newRate) external {
getRate = newRate;
}

function getRateProviders() external view returns (address[] memory) {
return rateProviders;
}

function hackRateProviders(uint256[] memory rates) external {
rateProviders = new address[](0);

for (uint256 i = 0; i < rates.length; ++i) {
address rateProvider = address(new BalancerRateProviderMock(rates[i]));
rateProviders.push(rateProvider);
}
}
}
4 changes: 3 additions & 1 deletion contracts/test/mocks/balancer/BalancerWeightedPoolMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ contract BalancerWeightedPoolMock is IBalancerWeightedPool {
uint256 public immutable override totalSupply;
bool public immutable actualSupplyEnabled;
uint256[] _weights;
address public getVault;

constructor(bytes32 poolId, uint256 supply, bool enableActualSupply, uint256[] memory weights) {
constructor(bytes32 poolId, uint256 supply, bool enableActualSupply, uint256[] memory weights, address vault) {
getPoolId = poolId;
totalSupply = supply;
_weights = weights;
actualSupplyEnabled = enableActualSupply;
getVault = vault;
}

function getNormalizedWeights() external view override returns (uint256[] memory) {
Expand Down
Loading
Loading