Skip to content

Commit fca08e9

Browse files
committed
fix: cleanup ERC4626PriceFeed + unit tests
1 parent 7756799 commit fca08e9

File tree

7 files changed

+224
-50
lines changed

7 files changed

+224
-50
lines changed
Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,89 @@
11
// SPDX-License-Identifier: BUSL-1.1
22
// Gearbox Protocol. Generalized leverage for DeFi protocols
3-
// (c) Gearbox Holdings, 2023
3+
// (c) Gearbox Foundation, 2023.
44
pragma solidity ^0.8.17;
55

6-
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
77
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
88
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
9-
import {PriceFeedType} from "../../interfaces/IPriceFeedType.sol";
109

10+
import {PriceFeedType} from "../../interfaces/IPriceFeedType.sol";
1111
import {LPPriceFeed} from "../LPPriceFeed.sol";
1212

1313
// EXCEPTIONS
14-
import {ZeroAddressException} from "@gearbox-protocol/core-v2/contracts/interfaces/IErrors.sol";
14+
import {ZeroAddressException} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
1515

16-
uint256 constant RANGE_WIDTH = 200; // 2%
16+
uint256 constant RANGE_WIDTH = 200;
1717

1818
/// @title ERC4626 vault shares price feed
1919
contract ERC4626PriceFeed is LPPriceFeed {
20-
/// @dev Chainlink price feed for the vault's underlying
21-
AggregatorV3Interface public immutable priceFeed;
20+
PriceFeedType public constant override priceFeedType = PriceFeedType.ERC4626_VAULT_ORACLE;
21+
uint256 public constant override version = 1;
22+
23+
/// @notice Vault to compute prices for
24+
address public immutable vault;
2225

23-
/// @dev Address of the vault to compute prices for
24-
IERC4626 public immutable vault;
26+
/// @notice Vault's underlying asset price feed
27+
address public immutable assetPriceFeed;
2528

26-
/// @dev Amount of shares comprising a single unit (accounting for decimals)
29+
/// @notice Amount of shares comprising a single unit (accounting for decimals)
2730
uint256 public immutable vaultShareUnit;
2831

29-
/// @dev Amount of underlying comprising a single unit (accounting for decimals)
32+
/// @notice Amount of underlying comprising a single unit (accounting for decimals)
3033
uint256 public immutable underlyingUnit;
3134

32-
PriceFeedType public constant override priceFeedType = PriceFeedType.ERC4626_VAULT_ORACLE;
33-
uint256 public constant override version = 1;
34-
35-
/// @dev Whether to skip price sanity checks.
36-
/// @notice Always set to true for LP price feeds,
37-
/// since they perform their own sanity checks
35+
/// @notice Whether to skip price sanity checks (always true for LP price feeds which perform their own checks)
3836
bool public constant override skipPriceCheck = true;
3937

40-
constructor(address addressProvider, address _vault, address _priceFeed)
38+
/// @notice Constructor
39+
/// @param addressProvider Address provider contract
40+
/// @param _vault Vault to compute prices for
41+
/// @param _assetPriceFeed Vault's underlying asset price feed
42+
constructor(address addressProvider, address _vault, address _assetPriceFeed)
4143
LPPriceFeed(
4244
addressProvider,
4345
RANGE_WIDTH,
44-
_vault != address(0) ? string(abi.encodePacked(IERC20Metadata(_vault).name(), " priceFeed")) : ""
45-
)
46+
_vault != address(0) ? string(abi.encodePacked(ERC20(_vault).name(), " priceFeed")) : ""
47+
) // U:[TVPF-2]
48+
nonZeroAddress(_vault) // U:[TVPF-1]
49+
nonZeroAddress(_assetPriceFeed) // U:[TVPF-1]
4650
{
47-
if (_vault == address(0) || _priceFeed == address(0)) {
48-
revert ZeroAddressException();
49-
}
50-
51-
vault = IERC4626(_vault);
52-
priceFeed = AggregatorV3Interface(_priceFeed);
51+
vault = _vault; // U:[TVPF-2]
52+
assetPriceFeed = _assetPriceFeed; // U:[TVPF-2]
5353

54-
vaultShareUnit = 10 ** vault.decimals();
55-
underlyingUnit = 10 ** IERC20Metadata(vault.asset()).decimals();
54+
vaultShareUnit = 10 ** IERC4626(_vault).decimals(); // U:[TVPF-2]
55+
underlyingUnit = 10 ** ERC20(IERC4626(_vault).asset()).decimals(); // U:[TVPF-2]
5656

57-
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
58-
_setLimiter(assetsPerShare);
57+
_setLimiter(IERC4626(_vault).convertToAssets(vaultShareUnit)); // U:[TVPF-2]
5958
}
6059

61-
/// @dev Returns the USD price of the pool's share
60+
/// @notice Returns the USD price of a single pool share
6261
function latestRoundData()
6362
external
6463
view
6564
override
6665
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
6766
{
68-
(roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.latestRoundData();
67+
(roundId, answer, startedAt, updatedAt, answeredInRound) =
68+
AggregatorV3Interface(assetPriceFeed).latestRoundData(); // U:[TVPF-3,4]
6969

70-
// Sanity check for chainlink pricefeed
71-
_checkAnswer(roundId, answer, updatedAt, answeredInRound);
70+
_checkAnswer(roundId, answer, updatedAt, answeredInRound); // U:[TVPF-3]
7271

73-
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
72+
uint256 assetsPerShare = IERC4626(vault).convertToAssets(vaultShareUnit); // U:[TVPF-4]
7473

75-
assetsPerShare = _checkAndUpperBoundValue(assetsPerShare);
74+
assetsPerShare = _checkAndUpperBoundValue(assetsPerShare); // U:[TVPF-4]
7675

77-
answer = int256((assetsPerShare * uint256(answer)) / underlyingUnit);
76+
answer = int256((assetsPerShare * uint256(answer)) / underlyingUnit); // U:[TVPF-4]
7877
}
7978

80-
function _checkCurrentValueInBounds(uint256 _lowerBound, uint256 _uBound) internal view override returns (bool) {
81-
uint256 assetsPerShare = vault.convertToAssets(vaultShareUnit);
82-
if (assetsPerShare < _lowerBound || assetsPerShare > _uBound) {
83-
return false;
84-
}
85-
return true;
79+
/// @dev Returns true if assets per share falls within bounds and false otherwise
80+
function _checkCurrentValueInBounds(uint256 _lowerBound, uint256 _upperBound)
81+
internal
82+
view
83+
override
84+
returns (bool)
85+
{
86+
uint256 assetsPerShare = IERC4626(vault).convertToAssets(vaultShareUnit); // U:[TVPF-5]
87+
return assetsPerShare >= _lowerBound && assetsPerShare <= _upperBound; // U:[TVPF-5]
8688
}
8789
}

contracts/oracles/redstone/RedstonePriceFeed.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: BUSL-1.1
22
// Gearbox Protocol. Generalized leverage for DeFi protocols
3-
// (c) Gearbox Holdings, 2023
3+
// (c) Gearbox Foundation, 2023.
44
pragma solidity ^0.8.17;
55

66
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2023.
4+
pragma solidity ^0.8.17;
5+
6+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
7+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8+
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
9+
10+
contract ERC4626Mock is ERC4626 {
11+
uint256 public assetsPerShare;
12+
13+
constructor(address asset, string memory name, string memory symbol) ERC20(name, symbol) ERC4626(IERC20(asset)) {}
14+
15+
function convertToAssets(uint256 shares) public view override returns (uint256 assets) {
16+
return shares * assetsPerShare / 10 ** 18;
17+
}
18+
19+
function setAssetsPerShare(uint256 newAssetsPerShare) external {
20+
assetsPerShare = newAssetsPerShare;
21+
}
22+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// Gearbox Protocol. Generalized leverage for DeFi protocols
3+
// (c) Gearbox Foundation, 2023.
4+
pragma solidity ^0.8.17;
5+
6+
import {Test} from "forge-std/Test.sol";
7+
import {CONFIGURATOR} from "@gearbox-protocol/core-v3/contracts/test/lib/constants.sol";
8+
9+
import {ERC4626PriceFeed, RANGE_WIDTH} from "../../oracles/erc4626/ERC4626PriceFeed.sol";
10+
import {PERCENTAGE_FACTOR} from "@gearbox-protocol/core-v2/contracts/libraries/PercentageMath.sol";
11+
12+
// MOCKS
13+
import {ERC4626Mock} from "../mocks/integrations/erc4626/ERC4626Mock.sol";
14+
import {PriceFeedMock} from "@gearbox-protocol/core-v3/contracts/test/mocks/oracles/PriceFeedMock.sol";
15+
import {AddressProviderV3ACLMock} from
16+
"@gearbox-protocol/core-v3/contracts/test/mocks/core/AddressProviderV3ACLMock.sol";
17+
18+
// SUITES
19+
import {TokensTestSuite} from "@gearbox-protocol/core-v3/contracts/test/suites/TokensTestSuite.sol";
20+
import {Tokens} from "@gearbox-protocol/sdk/contracts/Tokens.sol";
21+
22+
// EXCEPTIONS
23+
import {IPriceOracleV2Exceptions} from "../../interfaces/IPriceOracleV2.sol";
24+
import {
25+
ZeroAddressException,
26+
ValueOutOfRangeException,
27+
IncorrectLimitsException,
28+
NotImplementedException
29+
} from "@gearbox-protocol/core-v3/contracts/interfaces/IExceptions.sol";
30+
31+
/// @title ERC4626 price feed unit test
32+
/// @notice U:[TVPF]: Unit tests for ERC4626 tokenized vault price feed
33+
contract ERC4626PriceFeedUnitTest is Test, IPriceOracleV2Exceptions {
34+
ERC4626PriceFeed vaultPriceFeed;
35+
36+
ERC4626Mock vault;
37+
PriceFeedMock assetPriceFeed;
38+
AddressProviderV3ACLMock addressProvider;
39+
40+
TokensTestSuite tokenTestSuite;
41+
42+
function setUp() public {
43+
vm.startPrank(CONFIGURATOR);
44+
45+
addressProvider = new AddressProviderV3ACLMock();
46+
tokenTestSuite = new TokensTestSuite();
47+
48+
assetPriceFeed = new PriceFeedMock(1000, 8);
49+
vm.label(address(assetPriceFeed), "DAI_PRICEFEED");
50+
51+
vault = new ERC4626Mock(tokenTestSuite.addressOf(Tokens.DAI), "Mock vault", "MOCK");
52+
vm.label(address(vault), "ERC4626_MOCK");
53+
vault.setAssetsPerShare(1 ether);
54+
55+
vaultPriceFeed = new ERC4626PriceFeed(address(addressProvider), address(vault), address(assetPriceFeed));
56+
vm.label(address(vaultPriceFeed), "ERC4626_PRICE_FEED");
57+
58+
vm.stopPrank();
59+
}
60+
61+
/// @notice U:[TVPF-1]: Constructor reverts on zero addresses
62+
function test_TVPF_01_constructor_reverts_for_zero_addresses() public {
63+
vm.expectRevert(ZeroAddressException.selector);
64+
new ERC4626PriceFeed(address(addressProvider), address(0), address(0));
65+
66+
vm.expectRevert(ZeroAddressException.selector);
67+
new ERC4626PriceFeed(address(addressProvider), address(vault), address(0));
68+
}
69+
70+
/// @notice U:[TVPF-2]: Constructor sets correct values
71+
function test_TVPF_02_constructor_sets_correct_values() public {
72+
assertEq(vaultPriceFeed.description(), "Mock vault priceFeed", "Incorrect description");
73+
assertEq(vaultPriceFeed.vault(), address(vault), "Incorrect vault");
74+
assertEq(vaultPriceFeed.assetPriceFeed(), address(assetPriceFeed), "Incorrect assetPriceFeed");
75+
assertEq(vaultPriceFeed.vaultShareUnit(), 10 ** 18, "Incorrect vaultShareUnit");
76+
assertEq(vaultPriceFeed.underlyingUnit(), 10 ** 18, "Incorrect underlyingUnit");
77+
assertEq(vaultPriceFeed.lowerBound(), 1 ether, "Incorrect lowerBound");
78+
assertEq(vaultPriceFeed.upperBound(), 1.02 ether, "Incorrect upperBound");
79+
}
80+
81+
/// @notice U:[TVPF-3]: `latestRoundData` reverts on incorrect asset price
82+
function test_TVPF_03_latestRoundData_reverts_on_incorrect_asset_price() public {
83+
assetPriceFeed.setPrice(0);
84+
85+
vm.expectRevert(ZeroPriceException.selector);
86+
vaultPriceFeed.latestRoundData();
87+
}
88+
89+
struct LatestRoundDataTestCase {
90+
string name;
91+
// scenario
92+
uint256 exchangeRate;
93+
// outcome
94+
bool mustRevert;
95+
int256 expectedAnswer;
96+
}
97+
98+
/// @notice U:[TVPF-4]: `latestRoundData` works as expected
99+
function test_TVPF_04_latestRoundData_works_as_expected() public {
100+
LatestRoundDataTestCase[3] memory cases = [
101+
LatestRoundDataTestCase({
102+
name: "exchangeRate below lower bound",
103+
exchangeRate: 0.99 ether,
104+
mustRevert: true,
105+
expectedAnswer: 0
106+
}),
107+
LatestRoundDataTestCase({
108+
name: "exchangeRate within bounds",
109+
exchangeRate: 1.01 ether,
110+
mustRevert: false,
111+
expectedAnswer: 1010 // 1.01 * 1000
112+
}),
113+
LatestRoundDataTestCase({
114+
name: "exchangeRate above upper bound",
115+
exchangeRate: 1.03 ether,
116+
mustRevert: false,
117+
expectedAnswer: 1020 // 1.02 * 1000 (not 1.03 * 1000)
118+
})
119+
];
120+
121+
for (uint256 i; i < cases.length; ++i) {
122+
vault.setAssetsPerShare(cases[i].exchangeRate);
123+
124+
if (cases[i].mustRevert) {
125+
vm.expectRevert(ValueOutOfRangeException.selector);
126+
}
127+
128+
(, int256 answer,,,) = vaultPriceFeed.latestRoundData();
129+
130+
if (!cases[i].mustRevert) {
131+
assertEq(answer, cases[i].expectedAnswer, string.concat("Incorrect answer, case ", cases[i].name));
132+
}
133+
}
134+
}
135+
136+
/// @notice U:[TVPF-5]: `setLimiter` reverts on exchange rate out of bounds
137+
function test_TVPF_05_setLimiter_reverts_on_exchange_rate_out_of_bounds() public {
138+
vault.setAssetsPerShare(1.5 ether);
139+
140+
vm.expectRevert(IncorrectLimitsException.selector);
141+
vm.prank(CONFIGURATOR);
142+
vaultPriceFeed.setLimiter(1 ether);
143+
144+
vault.setAssetsPerShare(0.5 ether);
145+
146+
vm.expectRevert(IncorrectLimitsException.selector);
147+
vm.prank(CONFIGURATOR);
148+
vaultPriceFeed.setLimiter(1 ether);
149+
}
150+
}

contracts/test/unit/RedstonePriceFeed.unit.t.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: UNLICENSED
22
// Gearbox Protocol. Generalized leverage for DeFi protocols
3-
// (c) Gearbox Holdings, 2022
3+
// (c) Gearbox Foundation, 2023.
44
pragma solidity ^0.8.10;
55

66
import {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"dependencies": {
4040
"@gearbox-protocol/core-v2": "1.19.0-base.5",
4141
"@gearbox-protocol/core-v3": "^1.27.4",
42-
"@gearbox-protocol/sdk": "^1.25.0",
42+
"@gearbox-protocol/sdk": "^1.26.1",
4343
"@redstone-finance/evm-connector": "^0.2.2"
4444
},
4545
"devDependencies": {

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,10 +1073,10 @@
10731073
deep-eql "^4.1.0"
10741074
moment "^2.29.4"
10751075

1076-
"@gearbox-protocol/sdk@^1.25.0":
1077-
version "1.25.0"
1078-
resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk/-/sdk-1.25.0.tgz#cacffde081642a7970afca8e085bc8853ba16c74"
1079-
integrity sha512-K3C/yc9VEYmCZ4HIfDqhUdZNRlGxH4sgpWa/FwccNp0sROYkbw2WAKYkzoRXfH6YScNkateLzYTUgDJ+tUNeSg==
1076+
"@gearbox-protocol/sdk@^1.26.1":
1077+
version "1.26.1"
1078+
resolved "https://registry.yarnpkg.com/@gearbox-protocol/sdk/-/sdk-1.26.1.tgz#6de4d758725d5c96602b006af2b195b546ec985c"
1079+
integrity sha512-I6HmOh5a/gGJgOK1KObqiQdcLU7VCngKqu0NZqPFNCanaO/xAuOiyMan6vb2/E/pam0IEL+ji98jYojZGcC+jA==
10801080
dependencies:
10811081
"@types/deep-eql" "^4.0.0"
10821082
axios "^1.2.6"

0 commit comments

Comments
 (0)