|
| 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