Skip to content

Commit b055af8

Browse files
authored
test: horizon e2e (#1)
1 parent 039dac3 commit b055af8

File tree

1 file changed

+353
-0
lines changed

1 file changed

+353
-0
lines changed

src/ProtocolV3HorizonTestBase.sol

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {console2 as console} from 'forge-std/console2.sol';
5+
import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/IERC20.sol';
6+
import {IPool, IPoolAddressesProvider, IPoolConfigurator} from 'aave-address-book/AaveV3.sol';
7+
import {AaveV3EthereumAssets} from 'aave-address-book/AaveV3Ethereum.sol';
8+
import {ProtocolV3TestBase, ReserveConfig} from 'src/ProtocolV3TestBase.sol';
9+
10+
/**
11+
* @dev Adapted from ProtocolV3TestBase for the Horizon market (currently at Aave v3.3).
12+
* - GHO is listed as a normal reserve like prime market, removes special branches.
13+
* - Enable eMode before supplying if available.
14+
* - Skip liquidation with receiveAToken=true when collateral is RWA, since it is disabled.
15+
* - Adds helper to return all actors used in E2E test such that they may be whitelisted to hold RWA tokens.
16+
* - Update errors to v3.3 string format.
17+
*/
18+
abstract contract ProtocolV3HorizonTestBase is ProtocolV3TestBase {
19+
string public constant BORROW_CAP_EXCEEDED = '50';
20+
string public constant SUPPLY_CAP_EXCEEDED = '51';
21+
22+
/**
23+
* @dev runs the default test suite that should run on any proposal touching the aave protocol which includes:
24+
* - diffing the config
25+
* - checking if the changes are plausible (no conflicting config changes etc)
26+
* - running an e2e testsuite over all assets
27+
*/
28+
function defaultTest_v3_3(
29+
string memory reportName,
30+
IPool pool,
31+
address payload
32+
) public returns (ReserveConfig[] memory, ReserveConfig[] memory) {
33+
string memory beforeString = string(abi.encodePacked(reportName, '_before'));
34+
ReserveConfig[] memory configBefore = createConfigurationSnapshot(beforeString, pool);
35+
36+
uint256 startGas = gasleft();
37+
38+
vm.startStateDiffRecording();
39+
executePayload(vm, payload);
40+
string memory rawDiff = vm.getStateDiffJson();
41+
42+
uint256 gasUsed = startGas - gasleft();
43+
assertLt(gasUsed, (block.gaslimit * 95) / 100, 'BLOCK_GAS_LIMIT_EXCEEDED'); // 5% is kept as a buffer
44+
45+
string memory afterString = string(abi.encodePacked(reportName, '_after'));
46+
ReserveConfig[] memory configAfter = createConfigurationSnapshot(afterString, pool);
47+
string memory output = vm.serializeString('root', 'raw', rawDiff);
48+
vm.writeJson(output, string(abi.encodePacked('./reports/', afterString, '.json')));
49+
50+
diffReports(beforeString, afterString);
51+
52+
configChangePlausibilityTest(configBefore, configAfter);
53+
54+
e2eTest_v3_3(pool);
55+
return (configBefore, configAfter);
56+
}
57+
58+
/**
59+
* @dev Makes a e2e test including withdrawals/borrows and supplies to various reserves.
60+
* @param pool the pool that should be tested
61+
*/
62+
function e2eTest_v3_3(IPool pool) public {
63+
ReserveConfig[] memory configs = _getReservesConfigs(pool);
64+
ReserveConfig memory collateralConfig = _goodCollateral(configs);
65+
uint256 snapshot = vm.snapshotState();
66+
for (uint256 i; i < configs.length; i++) {
67+
if (_includeInE2e(configs[i])) {
68+
e2eTestAsset_v3_3(pool, collateralConfig, configs[i]);
69+
vm.revertToState(snapshot);
70+
} else {
71+
console.log('E2E: TestAsset %s SKIPPED', configs[i].symbol);
72+
}
73+
}
74+
}
75+
76+
struct E2ETestAssetLocalVars {
77+
address collateralSupplier;
78+
address testAssetSupplier;
79+
address liquidator;
80+
uint256 collateralAssetAmount;
81+
uint256 testAssetAmount;
82+
uint256 snapshotAfterDeposits;
83+
uint256 aTokenTotalSupply;
84+
uint256 variableDebtTokenTotalSupply;
85+
uint256 borrowAmount;
86+
uint256 snapshotBeforeRepay;
87+
}
88+
89+
function e2eTestAsset_v3_3(
90+
IPool pool,
91+
ReserveConfig memory collateralConfig,
92+
ReserveConfig memory testAssetConfig
93+
) public {
94+
console.log(
95+
'E2E: Collateral %s, TestAsset %s',
96+
collateralConfig.symbol,
97+
testAssetConfig.symbol
98+
);
99+
E2ETestAssetLocalVars memory vars;
100+
vars.collateralSupplier = makeAddr('collateralSupplier');
101+
vars.testAssetSupplier = makeAddr('testAssetSupplier');
102+
vars.liquidator = makeAddr('liquidator');
103+
require(collateralConfig.usageAsCollateralEnabled, 'COLLATERAL_CONFIG_MUST_BE_COLLATERAL');
104+
vars.collateralAssetAmount = _getTokenAmountByDollarValue(pool, collateralConfig, 100_000);
105+
vars.testAssetAmount = _getTokenAmountByDollarValue(pool, testAssetConfig, 10_000);
106+
107+
// remove caps as they should not prevent testing
108+
IPoolAddressesProvider addressesProvider = IPoolAddressesProvider(pool.ADDRESSES_PROVIDER());
109+
IPoolConfigurator poolConfigurator = IPoolConfigurator(addressesProvider.getPoolConfigurator());
110+
vm.startPrank(addressesProvider.getACLAdmin());
111+
if (collateralConfig.supplyCap != 0) {
112+
poolConfigurator.setSupplyCap(collateralConfig.underlying, 0);
113+
}
114+
if (testAssetConfig.supplyCap != 0) {
115+
poolConfigurator.setSupplyCap(testAssetConfig.underlying, 0);
116+
}
117+
if (testAssetConfig.borrowCap != 0) {
118+
poolConfigurator.setBorrowCap(testAssetConfig.underlying, 0);
119+
}
120+
vm.stopPrank();
121+
122+
_enableIfEMode(collateralConfig, pool, vars.collateralSupplier);
123+
_deposit(collateralConfig, pool, vars.collateralSupplier, vars.collateralAssetAmount);
124+
_deposit(testAssetConfig, pool, vars.testAssetSupplier, vars.testAssetAmount);
125+
126+
uint256 snapshotAfterDeposits = vm.snapshotState();
127+
128+
// test deposits and withdrawals
129+
vars.aTokenTotalSupply = IERC20(testAssetConfig.aToken).totalSupply();
130+
vars.variableDebtTokenTotalSupply = IERC20(testAssetConfig.variableDebtToken).totalSupply();
131+
132+
vm.prank(addressesProvider.getACLAdmin());
133+
poolConfigurator.setSupplyCap(
134+
testAssetConfig.underlying,
135+
vars.aTokenTotalSupply / 10 ** testAssetConfig.decimals + 1
136+
);
137+
vm.prank(addressesProvider.getACLAdmin());
138+
poolConfigurator.setBorrowCap(
139+
testAssetConfig.underlying,
140+
vars.variableDebtTokenTotalSupply / 10 ** testAssetConfig.decimals + 1
141+
);
142+
143+
// caps should revert when supplying slightly more
144+
vm.expectRevert(bytes(SUPPLY_CAP_EXCEEDED));
145+
vm.prank(vars.testAssetSupplier);
146+
pool.deposit({
147+
asset: testAssetConfig.underlying,
148+
amount: 11 ** testAssetConfig.decimals,
149+
onBehalfOf: vars.testAssetSupplier,
150+
referralCode: 0
151+
});
152+
if (testAssetConfig.borrowingEnabled) {
153+
vars.borrowAmount = 11 ** testAssetConfig.decimals;
154+
155+
if (vars.aTokenTotalSupply < vars.borrowAmount) {
156+
vm.prank(addressesProvider.getACLAdmin());
157+
poolConfigurator.setSupplyCap(testAssetConfig.underlying, 0);
158+
159+
// aTokenTotalSupply == 10'000$
160+
// borrowAmount > 10'000$
161+
// need to add more test asset in order to be able to borrow it
162+
// right now there is not enough underlying tokens in the aToken
163+
_deposit(
164+
testAssetConfig,
165+
pool,
166+
vars.testAssetSupplier,
167+
vars.borrowAmount - vars.aTokenTotalSupply
168+
);
169+
170+
// need to add more collateral in order to be able to borrow
171+
// collateralAssetAmount == 100'000$
172+
_deposit(
173+
collateralConfig,
174+
pool,
175+
vars.collateralSupplier,
176+
(vars.collateralAssetAmount * vars.borrowAmount) / vars.aTokenTotalSupply
177+
);
178+
}
179+
180+
vm.expectRevert(bytes(BORROW_CAP_EXCEEDED));
181+
vm.prank(vars.collateralSupplier);
182+
pool.borrow({
183+
asset: testAssetConfig.underlying,
184+
amount: vars.borrowAmount,
185+
interestRateMode: 2,
186+
referralCode: 0,
187+
onBehalfOf: vars.collateralSupplier
188+
});
189+
}
190+
191+
vm.revertToState(snapshotAfterDeposits);
192+
193+
_withdraw(testAssetConfig, pool, vars.testAssetSupplier, vars.testAssetAmount / 2);
194+
_withdraw(testAssetConfig, pool, vars.testAssetSupplier, type(uint256).max);
195+
196+
vm.revertToState(snapshotAfterDeposits);
197+
198+
// test borrows, repayments and liquidations
199+
if (testAssetConfig.borrowingEnabled) {
200+
// test borrowing and repayment
201+
_borrow({
202+
config: testAssetConfig,
203+
pool: pool,
204+
user: vars.collateralSupplier,
205+
amount: vars.testAssetAmount
206+
});
207+
208+
uint256 snapshotBeforeRepay = vm.snapshotState();
209+
210+
_repay({
211+
config: testAssetConfig,
212+
pool: pool,
213+
user: vars.collateralSupplier,
214+
amount: vars.testAssetAmount,
215+
withATokens: false
216+
});
217+
218+
vm.revertToState(snapshotBeforeRepay);
219+
220+
_repay({
221+
config: testAssetConfig,
222+
pool: pool,
223+
user: vars.collateralSupplier,
224+
amount: vars.testAssetAmount,
225+
withATokens: true
226+
});
227+
228+
vm.revertToState(snapshotAfterDeposits);
229+
230+
// test liquidations
231+
_borrow({
232+
config: testAssetConfig,
233+
pool: pool,
234+
user: vars.collateralSupplier,
235+
amount: vars.testAssetAmount
236+
});
237+
238+
if (testAssetConfig.underlying != collateralConfig.underlying) {
239+
_changeAssetPrice(pool, testAssetConfig, 1000_00); // price increases to 1'000%
240+
} else {
241+
_setAssetLtvAndLiquidationThreshold({
242+
pool: pool,
243+
config: testAssetConfig,
244+
newLtv: 5_00,
245+
newLiquidationThreshold: 5_00
246+
});
247+
}
248+
249+
uint256 snapshotBeforeLiquidation = vm.snapshotState();
250+
251+
// receive underlying tokens
252+
_liquidationCall({
253+
collateralConfig: collateralConfig,
254+
debtConfig: testAssetConfig,
255+
pool: pool,
256+
liquidator: makeAddr('liquidator'),
257+
borrower: vars.collateralSupplier,
258+
debtToCover: type(uint256).max,
259+
receiveAToken: false
260+
});
261+
262+
vm.revertToState(snapshotBeforeLiquidation);
263+
264+
// receive ATokens
265+
if (!_isRwaToken(collateralConfig)) {
266+
// cannot receive ATokens for RWA collateral liquidations
267+
_liquidationCall({
268+
collateralConfig: collateralConfig,
269+
debtConfig: testAssetConfig,
270+
pool: pool,
271+
liquidator: makeAddr('liquidator'),
272+
borrower: vars.collateralSupplier,
273+
debtToCover: type(uint256).max,
274+
receiveAToken: true
275+
});
276+
}
277+
278+
vm.revertToState(snapshotAfterDeposits);
279+
}
280+
281+
// test flashloans
282+
if (testAssetConfig.isFlashloanable) {
283+
_flashLoan({
284+
config: testAssetConfig,
285+
pool: pool,
286+
user: vars.collateralSupplier,
287+
receiverAddress: address(this),
288+
amount: vars.testAssetAmount,
289+
interestRateMode: 0
290+
});
291+
292+
if (testAssetConfig.borrowingEnabled) {
293+
_flashLoan({
294+
config: testAssetConfig,
295+
pool: pool,
296+
user: vars.collateralSupplier,
297+
receiverAddress: address(this),
298+
amount: vars.testAssetAmount,
299+
interestRateMode: 2
300+
});
301+
}
302+
}
303+
}
304+
305+
function _isRwaToken(ReserveConfig memory config) internal view returns (bool) {
306+
address RWA_A_TOKEN_INSTANCE = 0x1d0Da70de08987b1888befECe0024443Bf3c2450;
307+
bytes32 IMPL_SLOT = bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);
308+
return address(uint160(uint256(vm.load(config.aToken, IMPL_SLOT)))) == RWA_A_TOKEN_INSTANCE;
309+
}
310+
311+
function _enableIfEMode(ReserveConfig memory config, IPool pool, address user) internal {
312+
vm.prank(user);
313+
pool.setUserEMode(0);
314+
315+
// eMode id 0 is skipped intentionally as it is the reserved default
316+
for (uint8 id = 1; id < 256; ++id) {
317+
uint256 reserveId = pool.getReserveData(config.underlying).id;
318+
if ((pool.getEModeCategoryCollateralBitmap(id) >> reserveId) & 1 != 0) {
319+
vm.prank(user);
320+
pool.setUserEMode(id);
321+
break;
322+
}
323+
}
324+
}
325+
326+
/**
327+
* @dev returns a "good" collateral in the list
328+
*/
329+
function _goodCollateral(
330+
ReserveConfig[] memory configs
331+
) internal pure returns (ReserveConfig memory config) {
332+
for (uint256 i = 0; i < configs.length; i++) {
333+
if (
334+
// not frozen etc
335+
// usable as collateral
336+
// not isolated asset as we can only borrow stablecoins against it
337+
// ltv is not 0
338+
_includeInE2e(configs[i]) &&
339+
configs[i].usageAsCollateralEnabled &&
340+
configs[i].debtCeiling == 0 &&
341+
configs[i].ltv != 0
342+
) return configs[i];
343+
}
344+
revert('ERROR: No usable collateral found');
345+
}
346+
347+
function _testActors() internal returns (address[] memory actors) {
348+
actors = new address[](3);
349+
actors[0] = makeAddr('collateralSupplier');
350+
actors[1] = makeAddr('testAssetSupplier');
351+
actors[2] = makeAddr('liquidator');
352+
}
353+
}

0 commit comments

Comments
 (0)