Skip to content

Commit 20908d9

Browse files
committed
feat: e2e test
1 parent dd372a1 commit 20908d9

File tree

1 file changed

+354
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)