Skip to content

Commit 25cb9a3

Browse files
authored
test: dynamic oracle bounds & param registry (#36)
1 parent 2d7d378 commit 25cb9a3

File tree

3 files changed

+462
-0
lines changed

3 files changed

+462
-0
lines changed
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import {console2 as console} from 'forge-std/console2.sol';
5+
6+
import {Test, Vm} from 'forge-std/Test.sol';
7+
import {IAaveOracle} from '../../src/contracts/interfaces/IAaveOracle.sol';
8+
import {IPool} from '../../src/contracts/interfaces/IPool.sol';
9+
import {AggregatorInterface} from '../../src/contracts/dependencies/chainlink/AggregatorInterface.sol';
10+
11+
import {AaveV3HorizonEthereum} from './utils/AaveV3HorizonEthereum.sol';
12+
13+
import {IParameterRegistry} from './dependencies/IParameterRegistry.sol';
14+
15+
abstract contract OracleDynamicBoundsTestBase is Test {
16+
address constant USTB_NEW_AGGREGATOR = 0x267D0DD05fbc989565C521e0B8882f61027FF32A;
17+
address constant USCC_NEW_AGGREGATOR = 0x2d7Cd12f24bD28684847bF3e4317899a4Db53c58;
18+
address constant USYC_NEW_AGGREGATOR = 0x3C405e1FE8a6BE5d9b714B8C88Ad913F236B1639;
19+
address constant JTRSY_NEW_AGGREGATOR = 0xcf8683fFdFC4b871DF35D05bc763F239612e7272;
20+
address constant JAAA_NEW_AGGREGATOR = 0x3a8E8491236368a582b651786bEdA49BD5c3BA7B;
21+
address constant VBILL_NEW_AGGREGATOR = 0x04d81C346252E31Ee888393AF6E2037a9a4d70Af;
22+
23+
struct ExpectedParams {
24+
uint64 maxExpectedApy;
25+
uint32 upperBoundTolerance;
26+
uint32 lowerBoundTolerance;
27+
uint32 maxDiscount;
28+
uint80 lookbackWindowSize;
29+
bool isUpperBoundEnabled;
30+
bool isLowerBoundEnabled;
31+
bool isActionTakingEnabled;
32+
}
33+
34+
struct NewAggregator {
35+
address aggregator;
36+
}
37+
38+
mapping(address => ExpectedParams) internal expectedParams; // asset => expected params
39+
mapping(address => NewAggregator) internal newAggregators; // asset => new aggregator
40+
41+
IAaveOracle internal aaveOracle;
42+
IParameterRegistry internal parameterRegistry;
43+
function setUp() public virtual {
44+
parameterRegistry = IParameterRegistry(AaveV3HorizonEthereum.RWA_ORACLE_PARAMS_REGISTRY);
45+
}
46+
47+
function test_asset(address asset, address oracleSource, bool isAdapter) internal {
48+
oracleSource = test_horizon_adapter(asset, oracleSource, isAdapter);
49+
test_registry_params(asset);
50+
test_lookback_data(asset);
51+
int256 newAggregatorPrice = test_new_aggregator(asset);
52+
test_matching_price_data(oracleSource, newAggregatorPrice);
53+
}
54+
55+
// check that the price from the oracle source is the same as the price from the new aggregator
56+
function test_matching_price_data(address oracleSource, int256 newAggregatorPrice) internal {
57+
// current horizon feed price
58+
(bool success, bytes memory data) = oracleSource.call(
59+
abi.encodeWithSignature('latestAnswer()')
60+
);
61+
require(success, 'Failed to call latestAnswer()');
62+
int256 price = abi.decode(data, (int256));
63+
64+
assertApproxEqRel(price, newAggregatorPrice, 1e12, 'price');
65+
}
66+
67+
// test param registry params are configured properly
68+
function test_registry_params(address asset) internal {
69+
assertEq(parameterRegistry.assetExists(asset), true, 'assetExists');
70+
(
71+
uint64 maxExpectedApy,
72+
uint32 upperBoundTolerance,
73+
uint32 lowerBoundTolerance,
74+
uint32 maxDiscount,
75+
uint80 lookbackWindowSize,
76+
bool isUpperBoundEnabled,
77+
bool isLowerBoundEnabled,
78+
bool isActionTakingEnabled
79+
) = parameterRegistry.getParametersForAsset(asset);
80+
81+
ExpectedParams memory expectedParam = expectedParams[asset];
82+
83+
assertEq(maxExpectedApy, expectedParam.maxExpectedApy, 'maxExpectedApy');
84+
assertEq(upperBoundTolerance, expectedParam.upperBoundTolerance, 'upperBoundTolerance');
85+
assertEq(lowerBoundTolerance, expectedParam.lowerBoundTolerance, 'lowerBoundTolerance');
86+
assertEq(maxDiscount, expectedParam.maxDiscount, 'maxDiscount');
87+
assertEq(lookbackWindowSize, expectedParam.lookbackWindowSize, 'lookbackWindowSize');
88+
assertEq(isUpperBoundEnabled, expectedParam.isUpperBoundEnabled, 'isUpperBoundEnabled');
89+
assertEq(isLowerBoundEnabled, expectedParam.isLowerBoundEnabled, 'isLowerBoundEnabled');
90+
assertEq(isActionTakingEnabled, expectedParam.isActionTakingEnabled, 'isActionTakingEnabled');
91+
}
92+
93+
/// test that the oracle source from horizon protocol adapter is the same as the oracle address from the param registry
94+
function test_horizon_adapter(
95+
address asset,
96+
address oracleSource,
97+
bool isAdapter
98+
) internal returns (address) {
99+
bool success;
100+
bytes memory data;
101+
if (isAdapter) {
102+
// if adapter, get oracle source from horizon adapter
103+
(success, data) = oracleSource.call(abi.encodeWithSignature('source()'));
104+
require(success, 'Failed to call source()');
105+
oracleSource = abi.decode(data, (address));
106+
}
107+
address paramRegistryOracle = _getParamRegistryOracle(asset);
108+
assertEq(paramRegistryOracle, oracleSource, 'paramRegistryOracle');
109+
110+
return oracleSource;
111+
}
112+
113+
// test look back data from param registry is valid
114+
function test_lookback_data(address asset) internal {
115+
(
116+
uint80 roundId,
117+
int256 answer,
118+
uint256 startedAt,
119+
uint256 updatedAt,
120+
uint80 answeredInRound
121+
) = parameterRegistry.getLookbackData(asset);
122+
123+
assertGt(roundId, 0, 'lookback roundId');
124+
assertGt(answer, 0, 'lookback answer');
125+
assertApproxEqAbs(
126+
startedAt,
127+
vm.getBlockTimestamp() - expectedParams[asset].lookbackWindowSize * 1 days, // within expected lookback window
128+
1 days * 1.5, // account for differences in update times throughout the day
129+
'lookback startedAt'
130+
);
131+
assertApproxEqAbs(
132+
updatedAt,
133+
vm.getBlockTimestamp() - expectedParams[asset].lookbackWindowSize * 1 days, // within expected lookback window
134+
1 days * 1.5, // account for differences in update times throughout the day
135+
'lookback updatedAt'
136+
);
137+
assertGt(answeredInRound, 0, 'lookback answeredInRound');
138+
}
139+
140+
// test new aggregator data is valid; enough rounds for lookback window and valid answers
141+
function test_new_aggregator(address asset) internal returns (int256) {
142+
address paramRegistryOracle = _getParamRegistryOracle(asset);
143+
vm.startPrank(paramRegistryOracle); // has access to price feed
144+
(
145+
uint80 roundId,
146+
int256 answer,
147+
uint256 startedAt,
148+
uint256 updatedAt,
149+
uint80 answeredInRound
150+
) = AggregatorInterface(newAggregators[asset].aggregator).latestRoundData();
151+
vm.stopPrank();
152+
153+
assertGt(roundId, expectedParams[asset].lookbackWindowSize, 'roundId');
154+
assertGt(answer, 0, 'answer');
155+
assertApproxEqAbs(startedAt, vm.getBlockTimestamp(), 1 days, 'startedAt');
156+
assertApproxEqAbs(updatedAt, vm.getBlockTimestamp(), 1 days, 'updatedAt');
157+
assertGt(answeredInRound, expectedParams[asset].lookbackWindowSize, 'answeredInRound');
158+
159+
return answer;
160+
}
161+
162+
// read oracle address from param registry
163+
function _getParamRegistryOracle(address asset) internal returns (address) {
164+
return parameterRegistry.getOracle(asset);
165+
}
166+
}
167+
168+
/// forge-config: default.evm_version = "cancun"
169+
contract OracleDynamicBoundsTest is OracleDynamicBoundsTestBase {
170+
function setUp() public virtual override {
171+
super.setUp();
172+
vm.createSelectFork('mainnet', 23478406);
173+
_initEnvironment();
174+
}
175+
176+
ExpectedParams internal USTB_EXPECTED_PARAMS =
177+
ExpectedParams({
178+
maxExpectedApy: 415,
179+
upperBoundTolerance: 15,
180+
lowerBoundTolerance: 5,
181+
maxDiscount: 10,
182+
lookbackWindowSize: 4,
183+
isUpperBoundEnabled: true,
184+
isLowerBoundEnabled: true,
185+
isActionTakingEnabled: false
186+
});
187+
ExpectedParams internal USCC_EXPECTED_PARAMS =
188+
ExpectedParams({
189+
maxExpectedApy: 2500,
190+
upperBoundTolerance: 50,
191+
lowerBoundTolerance: 10,
192+
maxDiscount: 40,
193+
lookbackWindowSize: 4,
194+
isUpperBoundEnabled: true,
195+
isLowerBoundEnabled: true,
196+
isActionTakingEnabled: false
197+
});
198+
ExpectedParams internal USYC_EXPECTED_PARAMS =
199+
ExpectedParams({
200+
maxExpectedApy: 420,
201+
upperBoundTolerance: 15,
202+
lowerBoundTolerance: 5,
203+
maxDiscount: 10,
204+
lookbackWindowSize: 4,
205+
isUpperBoundEnabled: true,
206+
isLowerBoundEnabled: true,
207+
isActionTakingEnabled: false
208+
});
209+
ExpectedParams internal JTRSY_EXPECTED_PARAMS =
210+
ExpectedParams({
211+
maxExpectedApy: 390,
212+
upperBoundTolerance: 15,
213+
lowerBoundTolerance: 5,
214+
maxDiscount: 10,
215+
lookbackWindowSize: 4,
216+
isUpperBoundEnabled: true,
217+
isLowerBoundEnabled: true,
218+
isActionTakingEnabled: false
219+
});
220+
ExpectedParams internal JAAA_EXPECTED_PARAMS =
221+
ExpectedParams({
222+
maxExpectedApy: 520,
223+
upperBoundTolerance: 50,
224+
lowerBoundTolerance: 10,
225+
maxDiscount: 75,
226+
lookbackWindowSize: 4,
227+
isUpperBoundEnabled: true,
228+
isLowerBoundEnabled: true,
229+
isActionTakingEnabled: false
230+
});
231+
ExpectedParams internal VBILL_EXPECTED_PARAMS =
232+
ExpectedParams({
233+
maxExpectedApy: 0,
234+
upperBoundTolerance: 10,
235+
lowerBoundTolerance: 10,
236+
maxDiscount: 0,
237+
lookbackWindowSize: 4,
238+
isUpperBoundEnabled: true,
239+
isLowerBoundEnabled: true,
240+
isActionTakingEnabled: false
241+
});
242+
243+
function _initEnvironment() internal virtual {
244+
expectedParams[AaveV3HorizonEthereum.USTB_ADDRESS] = USTB_EXPECTED_PARAMS;
245+
expectedParams[AaveV3HorizonEthereum.USCC_ADDRESS] = USCC_EXPECTED_PARAMS;
246+
expectedParams[AaveV3HorizonEthereum.USYC_ADDRESS] = USYC_EXPECTED_PARAMS;
247+
expectedParams[AaveV3HorizonEthereum.JTRSY_ADDRESS] = JTRSY_EXPECTED_PARAMS;
248+
expectedParams[AaveV3HorizonEthereum.JAAA_ADDRESS] = JAAA_EXPECTED_PARAMS;
249+
expectedParams[AaveV3HorizonEthereum.VBILL_ADDRESS] = VBILL_EXPECTED_PARAMS;
250+
251+
newAggregators[AaveV3HorizonEthereum.USTB_ADDRESS] = NewAggregator({
252+
aggregator: USTB_NEW_AGGREGATOR
253+
});
254+
newAggregators[AaveV3HorizonEthereum.USCC_ADDRESS] = NewAggregator({
255+
aggregator: USCC_NEW_AGGREGATOR
256+
});
257+
newAggregators[AaveV3HorizonEthereum.USYC_ADDRESS] = NewAggregator({
258+
aggregator: USYC_NEW_AGGREGATOR
259+
});
260+
newAggregators[AaveV3HorizonEthereum.JTRSY_ADDRESS] = NewAggregator({
261+
aggregator: JTRSY_NEW_AGGREGATOR
262+
});
263+
newAggregators[AaveV3HorizonEthereum.JAAA_ADDRESS] = NewAggregator({
264+
aggregator: JAAA_NEW_AGGREGATOR
265+
});
266+
newAggregators[AaveV3HorizonEthereum.VBILL_ADDRESS] = NewAggregator({
267+
aggregator: VBILL_NEW_AGGREGATOR
268+
});
269+
270+
aaveOracle = IAaveOracle(
271+
IPool(AaveV3HorizonEthereum.POOL).ADDRESSES_PROVIDER().getPriceOracle()
272+
);
273+
}
274+
275+
// check that param registry admin are set properly
276+
function test_registry_admin() external {
277+
assertEq(parameterRegistry.owner(), AaveV3HorizonEthereum.HORIZON_OPS, 'owner');
278+
assertEq(parameterRegistry.updater(), AaveV3HorizonEthereum.HORIZON_OPS, 'updater');
279+
}
280+
281+
function test_ustb() external virtual {
282+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USTB_ADDRESS);
283+
test_asset(AaveV3HorizonEthereum.USTB_ADDRESS, oracleSource, true);
284+
}
285+
286+
function test_uscc() external virtual {
287+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USCC_ADDRESS);
288+
test_asset(AaveV3HorizonEthereum.USCC_ADDRESS, oracleSource, true);
289+
}
290+
291+
function test_usyc() external virtual {
292+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USYC_ADDRESS);
293+
test_asset(AaveV3HorizonEthereum.USYC_ADDRESS, oracleSource, false);
294+
}
295+
296+
function test_jtrsy() external virtual {
297+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.JTRSY_ADDRESS);
298+
test_asset(AaveV3HorizonEthereum.JTRSY_ADDRESS, oracleSource, true);
299+
}
300+
301+
function test_jaaa() external virtual {
302+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.JAAA_ADDRESS);
303+
test_asset(AaveV3HorizonEthereum.JAAA_ADDRESS, oracleSource, true);
304+
}
305+
306+
function test_vbill() external virtual {
307+
// VBILL not deployed yet, get price feed directly from lib
308+
test_asset(AaveV3HorizonEthereum.VBILL_ADDRESS, AaveV3HorizonEthereum.VBILL_PRICE_FEED, false);
309+
}
310+
}
311+
312+
/// forge-config: default.evm_version = "cancun"
313+
contract OracleDynamicBoundsPostMigrationTest is OracleDynamicBoundsTest {
314+
function setUp() public virtual override {
315+
super.setUp();
316+
vm.createSelectFork('mainnet', 23483206);
317+
_initEnvironment();
318+
}
319+
320+
function test_ustb() public virtual override {
321+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USTB_ADDRESS);
322+
_printAssetPrice(AaveV3HorizonEthereum.USTB_ADDRESS, oracleSource);
323+
test_aggregator_from_registry(AaveV3HorizonEthereum.USTB_ADDRESS, USTB_NEW_AGGREGATOR);
324+
}
325+
326+
function test_uscc() public virtual override {
327+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USCC_ADDRESS);
328+
_printAssetPrice(AaveV3HorizonEthereum.USCC_ADDRESS, oracleSource);
329+
test_aggregator_from_registry(AaveV3HorizonEthereum.USCC_ADDRESS, USCC_NEW_AGGREGATOR);
330+
}
331+
332+
function test_usyc() public virtual override {
333+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.USYC_ADDRESS);
334+
_printAssetPrice(AaveV3HorizonEthereum.USYC_ADDRESS, oracleSource);
335+
test_aggregator_from_registry(AaveV3HorizonEthereum.USYC_ADDRESS, USYC_NEW_AGGREGATOR);
336+
}
337+
338+
function test_jtrsy() public virtual override {
339+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.JTRSY_ADDRESS);
340+
_printAssetPrice(AaveV3HorizonEthereum.JTRSY_ADDRESS, oracleSource);
341+
test_aggregator_from_registry(AaveV3HorizonEthereum.JTRSY_ADDRESS, JTRSY_NEW_AGGREGATOR);
342+
}
343+
344+
function test_jaaa() public virtual override {
345+
address oracleSource = aaveOracle.getSourceOfAsset(AaveV3HorizonEthereum.JAAA_ADDRESS);
346+
_printAssetPrice(AaveV3HorizonEthereum.JAAA_ADDRESS, oracleSource);
347+
test_aggregator_from_registry(AaveV3HorizonEthereum.JAAA_ADDRESS, JAAA_NEW_AGGREGATOR);
348+
}
349+
350+
function test_vbill() public virtual override {
351+
_printAssetPrice(AaveV3HorizonEthereum.VBILL_ADDRESS, AaveV3HorizonEthereum.VBILL_PRICE_FEED);
352+
test_aggregator_from_registry(AaveV3HorizonEthereum.VBILL_ADDRESS, VBILL_NEW_AGGREGATOR);
353+
}
354+
355+
function _printAssetPrice(address asset, address oracleSource) internal {
356+
(bool success, bytes memory data) = oracleSource.call(
357+
abi.encodeWithSignature('latestAnswer()')
358+
);
359+
require(success, 'Failed to call latestAnswer()');
360+
int256 price = abi.decode(data, (int256));
361+
console.log('asset %s price %8e', asset, uint256(price));
362+
}
363+
364+
// check that oracle from param registry points to new aggregator
365+
function test_aggregator_from_registry(address asset, address newAggregator) internal {
366+
address paramRegistryOracle = _getParamRegistryOracle(asset);
367+
(bool success, bytes memory data) = paramRegistryOracle.call(
368+
abi.encodeWithSignature('aggregator()')
369+
);
370+
require(success, 'Failed to call aggregator()');
371+
address aggregator = abi.decode(data, (address));
372+
assertEq(aggregator, newAggregator, 'aggregator');
373+
}
374+
}

0 commit comments

Comments
 (0)