diff --git a/.gitignore b/.gitignore index b1cd1d7d..c199a61c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ out/ # general .env .env.bk +.assets reports/ diffs/ downloads/ diff --git a/README.md b/README.md index 67c239b0..55f8caaa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Aave V3.1 Origin -![Aave v3.1 Origin_banner](./v3-1-banner.jpeg) +![Aave v3.1 Origin_banner](./resources/v3-1-banner.jpeg) Aave v3.1 complete codebase, Foundry-based. diff --git a/reports/.empty b/reports/.empty new file mode 100644 index 00000000..e69de29b diff --git a/resources/configs-engine.svg b/resources/configs-engine.svg new file mode 100644 index 00000000..57e4a50a --- /dev/null +++ b/resources/configs-engine.svg @@ -0,0 +1,16 @@ + + + + + + + PoolConfiguratorAave v3 poolAaveV3ConfigEngineEntity with permissions(e.g. Level 1 Executor)PayloadToListAssetA(e.g. Aave v3 Polygon)AaveV3PayloadAaveV3PayloadEthereumAaveV3PayloadPolygonCALLDELEGATECALLDELEGATECALL \ No newline at end of file diff --git a/resources/static-a-token-wrapping.jpeg b/resources/static-a-token-wrapping.jpeg new file mode 100644 index 00000000..99e81e85 Binary files /dev/null and b/resources/static-a-token-wrapping.jpeg differ diff --git a/v3-1-banner.jpeg b/resources/v3-1-banner.jpeg similarity index 100% rename from v3-1-banner.jpeg rename to resources/v3-1-banner.jpeg diff --git a/scripts/misc/DeployAaveV3MarketBatchedBase.sol b/scripts/misc/DeployAaveV3MarketBatchedBase.sol index e6fddd33..25af8793 100644 --- a/scripts/misc/DeployAaveV3MarketBatchedBase.sol +++ b/scripts/misc/DeployAaveV3MarketBatchedBase.sol @@ -25,7 +25,7 @@ abstract contract DeployAaveV3MarketBatchedBase is DeployUtils, MarketInput, Scr (roles, config, flags, report) = _getMarketInput(msg.sender); - _loadWarnings(config); + _loadWarnings(config, flags); vm.startBroadcast(); report = AaveV3BatchOrchestration.deployAaveV3(msg.sender, roles, config, flags, report); @@ -38,12 +38,21 @@ abstract contract DeployAaveV3MarketBatchedBase is DeployUtils, MarketInput, Scr metadataReporter.writeJsonReportMarket(report); } - function _loadWarnings(MarketConfig memory config) internal view { + function _loadWarnings(MarketConfig memory config, DeployFlags memory flags) internal view { if (config.paraswapAugustusRegistry == address(0)) { console.log( 'Warning: Paraswap Adapters will be skipped at deployment due missing config.paraswapAugustusRegistry' ); } + if ( + (flags.l2 && + (config.l2SequencerUptimeFeed == address(0) || + config.l2PriceOracleSentinelGracePeriod == 0)) + ) { + console.log( + 'Warning: L2 Sequencer uptime feed wont be set at deployment due missing config.l2SequencerUptimeFeed config.l2PriceOracleSentinelGracePeriod' + ); + } if ( config.networkBaseTokenPriceInUsdProxyAggregator == address(0) || config.marketReferenceCurrencyPriceInUsdProxyAggregator == address(0) diff --git a/src/core/contracts/flashloan/base/LICENSE.md b/src/core/contracts/flashloan/base/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/core/contracts/flashloan/base/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/core/contracts/flashloan/interfaces/LICENSE.md b/src/core/contracts/flashloan/interfaces/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/core/contracts/flashloan/interfaces/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/core/contracts/interfaces/LICENSE.md b/src/core/contracts/interfaces/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/core/contracts/interfaces/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/core/contracts/misc/interfaces/LICENSE.md b/src/core/contracts/misc/interfaces/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/core/contracts/misc/interfaces/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/deployments/contracts/procedures/AaveV3HelpersProcedureOne.sol b/src/deployments/contracts/procedures/AaveV3HelpersProcedureOne.sol new file mode 100644 index 00000000..d261158f --- /dev/null +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureOne.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {Create2Utils} from '../utilities/Create2Utils.sol'; +import {ConfigEngineReport} from '../../interfaces/IMarketReportTypes.sol'; +import {AaveV3ConfigEngine, IAaveV3ConfigEngine, CapsEngine, BorrowEngine, CollateralEngine, RateEngine, PriceFeedEngine, EModeEngine, ListingEngine} from 'aave-v3-periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; +import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; +import {IPoolConfigurator} from 'aave-v3-core/contracts/interfaces/IPoolConfigurator.sol'; +import {IAaveOracle} from 'aave-v3-core/contracts/interfaces/IAaveOracle.sol'; + +contract AaveV3HelpersProcedureOne { + function _deployConfigEngine( + address pool, + address poolConfigurator, + address defaultInterestRateStrategy, + address aaveOracle, + address rewardsController, + address collector, + address aTokenImpl, + address vTokenImpl, + address sTokenImpl + ) internal returns (ConfigEngineReport memory configEngineReport) { + IAaveV3ConfigEngine.EngineLibraries memory engineLibraries = IAaveV3ConfigEngine + .EngineLibraries({ + listingEngine: Create2Utils._create2Deploy('v1', type(ListingEngine).creationCode), + eModeEngine: Create2Utils._create2Deploy('v1', type(EModeEngine).creationCode), + borrowEngine: Create2Utils._create2Deploy('v1', type(BorrowEngine).creationCode), + collateralEngine: Create2Utils._create2Deploy('v1', type(CollateralEngine).creationCode), + priceFeedEngine: Create2Utils._create2Deploy('v1', type(PriceFeedEngine).creationCode), + rateEngine: Create2Utils._create2Deploy('v1', type(RateEngine).creationCode), + capsEngine: Create2Utils._create2Deploy('v1', type(CapsEngine).creationCode) + }); + + IAaveV3ConfigEngine.EngineConstants memory engineConstants = IAaveV3ConfigEngine + .EngineConstants({ + pool: IPool(pool), + poolConfigurator: IPoolConfigurator(poolConfigurator), + defaultInterestRateStrategy: defaultInterestRateStrategy, + oracle: IAaveOracle(aaveOracle), + rewardsController: rewardsController, + collector: collector + }); + + configEngineReport.listingEngine = engineLibraries.listingEngine; + configEngineReport.eModeEngine = engineLibraries.eModeEngine; + configEngineReport.borrowEngine = engineLibraries.borrowEngine; + configEngineReport.collateralEngine = engineLibraries.collateralEngine; + configEngineReport.priceFeedEngine = engineLibraries.priceFeedEngine; + configEngineReport.rateEngine = engineLibraries.rateEngine; + configEngineReport.capsEngine = engineLibraries.capsEngine; + + configEngineReport.configEngine = address( + new AaveV3ConfigEngine(aTokenImpl, vTokenImpl, sTokenImpl, engineConstants, engineLibraries) + ); + return configEngineReport; + } +} diff --git a/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol new file mode 100644 index 00000000..6d4abb9f --- /dev/null +++ b/src/deployments/contracts/procedures/AaveV3HelpersProcedureTwo.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../interfaces/IMarketReportTypes.sol'; +import {TransparentProxyFactory, ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; +import {StaticATokenLM} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {StaticATokenFactory} from 'aave-v3-periphery/contracts/static-a-token/StaticATokenFactory.sol'; +import {IErrors} from '../../interfaces/IErrors.sol'; + +contract AaveV3HelpersProcedureTwo is IErrors { + function _deployStaticAToken( + address pool, + address rewardsController, + address proxyAdmin + ) internal returns (StaticATokenReport memory staticATokenReport) { + if (proxyAdmin == address(0)) revert ProxyAdminNotFound(); + + staticATokenReport.transparentProxyFactory = address(new TransparentProxyFactory()); + staticATokenReport.staticATokenImplementation = address( + new StaticATokenLM(IPool(pool), IRewardsController(rewardsController)) + ); + staticATokenReport.staticATokenFactoryImplementation = address( + new StaticATokenFactory( + IPool(pool), + proxyAdmin, + ITransparentProxyFactory(staticATokenReport.transparentProxyFactory), + staticATokenReport.staticATokenImplementation + ) + ); + + staticATokenReport.staticATokenFactoryProxy = ITransparentProxyFactory( + staticATokenReport.transparentProxyFactory + ).create( + staticATokenReport.staticATokenFactoryImplementation, + proxyAdmin, + abi.encodeWithSelector(StaticATokenFactory.initialize.selector) + ); + + return staticATokenReport; + } +} diff --git a/src/deployments/contracts/procedures/AaveV3MiscProcedure.sol b/src/deployments/contracts/procedures/AaveV3MiscProcedure.sol new file mode 100644 index 00000000..516ed9d9 --- /dev/null +++ b/src/deployments/contracts/procedures/AaveV3MiscProcedure.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../interfaces/IMarketReportTypes.sol'; +import {PriceOracleSentinel, ISequencerOracle} from 'aave-v3-core/contracts/protocol/configuration/PriceOracleSentinel.sol'; +import {DefaultReserveInterestRateStrategyV2} from 'aave-v3-core/contracts/protocol/pool/DefaultReserveInterestRateStrategyV2.sol'; +import {IErrors} from '../../interfaces/IErrors.sol'; + +contract AaveV3MiscProcedure is IErrors { + function _deploySentinelAndDefaultIR( + bool l2Flag, + address poolAddressesProvider, + address sequencerUptimeOracle, + uint256 gracePeriod + ) internal returns (MiscReport memory miscReport) { + if (poolAddressesProvider == address(0)) revert ProviderNotFound(); + + if (l2Flag && sequencerUptimeOracle != address(0) && gracePeriod != 0) { + miscReport.priceOracleSentinel = address( + new PriceOracleSentinel( + IPoolAddressesProvider(poolAddressesProvider), + ISequencerOracle(sequencerUptimeOracle), + gracePeriod + ) + ); + } + + miscReport.defaultInterestRateStrategy = address( + new DefaultReserveInterestRateStrategyV2(poolAddressesProvider) + ); + + return miscReport; + } +} diff --git a/src/deployments/contracts/procedures/AaveV3SetupProcedure.sol b/src/deployments/contracts/procedures/AaveV3SetupProcedure.sol index ce411cc5..30aeb08d 100644 --- a/src/deployments/contracts/procedures/AaveV3SetupProcedure.sol +++ b/src/deployments/contracts/procedures/AaveV3SetupProcedure.sol @@ -38,7 +38,8 @@ contract AaveV3SetupProcedure { address poolConfiguratorImplementation, address protocolDataProvider, address aaveOracle, - address rewardsControllerImplementation + address rewardsControllerImplementation, + address priceOracleSentinel ) internal returns (SetupReport memory) { _validateMarketSetup(roles); @@ -49,7 +50,8 @@ contract AaveV3SetupProcedure { protocolDataProvider, roles.poolAdmin, aaveOracle, - rewardsControllerImplementation + rewardsControllerImplementation, + priceOracleSentinel ); report.aclManager = _setupACL( @@ -94,7 +96,8 @@ contract AaveV3SetupProcedure { address protocolDataProvider, address poolAdmin, address aaveOracle, - address rewardsControllerImplementation + address rewardsControllerImplementation, + address priceOracleSentinel ) internal returns (SetupReport memory) { SetupReport memory report; @@ -107,6 +110,10 @@ contract AaveV3SetupProcedure { report.poolProxy = address(provider.getPool()); report.poolConfiguratorProxy = address(provider.getPoolConfigurator()); + if (priceOracleSentinel != address(0)) { + provider.setPriceOracleSentinel(priceOracleSentinel); + } + bytes32 controllerId = keccak256('INCENTIVES_CONTROLLER'); provider.setAddressAsProxy(controllerId, rewardsControllerImplementation); report.rewardsControllerProxy = provider.getAddress(controllerId); diff --git a/src/deployments/contracts/utilities/MarketReportUtils.sol b/src/deployments/contracts/utilities/MarketReportUtils.sol index 34e10e87..341a64c2 100644 --- a/src/deployments/contracts/utilities/MarketReportUtils.sol +++ b/src/deployments/contracts/utilities/MarketReportUtils.sol @@ -21,8 +21,8 @@ library MarketReportUtils { aaveOracle: IAaveOracle(report.aaveOracle), aclManager: IACLManager(report.aclManager), treasury: ICollector(report.treasury), - defaultInterestRateStrategyV2: IDefaultInterestRateStrategyV2( - report.defaultInterestRateStrategyV2 + defaultInterestRateStrategy: IDefaultInterestRateStrategyV2( + report.defaultInterestRateStrategy ), proxyAdmin: ProxyAdmin(report.proxyAdmin), treasuryImplementation: ICollector(report.treasuryImplementation), diff --git a/src/deployments/contracts/utilities/MetadataReporter.sol b/src/deployments/contracts/utilities/MetadataReporter.sol index a3a9b8ab..59b5141c 100644 --- a/src/deployments/contracts/utilities/MetadataReporter.sol +++ b/src/deployments/contracts/utilities/MetadataReporter.sol @@ -69,6 +69,25 @@ contract MetadataReporter is IMetadataReporter { report.paraSwapWithdrawSwapAdapter ); vm.serializeAddress(jsonReport, 'aaveParaSwapFeeClaimer', report.aaveParaSwapFeeClaimer); + vm.serializeAddress( + jsonReport, + 'defaultInterestRateStrategy', + report.defaultInterestRateStrategy + ); + vm.serializeAddress(jsonReport, 'priceOracleSentinel', report.priceOracleSentinel); + vm.serializeAddress(jsonReport, 'configEngine', report.configEngine); + vm.serializeAddress( + jsonReport, + 'staticATokenFactoryImplementation', + report.staticATokenFactoryImplementation + ); + vm.serializeAddress(jsonReport, 'staticATokenFactoryProxy', report.staticATokenFactoryProxy); + vm.serializeAddress( + jsonReport, + 'staticATokenImplementation', + report.staticATokenImplementation + ); + vm.serializeAddress(jsonReport, 'transparentProxyFactory', report.transparentProxyFactory); string memory output = vm.serializeAddress( jsonReport, 'paraSwapRepayAdapter', diff --git a/src/deployments/interfaces/IErrors.sol b/src/deployments/interfaces/IErrors.sol index 5faefcc5..7d0df80a 100644 --- a/src/deployments/interfaces/IErrors.sol +++ b/src/deployments/interfaces/IErrors.sol @@ -5,4 +5,5 @@ interface IErrors { error L2MustBeEnabled(); error L2MustBeDisabled(); error ProviderNotFound(); + error ProxyAdminNotFound(); } diff --git a/src/deployments/interfaces/IMarketReportTypes.sol b/src/deployments/interfaces/IMarketReportTypes.sol index 2eb742f9..df1156cb 100644 --- a/src/deployments/interfaces/IMarketReportTypes.sol +++ b/src/deployments/interfaces/IMarketReportTypes.sol @@ -35,7 +35,7 @@ struct ContractsReport { IAaveOracle aaveOracle; IACLManager aclManager; ICollector treasury; - IDefaultInterestRateStrategyV2 defaultInterestRateStrategyV2; + IDefaultInterestRateStrategyV2 defaultInterestRateStrategy; ProxyAdmin proxyAdmin; ICollector treasuryImplementation; IWrappedTokenGatewayV3 wrappedTokenGateway; @@ -63,7 +63,8 @@ struct MarketReport { address poolConfiguratorImplementation; address protocolDataProvider; address aaveOracle; - address defaultInterestRateStrategyV2; + address defaultInterestRateStrategy; + address priceOracleSentinel; address aclManager; address treasury; address proxyAdmin; @@ -83,6 +84,11 @@ struct MarketReport { address emissionManager; address rewardsControllerImplementation; address rewardsControllerProxy; + address configEngine; + address transparentProxyFactory; + address staticATokenFactoryImplementation; + address staticATokenFactoryProxy; + address staticATokenImplementation; } struct LibrariesReport { @@ -109,6 +115,8 @@ struct MarketConfig { uint8 oracleDecimals; address paraswapAugustusRegistry; address paraswapFeeClaimer; + address l2SequencerUptimeFeed; + uint256 l2PriceOracleSentinelGracePeriod; uint256 providerId; bytes32 salt; address wrappedNativeToken; @@ -126,6 +134,29 @@ struct PoolReport { address poolConfiguratorImplementation; } +struct MiscReport { + address priceOracleSentinel; + address defaultInterestRateStrategy; +} + +struct ConfigEngineReport { + address configEngine; + address listingEngine; + address eModeEngine; + address borrowEngine; + address collateralEngine; + address priceFeedEngine; + address rateEngine; + address capsEngine; +} + +struct StaticATokenReport { + address transparentProxyFactory; + address staticATokenImplementation; + address staticATokenFactoryImplementation; + address staticATokenFactoryProxy; +} + struct InitialReport { address poolAddressesProvider; address poolAddressesProviderRegistry; @@ -145,7 +176,6 @@ struct PeripheryReport { address treasuryImplementation; address emissionManager; address rewardsControllerImplementation; - address defaultInterestRateStrategyV2; } struct ParaswapReport { diff --git a/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol b/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol index 1512596b..ec93fc80 100644 --- a/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol +++ b/src/deployments/projects/aave-v3-batched/AaveV3BatchOrchestration.sol @@ -10,6 +10,9 @@ import {AaveV3GettersProcedureTwo} from '../../contracts/procedures/AaveV3Getter import {AaveV3PeripheryBatch} from './batches/AaveV3PeripheryBatch.sol'; import {AaveV3ParaswapBatch} from './batches/AaveV3ParaswapBatch.sol'; import {AaveV3SetupBatch} from './batches/AaveV3SetupBatch.sol'; +import {AaveV3HelpersBatchOne} from './batches/AaveV3HelpersBatchOne.sol'; +import {AaveV3HelpersBatchTwo} from './batches/AaveV3HelpersBatchTwo.sol'; +import {AaveV3MiscBatch} from './batches/AaveV3MiscBatch.sol'; import '../../interfaces/IMarketReportTypes.sol'; import {IMarketReportStorage} from '../../interfaces/IMarketReportStorage.sol'; import {IPoolReport} from '../../interfaces/IPoolReport.sol'; @@ -52,6 +55,13 @@ library AaveV3BatchOrchestration { address(setupBatch) ); + MiscReport memory miscReport = _deployMisc( + flags.l2, + initialReport.poolAddressesProvider, + config.l2SequencerUptimeFeed, + config.l2PriceOracleSentinelGracePeriod + ); + SetupReport memory setupReport = setupBatch.setupAaveV3Market( roles, config, @@ -59,7 +69,8 @@ library AaveV3BatchOrchestration { poolReport.poolConfiguratorImplementation, gettersReport1.protocolDataProvider, peripheryReport.aaveOracle, - peripheryReport.rewardsControllerImplementation + peripheryReport.rewardsControllerImplementation, + miscReport.priceOracleSentinel ); ParaswapReport memory paraswapReport = _deployParaswapAdapters( @@ -78,6 +89,19 @@ library AaveV3BatchOrchestration { AaveV3TokensBatch.TokensReport memory tokensReport = _deployTokens(setupReport.poolProxy); + ConfigEngineReport memory configEngineReport = _deployHelpersBatch1( + setupReport, + miscReport, + peripheryReport, + tokensReport + ); + + StaticATokenReport memory staticATokenReport = _deployHelpersBatch2( + setupReport.poolProxy, + setupReport.rewardsControllerProxy, + peripheryReport.proxyAdmin + ); + // Save final report at AaveV3SetupBatch contract MarketReport memory report = _generateMarketReport( initialReport, @@ -85,9 +109,12 @@ library AaveV3BatchOrchestration { gettersReport2, poolReport, peripheryReport, + miscReport, paraswapReport, setupReport, - tokensReport + tokensReport, + configEngineReport, + staticATokenReport ); setupBatch.setMarketReport(report); @@ -137,6 +164,57 @@ library AaveV3BatchOrchestration { }); } + function _deployHelpersBatch1( + SetupReport memory setupReport, + MiscReport memory miscReport, + PeripheryReport memory peripheryReport, + AaveV3TokensBatch.TokensReport memory tokensReport + ) internal returns (ConfigEngineReport memory) { + AaveV3HelpersBatchOne helpersBatchOne = new AaveV3HelpersBatchOne( + setupReport.poolProxy, + setupReport.poolConfiguratorProxy, + miscReport.defaultInterestRateStrategy, + peripheryReport.aaveOracle, + setupReport.rewardsControllerProxy, + peripheryReport.treasury, + tokensReport.aToken, + tokensReport.variableDebtToken, + tokensReport.stableDebtToken + ); + + return helpersBatchOne.getConfigEngineReport(); + } + + function _deployHelpersBatch2( + address pool, + address rewardsController, + address proxyAdmin + ) internal returns (StaticATokenReport memory) { + AaveV3HelpersBatchTwo helpersBatchTwo = new AaveV3HelpersBatchTwo( + pool, + rewardsController, + proxyAdmin + ); + + return helpersBatchTwo.staticATokenReport(); + } + + function _deployMisc( + bool l2Flag, + address poolAddressesProvider, + address sequencerUptimeOracle, + uint256 gracePeriod + ) internal returns (MiscReport memory) { + AaveV3MiscBatch miscBatch = new AaveV3MiscBatch( + l2Flag, + poolAddressesProvider, + sequencerUptimeOracle, + gracePeriod + ); + + return miscBatch.getMiscReport(); + } + function _deployPoolImplementations( address poolAddressesProvider, DeployFlags memory flags @@ -207,9 +285,12 @@ library AaveV3BatchOrchestration { AaveV3GettersBatchTwo.GettersReportBatchTwo memory gettersReportTwo, PoolReport memory poolReport, PeripheryReport memory peripheryReport, + MiscReport memory miscReport, ParaswapReport memory paraswapReport, SetupReport memory setupReport, - AaveV3TokensBatch.TokensReport memory tokensReport + AaveV3TokensBatch.TokensReport memory tokensReport, + ConfigEngineReport memory configEngineReport, + StaticATokenReport memory staticATokenReport ) internal pure returns (MarketReport memory) { MarketReport memory report; @@ -240,7 +321,13 @@ library AaveV3BatchOrchestration { report.aToken = tokensReport.aToken; report.variableDebtToken = tokensReport.variableDebtToken; report.stableDebtToken = tokensReport.stableDebtToken; - report.defaultInterestRateStrategyV2 = peripheryReport.defaultInterestRateStrategyV2; + report.priceOracleSentinel = miscReport.priceOracleSentinel; + report.defaultInterestRateStrategy = miscReport.defaultInterestRateStrategy; + report.configEngine = configEngineReport.configEngine; + report.staticATokenFactoryImplementation = staticATokenReport.staticATokenFactoryImplementation; + report.staticATokenFactoryProxy = staticATokenReport.staticATokenFactoryProxy; + report.staticATokenImplementation = staticATokenReport.staticATokenImplementation; + report.transparentProxyFactory = staticATokenReport.transparentProxyFactory; return report; } diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchOne.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchOne.sol new file mode 100644 index 00000000..f841b657 --- /dev/null +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchOne.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AaveV3HelpersProcedureOne} from '../../../contracts/procedures/AaveV3HelpersProcedureOne.sol'; +import '../../../interfaces/IMarketReportTypes.sol'; + +contract AaveV3HelpersBatchOne is AaveV3HelpersProcedureOne { + ConfigEngineReport internal _report; + + constructor( + address poolProxy, + address poolConfiguratorProxy, + address defaultInterestRateStrategy, + address aaveOracle, + address rewardsController, + address collector, + address aTokenImpl, + address vTokenImpl, + address sTokenImpl + ) { + _report = _deployConfigEngine( + poolProxy, + poolConfiguratorProxy, + defaultInterestRateStrategy, + aaveOracle, + rewardsController, + collector, + aTokenImpl, + vTokenImpl, + sTokenImpl + ); + } + + function getConfigEngineReport() external view returns (ConfigEngineReport memory) { + return _report; + } +} diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchTwo.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchTwo.sol new file mode 100644 index 00000000..1db9f7b9 --- /dev/null +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchTwo.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AaveV3HelpersProcedureTwo} from '../../../contracts/procedures/AaveV3HelpersProcedureTwo.sol'; +import '../../../interfaces/IMarketReportTypes.sol'; + +contract AaveV3HelpersBatchTwo is AaveV3HelpersProcedureTwo { + StaticATokenReport internal _report; + + constructor(address pool, address rewardsController, address proxyAdmin) { + _report = _deployStaticAToken(pool, rewardsController, proxyAdmin); + } + + function staticATokenReport() external view returns (StaticATokenReport memory) { + return _report; + } +} diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3MiscBatch.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3MiscBatch.sol new file mode 100644 index 00000000..a841d76a --- /dev/null +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3MiscBatch.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AaveV3MiscProcedure} from '../../../contracts/procedures/AaveV3MiscProcedure.sol'; +import '../../../interfaces/IMarketReportTypes.sol'; + +contract AaveV3MiscBatch is AaveV3MiscProcedure { + MiscReport internal _report; + + constructor( + bool l2Flag, + address poolAddressesProvider, + address sequencerUptimeOracle, + uint256 gracePeriod + ) { + MiscReport memory miscReport = _deploySentinelAndDefaultIR( + l2Flag, + poolAddressesProvider, + sequencerUptimeOracle, + gracePeriod + ); + _report.priceOracleSentinel = miscReport.priceOracleSentinel; + _report.defaultInterestRateStrategy = miscReport.defaultInterestRateStrategy; + } + + function getMiscReport() external view returns (MiscReport memory) { + return _report; + } +} diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol index 43a25d9b..6c8c5cc7 100644 --- a/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol @@ -10,8 +10,7 @@ import '../../../interfaces/IMarketReportTypes.sol'; contract AaveV3PeripheryBatch is AaveV3TreasuryProcedure, AaveV3OracleProcedure, - AaveV3IncentiveProcedure, - AaveV3DefaultRateStrategyProcedure + AaveV3IncentiveProcedure { PeripheryReport internal _report; @@ -34,8 +33,6 @@ contract AaveV3PeripheryBatch is (_report.emissionManager, _report.rewardsControllerImplementation) = _deployIncentives( setupBatch ); - - _report.defaultInterestRateStrategyV2 = _deployDefaultRateStrategyV2(poolAddressesProvider); } function getPeripheryReport() external view returns (PeripheryReport memory) { diff --git a/src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol b/src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol index 78db0c47..07b08b1d 100644 --- a/src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol +++ b/src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol @@ -32,7 +32,8 @@ contract AaveV3SetupBatch is MarketReportStorage, AaveV3SetupProcedure, Ownable address poolConfiguratorImplementation, address protocolDataProvider, address aaveOracle, - address rewardsControllerImplementation + address rewardsControllerImplementation, + address priceOracleSentinel ) external onlyOwner returns (SetupReport memory) { _setupReport = _setupAaveV3Market( roles, @@ -42,7 +43,8 @@ contract AaveV3SetupBatch is MarketReportStorage, AaveV3SetupProcedure, Ownable poolConfiguratorImplementation, protocolDataProvider, aaveOracle, - rewardsControllerImplementation + rewardsControllerImplementation, + priceOracleSentinel ); return _setupReport; diff --git a/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol b/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol new file mode 100644 index 00000000..e58805c6 --- /dev/null +++ b/src/periphery/contracts/dependencies/openzeppelin/ECDSA.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) +pragma solidity ^0.8.0; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover( + bytes32 hash, + bytes memory signature + ) internal pure returns (address, RecoverError, bytes32) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + /// @solidity memory-safe-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures] + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} diff --git a/src/periphery/contracts/dependencies/solmate/ERC20.sol b/src/periphery/contracts/dependencies/solmate/ERC20.sol new file mode 100644 index 00000000..546df288 --- /dev/null +++ b/src/periphery/contracts/dependencies/solmate/ERC20.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ECDSA} from '../openzeppelin/ECDSA.sol'; + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20 { + bytes32 public constant PERMIT_TYPEHASH = + keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'); + + /* ////////////////////////////////////////////////////////////// + EVENTS + ////////////////////////////////////////////////////////////// */ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval(address indexed owner, address indexed spender, uint256 amount); + + /* ////////////////////////////////////////////////////////////// + METADATA STORAGE + ////////////////////////////////////////////////////////////// */ + + string public name; + + string public symbol; + + uint8 public decimals; + + /* ////////////////////////////////////////////////////////////// + ERC20 STORAGE + ////////////////////////////////////////////////////////////// */ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /* ////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + ////////////////////////////////////////////////////////////// */ + + mapping(address => uint256) public nonces; + + /* ////////////////////////////////////////////////////////////// + CONSTRUCTOR + ////////////////////////////////////////////////////////////// */ + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + /* ////////////////////////////////////////////////////////////// + ERC20 LOGIC + ////////////////////////////////////////////////////////////// */ + + function approve(address spender, uint256 amount) public virtual returns (bool) { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual returns (bool) { + _beforeTokenTransfer(msg.sender, to, amount); + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { + _beforeTokenTransfer(from, to, amount); + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /* ////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + ////////////////////////////////////////////////////////////// */ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, 'PERMIT_DEADLINE_EXPIRED'); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + address signer = ECDSA.recover( + keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR(), + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)) + ) + ), + v, + r, + s + ); + + require(signer == owner, 'INVALID_SIGNER'); + + allowance[signer][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256( + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + ), + keccak256(bytes(name)), + keccak256('1'), + block.chainid, + address(this) + ) + ); + } + + /* ////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + ////////////////////////////////////////////////////////////// */ + + function _mint(address to, uint256 amount) internal virtual { + _beforeTokenTransfer(address(0), to, amount); + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + _beforeTokenTransfer(from, address(0), amount); + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} diff --git a/src/periphery/contracts/libraries/LICENSE.md b/src/periphery/contracts/libraries/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/libraries/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/libraries/RayMathExplicitRounding.sol b/src/periphery/contracts/libraries/RayMathExplicitRounding.sol new file mode 100644 index 00000000..8d3f3dcb --- /dev/null +++ b/src/periphery/contracts/libraries/RayMathExplicitRounding.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.8.10; + +enum Rounding { + UP, + DOWN +} + +/** + * Simplified version of RayMath that instead of half-up rounding does explicit rounding in a specified direction. + * This is needed to have a 4626 complient implementation, that always predictable rounds in favor of the vault / static a token. + */ +library RayMathExplicitRounding { + uint256 internal constant RAY = 1e27; + uint256 internal constant WAD_RAY_RATIO = 1e9; + + function rayMulRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0 || b == 0) { + return 0; + } + return (a * b) / RAY; + } + + function rayMulRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + if (a == 0 || b == 0) { + return 0; + } + return ((a * b) + RAY - 1) / RAY; + } + + function rayDivRoundDown(uint256 a, uint256 b) internal pure returns (uint256) { + return (a * RAY) / b; + } + + function rayDivRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + return ((a * RAY) + b - 1) / b; + } + + function rayToWadRoundDown(uint256 a) internal pure returns (uint256) { + return a / WAD_RAY_RATIO; + } +} diff --git a/src/periphery/contracts/misc/LICENSE.md b/src/periphery/contracts/misc/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/misc/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/misc/interfaces/LICENSE.md b/src/periphery/contracts/misc/interfaces/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/misc/interfaces/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/mocks/LICENSE.md b/src/periphery/contracts/mocks/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/mocks/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/rewards/interfaces/LICENSE.md b/src/periphery/contracts/rewards/interfaces/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/rewards/interfaces/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/rewards/libraries/LICENSE.md b/src/periphery/contracts/rewards/libraries/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/rewards/libraries/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/rewards/transfer-strategies/LICENSE.md b/src/periphery/contracts/rewards/transfer-strategies/LICENSE.md deleted file mode 100644 index 506d18ec..00000000 --- a/src/periphery/contracts/rewards/transfer-strategies/LICENSE.md +++ /dev/null @@ -1,12 +0,0 @@ -Copyright (C) 2022 Aave - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -[GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) -for more details diff --git a/src/periphery/contracts/static-a-token/README.md b/src/periphery/contracts/static-a-token/README.md new file mode 100644 index 00000000..9ced57a6 --- /dev/null +++ b/src/periphery/contracts/static-a-token/README.md @@ -0,0 +1,38 @@ +# stataToken - Static aToken vault/wrapper + +

+ +

+ +## About + +The static-a-token contains an [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) generic token vault/wrapper for all Aave v3 pools. + +## Features + +- **Full [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626) compatibility.** +- **Accounting for any potential liquidity mining rewards.** Let’s say some team of the Aave ecosystem (or the Aave community itself) decides to incentivize deposits of USDC on Aave v3 Ethereum. By holding `stataUSDC`, the user will still be eligible for those incentives. + It is important to highlight that while currently the wrapper supports infinite reward tokens by design (e.g. AAVE incentivizing stETH & Lido incentivizing stETH as well), each reward needs to be permissionlessly registered which bears some [⁽¹⁾](#limitations). +- **Meta-transactions support.** To enable interfaces to offer gas-less transactions to deposit/withdraw on the wrapper/Aave protocol (also supported on Aave v3). Including permit() for transfers of the `stataAToken` itself. +- **Upgradable by the Aave governance.** Similar to other contracts of the Aave ecosystem, the Level 1 executor (short executor) will be able to add new features to the deployed instances of the `stataTokens`. +- **Powered by a stataToken Factory.** Whenever a token will be listed on Aave v3, anybody will be able to call the stataToken Factory to deploy an instance for the new asset, permissionless, but still assuring the code used and permissions are properly configured without any extra headache. + +See [IStaticATokenLM.sol](./interfaces/IStaticATokenLM.sol) for detailed method documentation. + +## Deployed Addresses + +The staticATokenFactory is deployed for all major Aave v3 pools. +An up to date address can be fetched from the respective [address-book pool library](https://github.com/bgd-labs/aave-address-book/blob/main/src/AaveV3Ethereum.sol). + +## Limitations + +The `stataToken` is not natively integrated into the aave protocol and therefore cannot hook into the emissionManager. +This means a `reward` added **after** `statToken` creation needs to be registered manually on the token via the permissionless `refreshRewardTokens()` method. +As this process is not currently automated users might be missing out on rewards until the method is called. + +## Security procedures + +For this project, the security procedures applied/being finished are: + +- The test suite of the codebase itself. +- Certora audit/property checking for all the dynamics of the `stataToken`, including respecting all the specs of [EIP-4626](https://eips.ethereum.org/EIPS/eip-4626). diff --git a/src/periphery/contracts/static-a-token/StataOracle.sol b/src/periphery/contracts/static-a-token/StataOracle.sol new file mode 100644 index 00000000..d1d7e7ca --- /dev/null +++ b/src/periphery/contracts/static-a-token/StataOracle.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; +import {IPoolAddressesProvider} from '../../../core/contracts/interfaces/IPoolAddressesProvider.sol'; +import {IAaveOracle} from '../../../core/contracts/interfaces/IAaveOracle.sol'; +import {IStataOracle} from './interfaces/IStataOracle.sol'; +import {IERC4626} from './interfaces/IERC4626.sol'; + +/** + * @title StataOracle + * @author BGD Labs + * @notice Contract to get asset prices of stata tokens + */ +contract StataOracle is IStataOracle { + /// @inheritdoc IStataOracle + IPool public immutable POOL; + /// @inheritdoc IStataOracle + IAaveOracle public immutable AAVE_ORACLE; + + constructor(IPoolAddressesProvider provider) { + POOL = IPool(provider.getPool()); + AAVE_ORACLE = IAaveOracle(provider.getPriceOracle()); + } + + /// @inheritdoc IStataOracle + function getAssetPrice(address asset) public view returns (uint256) { + address underlying = IERC4626(asset).asset(); + return + (AAVE_ORACLE.getAssetPrice(underlying) * POOL.getReserveNormalizedIncome(underlying)) / 1e27; + } + + /// @inheritdoc IStataOracle + function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory) { + uint256[] memory prices = new uint256[](assets.length); + for (uint256 i = 0; i < assets.length; i++) { + prices[i] = getAssetPrice(assets[i]); + } + return prices; + } +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenErrors.sol b/src/periphery/contracts/static-a-token/StaticATokenErrors.sol new file mode 100644 index 00000000..bec417df --- /dev/null +++ b/src/periphery/contracts/static-a-token/StaticATokenErrors.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +library StaticATokenErrors { + string public constant INVALID_OWNER = '1'; + string public constant INVALID_EXPIRATION = '2'; + string public constant INVALID_SIGNATURE = '3'; + string public constant INVALID_DEPOSITOR = '4'; + string public constant INVALID_RECIPIENT = '5'; + string public constant INVALID_CLAIMER = '6'; + string public constant ONLY_ONE_AMOUNT_FORMAT_ALLOWED = '7'; + string public constant INVALID_ZERO_AMOUNT = '8'; + string public constant REWARD_NOT_INITIALIZED = '9'; +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenFactory.sol b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol new file mode 100644 index 00000000..c48e339a --- /dev/null +++ b/src/periphery/contracts/static-a-token/StaticATokenFactory.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IPool, DataTypes} from '../../../core/contracts/interfaces/IPool.sol'; +import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; +import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/interfaces/ITransparentProxyFactory.sol'; +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; +import {StaticATokenLM} from './StaticATokenLM.sol'; +import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol'; + +/** + * @title StaticATokenFactory + * @notice Factory contract that keeps track of all deployed static aToken wrappers for a specified pool. + * This registry also acts as a factory, allowing to deploy new static aTokens on demand. + * There can only be one static aToken per underlying on the registry at a time. + * @author BGD labs + */ +contract StaticATokenFactory is Initializable, IStaticATokenFactory { + IPool public immutable POOL; + address public immutable ADMIN; + ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY; + address public immutable STATIC_A_TOKEN_IMPL; + + mapping(address => address) internal _underlyingToStaticAToken; + address[] internal _staticATokens; + + event StaticTokenCreated(address indexed staticAToken, address indexed underlying); + + constructor( + IPool pool, + address proxyAdmin, + ITransparentProxyFactory transparentProxyFactory, + address staticATokenImpl + ) { + POOL = pool; + ADMIN = proxyAdmin; + TRANSPARENT_PROXY_FACTORY = transparentProxyFactory; + STATIC_A_TOKEN_IMPL = staticATokenImpl; + } + + function initialize() external initializer {} + + ///@inheritdoc IStaticATokenFactory + function createStaticATokens(address[] memory underlyings) external returns (address[] memory) { + address[] memory staticATokens = new address[](underlyings.length); + for (uint256 i = 0; i < underlyings.length; i++) { + address cachedStaticAToken = _underlyingToStaticAToken[underlyings[i]]; + if (cachedStaticAToken == address(0)) { + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(underlyings[i]); + require(reserveData.aTokenAddress != address(0), 'UNDERLYING_NOT_LISTED'); + bytes memory symbol = abi.encodePacked( + 'stat', + IERC20Metadata(reserveData.aTokenAddress).symbol() + ); + address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic( + STATIC_A_TOKEN_IMPL, + ADMIN, + abi.encodeWithSelector( + StaticATokenLM.initialize.selector, + reserveData.aTokenAddress, + string(abi.encodePacked('Static ', IERC20Metadata(reserveData.aTokenAddress).name())), + string(symbol) + ), + bytes32(uint256(uint160(underlyings[i]))) + ); + _underlyingToStaticAToken[underlyings[i]] = staticAToken; + staticATokens[i] = staticAToken; + _staticATokens.push(staticAToken); + emit StaticTokenCreated(staticAToken, underlyings[i]); + } else { + staticATokens[i] = cachedStaticAToken; + } + } + return staticATokens; + } + + ///@inheritdoc IStaticATokenFactory + function getStaticATokens() external view returns (address[] memory) { + return _staticATokens; + } + + ///@inheritdoc IStaticATokenFactory + function getStaticAToken(address underlying) external view returns (address) { + return _underlyingToStaticAToken[underlying]; + } +} diff --git a/src/periphery/contracts/static-a-token/StaticATokenLM.sol b/src/periphery/contracts/static-a-token/StaticATokenLM.sol new file mode 100644 index 00000000..9a55fe50 --- /dev/null +++ b/src/periphery/contracts/static-a-token/StaticATokenLM.sol @@ -0,0 +1,712 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IPool} from '../../../core/contracts/interfaces/IPool.sol'; +import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol'; +import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol'; +import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol'; +import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol'; +import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol'; +import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol'; +import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol'; +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; + +import {IStaticATokenLM} from './interfaces/IStaticATokenLM.sol'; +import {IAToken} from './interfaces/IAToken.sol'; +import {ERC20} from '../dependencies/solmate/ERC20.sol'; +import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATokenLM.sol'; +import {StaticATokenErrors} from './StaticATokenErrors.sol'; +import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.sol'; +import {IERC4626} from './interfaces/IERC4626.sol'; + +/** + * @title StaticATokenLM + * @notice Wrapper smart contract that allows to deposit tokens on the Aave protocol and receive + * a token which balance doesn't increase automatically, but uses an ever-increasing exchange rate. + * It supports claiming liquidity mining rewards from the Aave system. + * @author BGD labs + */ +contract StaticATokenLM is + Initializable, + ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18), + IStaticATokenLM, + IERC4626 +{ + using SafeERC20 for IERC20; + using SafeCast for uint256; + using WadRayMath for uint256; + using RayMathExplicitRounding for uint256; + + bytes32 public constant METADEPOSIT_TYPEHASH = + keccak256( + 'Deposit(address depositor,address receiver,uint256 assets,uint16 referralCode,bool depositToAave,uint256 nonce,uint256 deadline,PermitParams permit)' + ); + bytes32 public constant METAWITHDRAWAL_TYPEHASH = + keccak256( + 'Withdraw(address owner,address receiver,uint256 shares,uint256 assets,bool withdrawFromAave,uint256 nonce,uint256 deadline)' + ); + + uint256 public constant STATIC__ATOKEN_LM_REVISION = 2; + + IPool public immutable POOL; + IRewardsController public immutable INCENTIVES_CONTROLLER; + + IERC20 internal _aToken; + address internal _aTokenUnderlying; + address[] internal _rewardTokens; + mapping(address => RewardIndexCache) internal _startIndex; + mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData; + + constructor(IPool pool, IRewardsController rewardsController) { + POOL = pool; + INCENTIVES_CONTROLLER = rewardsController; + } + + ///@inheritdoc IInitializableStaticATokenLM + function initialize( + address newAToken, + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external initializer { + require(IAToken(newAToken).POOL() == address(POOL)); + _aToken = IERC20(newAToken); + + name = staticATokenName; + symbol = staticATokenSymbol; + decimals = IERC20Metadata(newAToken).decimals(); + + _aTokenUnderlying = IAToken(newAToken).UNDERLYING_ASSET_ADDRESS(); + IERC20(_aTokenUnderlying).forceApprove(address(POOL), type(uint256).max); + + if (INCENTIVES_CONTROLLER != IRewardsController(address(0))) { + refreshRewardTokens(); + } + + emit Initialized(newAToken, staticATokenName, staticATokenSymbol); + } + + ///@inheritdoc IStaticATokenLM + function refreshRewardTokens() public override { + address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken)); + for (uint256 i = 0; i < rewards.length; i++) { + _registerRewardToken(rewards[i]); + } + } + + ///@inheritdoc IStaticATokenLM + function isRegisteredRewardToken(address reward) public view override returns (bool) { + return _startIndex[reward].isRegistered; + } + + ///@inheritdoc IStaticATokenLM + function deposit( + uint256 assets, + address receiver, + uint16 referralCode, + bool depositToAave + ) external returns (uint256) { + (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, referralCode, depositToAave); + return shares; + } + + ///@inheritdoc IStaticATokenLM + function metaDeposit( + address depositor, + address receiver, + uint256 assets, + uint16 referralCode, + bool depositToAave, + uint256 deadline, + PermitParams calldata permit, + SignatureParams calldata sigParams + ) external returns (uint256) { + require(depositor != address(0), StaticATokenErrors.INVALID_DEPOSITOR); + //solium-disable-next-line + require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); + uint256 nonce = nonces[depositor]; + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + METADEPOSIT_TYPEHASH, + depositor, + receiver, + assets, + referralCode, + depositToAave, + nonce, + deadline, + permit + ) + ) + ) + ); + nonces[depositor] = nonce + 1; + require( + depositor == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), + StaticATokenErrors.INVALID_SIGNATURE + ); + } + // assume if deadline 0 no permit was supplied + if (permit.deadline != 0) { + try + IERC20WithPermit(depositToAave ? address(_aTokenUnderlying) : address(_aToken)).permit( + depositor, + address(this), + permit.value, + permit.deadline, + permit.v, + permit.r, + permit.s + ) + {} catch {} + } + (uint256 shares, ) = _deposit(depositor, receiver, 0, assets, referralCode, depositToAave); + return shares; + } + + ///@inheritdoc IStaticATokenLM + function metaWithdraw( + address owner, + address receiver, + uint256 shares, + uint256 assets, + bool withdrawFromAave, + uint256 deadline, + SignatureParams calldata sigParams + ) external returns (uint256, uint256) { + require(owner != address(0), StaticATokenErrors.INVALID_OWNER); + //solium-disable-next-line + require(deadline >= block.timestamp, StaticATokenErrors.INVALID_EXPIRATION); + uint256 nonce = nonces[owner]; + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + bytes32 digest = keccak256( + abi.encodePacked( + '\x19\x01', + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + METAWITHDRAWAL_TYPEHASH, + owner, + receiver, + shares, + assets, + withdrawFromAave, + nonce, + deadline + ) + ) + ) + ); + nonces[owner] = nonce + 1; + require( + owner == ecrecover(digest, sigParams.v, sigParams.r, sigParams.s), + StaticATokenErrors.INVALID_SIGNATURE + ); + } + return _withdraw(owner, receiver, shares, assets, withdrawFromAave); + } + + ///@inheritdoc IERC4626 + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return _convertToAssets(shares, Rounding.DOWN); + } + + ///@inheritdoc IERC4626 + function previewMint(uint256 shares) public view virtual returns (uint256) { + return _convertToAssets(shares, Rounding.UP); + } + + ///@inheritdoc IERC4626 + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + return _convertToShares(assets, Rounding.UP); + } + + ///@inheritdoc IERC4626 + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return _convertToShares(assets, Rounding.DOWN); + } + + ///@inheritdoc IStaticATokenLM + function rate() public view returns (uint256) { + return POOL.getReserveNormalizedIncome(_aTokenUnderlying); + } + + ///@inheritdoc IStaticATokenLM + function collectAndUpdateRewards(address reward) public returns (uint256) { + if (reward == address(0)) { + return 0; + } + + address[] memory assets = new address[](1); + assets[0] = address(_aToken); + + return INCENTIVES_CONTROLLER.claimRewards(assets, type(uint256).max, address(this), reward); + } + + ///@inheritdoc IStaticATokenLM + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external { + require( + msg.sender == onBehalfOf || msg.sender == INCENTIVES_CONTROLLER.getClaimer(onBehalfOf), + StaticATokenErrors.INVALID_CLAIMER + ); + _claimRewardsOnBehalf(onBehalfOf, receiver, rewards); + } + + ///@inheritdoc IStaticATokenLM + function claimRewards(address receiver, address[] memory rewards) external { + _claimRewardsOnBehalf(msg.sender, receiver, rewards); + } + + ///@inheritdoc IStaticATokenLM + function claimRewardsToSelf(address[] memory rewards) external { + _claimRewardsOnBehalf(msg.sender, msg.sender, rewards); + } + + ///@inheritdoc IStaticATokenLM + function getCurrentRewardsIndex(address reward) public view returns (uint256) { + if (address(reward) == address(0)) { + return 0; + } + (, uint256 nextIndex) = INCENTIVES_CONTROLLER.getAssetIndex(address(_aToken), reward); + return nextIndex; + } + + ///@inheritdoc IStaticATokenLM + function getTotalClaimableRewards(address reward) external view returns (uint256) { + if (reward == address(0)) { + return 0; + } + + address[] memory assets = new address[](1); + assets[0] = address(_aToken); + uint256 freshRewards = INCENTIVES_CONTROLLER.getUserRewards(assets, address(this), reward); + return IERC20(reward).balanceOf(address(this)) + freshRewards; + } + + ///@inheritdoc IStaticATokenLM + function getClaimableRewards(address user, address reward) external view returns (uint256) { + return _getClaimableRewards(user, reward, balanceOf[user], getCurrentRewardsIndex(reward)); + } + + ///@inheritdoc IStaticATokenLM + function getUnclaimedRewards(address user, address reward) external view returns (uint256) { + return _userRewardsData[user][reward].unclaimedRewards; + } + + ///@inheritdoc IERC4626 + function asset() external view returns (address) { + return address(_aTokenUnderlying); + } + + ///@inheritdoc IStaticATokenLM + function aToken() external view returns (IERC20) { + return _aToken; + } + + ///@inheritdoc IStaticATokenLM + function rewardTokens() external view returns (address[] memory) { + return _rewardTokens; + } + + ///@inheritdoc IERC4626 + function totalAssets() external view returns (uint256) { + return _aToken.balanceOf(address(this)); + } + + ///@inheritdoc IERC4626 + function convertToShares(uint256 assets) external view returns (uint256) { + return _convertToShares(assets, Rounding.DOWN); + } + + ///@inheritdoc IERC4626 + function convertToAssets(uint256 shares) external view returns (uint256) { + return _convertToAssets(shares, Rounding.DOWN); + } + + ///@inheritdoc IERC4626 + function maxMint(address) public view virtual returns (uint256) { + uint256 assets = maxDeposit(address(0)); + if (assets == type(uint256).max) return type(uint256).max; + return _convertToShares(assets, Rounding.DOWN); + } + + ///@inheritdoc IERC4626 + function maxWithdraw(address owner) public view virtual returns (uint256) { + uint256 shares = maxRedeem(owner); + return _convertToAssets(shares, Rounding.DOWN); + } + + ///@inheritdoc IERC4626 + function maxRedeem(address owner) public view virtual returns (uint256) { + address cachedATokenUnderlying = _aTokenUnderlying; + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(cachedATokenUnderlying); + + // if paused or inactive users cannot withdraw underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) + ) { + return 0; + } + + // otherwise users can withdraw up to the available amount + uint256 underlyingTokenBalanceInShares = _convertToShares( + IERC20(cachedATokenUnderlying).balanceOf(reserveData.aTokenAddress), + Rounding.DOWN + ); + uint256 cachedUserBalance = balanceOf[owner]; + return + underlyingTokenBalanceInShares >= cachedUserBalance + ? cachedUserBalance + : underlyingTokenBalanceInShares; + } + + ///@inheritdoc IERC4626 + function maxDeposit(address) public view virtual returns (uint256) { + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(_aTokenUnderlying); + + // if inactive, paused or frozen users cannot deposit underlying + if ( + !ReserveConfiguration.getActive(reserveData.configuration) || + ReserveConfiguration.getPaused(reserveData.configuration) || + ReserveConfiguration.getFrozen(reserveData.configuration) + ) { + return 0; + } + + uint256 supplyCap = ReserveConfiguration.getSupplyCap(reserveData.configuration) * + (10 ** ReserveConfiguration.getDecimals(reserveData.configuration)); + // if no supply cap deposit is unlimited + if (supplyCap == 0) return type(uint256).max; + // return remaining supply cap margin + uint256 currentSupply = (IAToken(reserveData.aTokenAddress).scaledTotalSupply() + + reserveData.accruedToTreasury).rayMulRoundUp(_getNormalizedIncome(reserveData)); + return currentSupply > supplyCap ? 0 : supplyCap - currentSupply; + } + + ///@inheritdoc IERC4626 + function deposit(uint256 assets, address receiver) external virtual returns (uint256) { + (uint256 shares, ) = _deposit(msg.sender, receiver, 0, assets, 0, true); + return shares; + } + + ///@inheritdoc IERC4626 + function mint(uint256 shares, address receiver) external virtual returns (uint256) { + (, uint256 assets) = _deposit(msg.sender, receiver, shares, 0, 0, true); + + return assets; + } + + ///@inheritdoc IERC4626 + function withdraw( + uint256 assets, + address receiver, + address owner + ) external virtual returns (uint256) { + (uint256 shares, ) = _withdraw(owner, receiver, 0, assets, true); + + return shares; + } + + ///@inheritdoc IERC4626 + function redeem( + uint256 shares, + address receiver, + address owner + ) external virtual returns (uint256) { + (, uint256 assets) = _withdraw(owner, receiver, shares, 0, true); + + return assets; + } + + ///@inheritdoc IStaticATokenLM + function redeem( + uint256 shares, + address receiver, + address owner, + bool withdrawFromAave + ) external virtual returns (uint256, uint256) { + return _withdraw(owner, receiver, shares, 0, withdrawFromAave); + } + + function _deposit( + address depositor, + address receiver, + uint256 _shares, + uint256 _assets, + uint16 referralCode, + bool depositToAave + ) internal returns (uint256, uint256) { + require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); + require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); + + uint256 assets = _assets; + uint256 shares = _shares; + if (shares > 0) { + if (depositToAave) { + require(shares <= maxMint(receiver), 'ERC4626: mint more than max'); + } + assets = previewMint(shares); + } else { + if (depositToAave) { + require(assets <= maxDeposit(receiver), 'ERC4626: deposit more than max'); + } + shares = previewDeposit(assets); + } + require(shares != 0, StaticATokenErrors.INVALID_ZERO_AMOUNT); + + if (depositToAave) { + address cachedATokenUnderlying = _aTokenUnderlying; + IERC20(cachedATokenUnderlying).safeTransferFrom(depositor, address(this), assets); + POOL.deposit(cachedATokenUnderlying, assets, address(this), referralCode); + } else { + _aToken.safeTransferFrom(depositor, address(this), assets); + } + + _mint(receiver, shares); + + emit Deposit(depositor, receiver, assets, shares); + + return (shares, assets); + } + + function _withdraw( + address owner, + address receiver, + uint256 _shares, + uint256 _assets, + bool withdrawFromAave + ) internal returns (uint256, uint256) { + require(receiver != address(0), StaticATokenErrors.INVALID_RECIPIENT); + require(_shares == 0 || _assets == 0, StaticATokenErrors.ONLY_ONE_AMOUNT_FORMAT_ALLOWED); + require(_shares != _assets, StaticATokenErrors.INVALID_ZERO_AMOUNT); + + uint256 assets = _assets; + uint256 shares = _shares; + + if (shares > 0) { + if (withdrawFromAave) { + require(shares <= maxRedeem(owner), 'ERC4626: redeem more than max'); + } + assets = previewRedeem(shares); + } else { + if (withdrawFromAave) { + require(assets <= maxWithdraw(owner), 'ERC4626: withdraw more than max'); + } + shares = previewWithdraw(assets); + } + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + if (withdrawFromAave) { + POOL.withdraw(_aTokenUnderlying, assets, receiver); + } else { + _aToken.safeTransfer(receiver, assets); + } + + return (shares, assets); + } + + /** + * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0)) + * @param from The address of the sender of tokens + * @param to The address of the receiver of tokens + */ + function _beforeTokenTransfer(address from, address to, uint256) internal override { + for (uint256 i = 0; i < _rewardTokens.length; i++) { + address rewardToken = address(_rewardTokens[i]); + uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken); + if (from != address(0)) { + _updateUser(from, rewardsIndex, rewardToken); + } + if (to != address(0) && from != to) { + _updateUser(to, rewardsIndex, rewardToken); + } + } + } + + /** + * @notice Adding the pending rewards to the unclaimed for specific user and updating user index + * @param user The address of the user to update + * @param currentRewardsIndex The current rewardIndex + * @param rewardToken The address of the reward token + */ + function _updateUser(address user, uint256 currentRewardsIndex, address rewardToken) internal { + uint256 balance = balanceOf[user]; + if (balance > 0) { + _userRewardsData[user][rewardToken].unclaimedRewards = _getClaimableRewards( + user, + rewardToken, + balance, + currentRewardsIndex + ).toUint128(); + } + _userRewardsData[user][rewardToken].rewardsIndexOnLastInteraction = currentRewardsIndex + .toUint128(); + } + + /** + * @notice Compute the pending in WAD. Pending is the amount to add (not yet unclaimed) rewards in WAD. + * @param balance The balance of the user + * @param rewardsIndexOnLastInteraction The index which was on the last interaction of the user + * @param currentRewardsIndex The current rewards index in the system + * @param assetUnit One unit of asset (10**decimals) + * @return The amount of pending rewards in WAD + */ + function _getPendingRewards( + uint256 balance, + uint256 rewardsIndexOnLastInteraction, + uint256 currentRewardsIndex, + uint256 assetUnit + ) internal pure returns (uint256) { + if (balance == 0) { + return 0; + } + return (balance * (currentRewardsIndex - rewardsIndexOnLastInteraction)) / assetUnit; + } + + /** + * @notice Compute the claimable rewards for a user + * @param user The address of the user + * @param reward The address of the reward + * @param balance The balance of the user in WAD + * @param currentRewardsIndex The current rewards index + * @return The total rewards that can be claimed by the user (if `fresh` flag true, after updating rewards) + */ + function _getClaimableRewards( + address user, + address reward, + uint256 balance, + uint256 currentRewardsIndex + ) internal view returns (uint256) { + RewardIndexCache memory rewardsIndexCache = _startIndex[reward]; + require(rewardsIndexCache.isRegistered == true, StaticATokenErrors.REWARD_NOT_INITIALIZED); + UserRewardsData memory currentUserRewardsData = _userRewardsData[user][reward]; + uint256 assetUnit = 10 ** decimals; + return + currentUserRewardsData.unclaimedRewards + + _getPendingRewards( + balance, + currentUserRewardsData.rewardsIndexOnLastInteraction == 0 + ? rewardsIndexCache.lastUpdatedIndex + : currentUserRewardsData.rewardsIndexOnLastInteraction, + currentRewardsIndex, + assetUnit + ); + } + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @param onBehalfOf The address to claim on behalf of + * @param rewards The addresses of the rewards + * @param receiver The address to receive the rewards + */ + function _claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) internal { + for (uint256 i = 0; i < rewards.length; i++) { + if (address(rewards[i]) == address(0)) { + continue; + } + uint256 currentRewardsIndex = getCurrentRewardsIndex(rewards[i]); + uint256 balance = balanceOf[onBehalfOf]; + uint256 userReward = _getClaimableRewards( + onBehalfOf, + rewards[i], + balance, + currentRewardsIndex + ); + uint256 totalRewardTokenBalance = IERC20(rewards[i]).balanceOf(address(this)); + uint256 unclaimedReward = 0; + + if (userReward > totalRewardTokenBalance) { + totalRewardTokenBalance += collectAndUpdateRewards(address(rewards[i])); + } + + if (userReward > totalRewardTokenBalance) { + unclaimedReward = userReward - totalRewardTokenBalance; + userReward = totalRewardTokenBalance; + } + if (userReward > 0) { + _userRewardsData[onBehalfOf][rewards[i]].unclaimedRewards = unclaimedReward.toUint128(); + _userRewardsData[onBehalfOf][rewards[i]].rewardsIndexOnLastInteraction = currentRewardsIndex + .toUint128(); + IERC20(rewards[i]).safeTransfer(receiver, userReward); + } + } + } + + function _convertToShares(uint256 assets, Rounding rounding) internal view returns (uint256) { + if (rounding == Rounding.UP) return assets.rayDivRoundUp(rate()); + return assets.rayDivRoundDown(rate()); + } + + function _convertToAssets(uint256 shares, Rounding rounding) internal view returns (uint256) { + if (rounding == Rounding.UP) return shares.rayMulRoundUp(rate()); + return shares.rayMulRoundDown(rate()); + } + + /** + * @notice Initializes a new rewardToken + * @param reward The reward token to be registered + */ + function _registerRewardToken(address reward) internal { + if (isRegisteredRewardToken(reward)) return; + uint256 startIndex = getCurrentRewardsIndex(reward); + + _rewardTokens.push(reward); + _startIndex[reward] = RewardIndexCache(true, startIndex.toUint240()); + + emit RewardTokenRegistered(reward, startIndex); + } + + /** + * Copy of https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ReserveLogic.sol#L47 with memory instead of calldata + * @notice Returns the ongoing normalized income for the reserve. + * @dev A value of 1e27 means there is no income. As time passes, the income is accrued + * @dev A value of 2*1e27 means for each unit of asset one unit of income has been accrued + * @param reserve The reserve object + * @return The normalized income, expressed in ray + */ + function _getNormalizedIncome( + DataTypes.ReserveDataLegacy memory reserve + ) internal view returns (uint256) { + uint40 timestamp = reserve.lastUpdateTimestamp; + + //solium-disable-next-line + if (timestamp == block.timestamp) { + //if the index was updated in the same block, no need to perform any calculation + return reserve.liquidityIndex; + } else { + return + MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul( + reserve.liquidityIndex + ); + } + } +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IAToken.sol b/src/periphery/contracts/static-a-token/interfaces/IAToken.sol new file mode 100644 index 00000000..31e9a805 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IAToken.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IAToken { + function POOL() external view returns (address); + + function getIncentivesController() external view returns (address); + + function UNDERLYING_ASSET_ADDRESS() external view returns (address); + + /** + * @notice Returns the scaled total supply of the scaled balance token. Represents sum(debt/index) + * @return The scaled total supply + */ + function scaledTotalSupply() external view returns (uint256); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol b/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol new file mode 100644 index 00000000..08f14f90 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IERC4626.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC4626.sol) + +pragma solidity ^0.8.10; + +/** + * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in + * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. + * + * _Available since v4.7._ + */ +interface IERC4626 { + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /** + * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + function asset() external view returns (address assetTokenAddress); + + /** + * @dev Returns the total amount of the underlying asset that is “managed” by Vault. + * + * - SHOULD include any compounding that occurs from yield. + * - MUST be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT revert. + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, + * through a deposit call. + * While deposit of aToken is not affected by aave pool configrations, deposit of the aTokenUnderlying will need to deposit to aave + * so it is affected by current aave pool configuration. + * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L57 + * - MUST return a limited value if receiver is subject to some deposit limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit + * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called + * in the same transaction. + * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the + * deposit would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * deposit execution, and are accounted for during deposit. + * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. + * - MUST return a limited value if receiver is subject to some mint limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. + * - MUST NOT revert. + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call + * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the + * same transaction. + * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint + * would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by minting. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint + * execution, and are accounted for during mint. + * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the + * Vault, through a withdraw call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw + * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if + * called + * in the same transaction. + * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though + * the withdrawal would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * withdraw execution, and are accounted for during withdraw. + * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, + * through a redeem call to the aToken underlying. + * While redeem of aToken is not affected by aave pool configrations, redeeming of the aTokenUnderlying will need to redeem from aave + * so it is affected by current aave pool configuration. + * Reference: https://github.com/aave/aave-v3-core/blob/29ff9b9f89af7cd8255231bc5faf26c3ce0fb7ce/contracts/protocol/libraries/logic/ValidationLogic.sol#L87 + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call + * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the + * same transaction. + * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the + * redemption would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by redeeming. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * redeem execution, and are accounted for during redeem. + * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function redeem( + uint256 shares, + address receiver, + address owner + ) external returns (uint256 assets); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol new file mode 100644 index 00000000..0eeb8955 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IInitializableStaticATokenLM.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; +import {IAaveIncentivesController} from '../../../../core/contracts/interfaces/IAaveIncentivesController.sol'; + +/** + * @title IInitializableStaticATokenLM + * @notice Interface for the initialize function on StaticATokenLM + * @author Aave + **/ +interface IInitializableStaticATokenLM { + /** + * @dev Emitted when a StaticATokenLM is initialized + * @param aToken The address of the underlying aToken (aWETH) + * @param staticATokenName The name of the Static aToken + * @param staticATokenSymbol The symbol of the Static aToken + **/ + event Initialized(address indexed aToken, string staticATokenName, string staticATokenSymbol); + + /** + * @dev Initializes the StaticATokenLM + * @param aToken The address of the underlying aToken (aWETH) + * @param staticATokenName The name of the Static aToken + * @param staticATokenSymbol The symbol of the Static aToken + */ + function initialize( + address aToken, + string calldata staticATokenName, + string calldata staticATokenSymbol + ) external; +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol b/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol new file mode 100644 index 00000000..acd4fc4f --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStataOracle.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IPool} from '../../../../core/contracts/interfaces/IPool.sol'; +import {IAaveOracle} from '../../../../core/contracts/interfaces/IAaveOracle.sol'; + +interface IStataOracle { + /** + * @return The pool used for fetching the rate on the aggregator oracle + */ + function POOL() external view returns (IPool); + + /** + * @return The aave oracle used for fetching the price of the underlying + */ + function AAVE_ORACLE() external view returns (IAaveOracle); + + /** + * @notice Returns the prices of an asset address + * @param asset The asset address + * @return The prices of the given asset + */ + function getAssetPrice(address asset) external view returns (uint256); + + /** + * @notice Returns a list of prices from a list of assets addresses + * @param assets The list of assets addresses + * @return The prices of the given assets + */ + function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol new file mode 100644 index 00000000..7532e92c --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenFactory.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +interface IStaticATokenFactory { + /** + * @notice Creates new staticATokens + * @param underlyings the addresses of the underlyings to create. + * @return address[] addresses of the new staticATokens. + */ + function createStaticATokens(address[] memory underlyings) external returns (address[] memory); + + /** + * @notice Returns all tokens deployed via this registry. + * @return address[] list of tokens + */ + function getStaticATokens() external view returns (address[] memory); + + /** + * @notice Returns the staticAToken for a given underlying. + * @param underlying the address of the underlying. + * @return address the staticAToken address. + */ + function getStaticAToken(address underlying) external view returns (address); +} diff --git a/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol new file mode 100644 index 00000000..eed469f3 --- /dev/null +++ b/src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol'; +import {IInitializableStaticATokenLM} from './IInitializableStaticATokenLM.sol'; + +interface IStaticATokenLM is IInitializableStaticATokenLM { + struct SignatureParams { + uint8 v; + bytes32 r; + bytes32 s; + } + + struct PermitParams { + address owner; + address spender; + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + struct UserRewardsData { + uint128 rewardsIndexOnLastInteraction; // (in RAYs) + uint128 unclaimedRewards; // (in RAYs) + } + + struct RewardIndexCache { + bool isRegistered; + uint248 lastUpdatedIndex; + } + + event RewardTokenRegistered(address indexed reward, uint256 startIndex); + + /** + * @notice Burns `amount` of static aToken, with receiver receiving the corresponding amount of `ASSET` + * @param shares The amount to withdraw, in static balance of StaticAToken + * @param receiver The address that will receive the amount of `ASSET` withdrawn from the Aave protocol + * @param withdrawFromAave bool + * - `true` for the receiver to get underlying tokens (e.g. USDC) + * - `false` for the receiver to get aTokens (e.g. aUSDC) + * @return amountToBurn: StaticATokens burnt, static balance + * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance + **/ + function redeem( + uint256 shares, + address receiver, + address owner, + bool withdrawFromAave + ) external returns (uint256, uint256); + + /** + * @notice Deposits `ASSET` in the Aave protocol and mints static aTokens to msg.sender + * @param assets The amount of underlying `ASSET` to deposit (e.g. deposit of 100 USDC) + * @param receiver The address that will receive the static aTokens + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param depositToAave bool + * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) + * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) + * @return uint256 The amount of StaticAToken minted, static balance + **/ + function deposit( + uint256 assets, + address receiver, + uint16 referralCode, + bool depositToAave + ) external returns (uint256); + + /** + * @notice Allows to deposit on Aave via meta-transaction + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param depositor Address from which the funds to deposit are going to be pulled + * @param receiver Address that will receive the staticATokens, in the average case, same as the `depositor` + * @param assets The amount to deposit + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param depositToAave bool + * - `true` if the msg.sender comes with underlying tokens (e.g. USDC) + * - `false` if the msg.sender comes already with aTokens (e.g. aUSDC) + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param sigParams Signature params: v,r,s + * @return uint256 The amount of StaticAToken minted, static balance + */ + function metaDeposit( + address depositor, + address receiver, + uint256 assets, + uint16 referralCode, + bool depositToAave, + uint256 deadline, + PermitParams calldata permit, + SignatureParams calldata sigParams + ) external returns (uint256); + + /** + * @notice Allows to withdraw from Aave via meta-transaction + * https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param owner Address owning the staticATokens + * @param receiver Address that will receive the underlying withdrawn from Aave + * @param shares The amount of staticAToken to withdraw. If > 0, `assets` needs to be 0 + * @param assets The amount of underlying/aToken to withdraw. If > 0, `shares` needs to be 0 + * @param withdrawFromAave bool + * - `true` for the receiver to get underlying tokens (e.g. USDC) + * - `false` for the receiver to get aTokens (e.g. aUSDC) + * @param deadline The deadline timestamp, type(uint256).max for max deadline + * @param sigParams Signature params: v,r,s + * @return amountToBurn: StaticATokens burnt, static balance + * @return amountToWithdraw: underlying/aToken send to `receiver`, dynamic balance + */ + function metaWithdraw( + address owner, + address receiver, + uint256 shares, + uint256 assets, + bool withdrawFromAave, + uint256 deadline, + SignatureParams calldata sigParams + ) external returns (uint256, uint256); + + /** + * @notice Returns the Aave liquidity index of the underlying aToken, denominated rate here + * as it can be considered as an ever-increasing exchange rate + * @return The liquidity index + **/ + function rate() external view returns (uint256); + + /** + * @notice Claims rewards from `INCENTIVES_CONTROLLER` and updates internal accounting of rewards. + * @param reward The reward to claim + * @return uint256 Amount collected + */ + function collectAndUpdateRewards(address reward) external returns (uint256); + + /** + * @notice Claim rewards on behalf of a user and send them to a receiver + * @dev Only callable by if sender is onBehalfOf or sender is approved claimer + * @param onBehalfOf The address to claim on behalf of + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewardsOnBehalf( + address onBehalfOf, + address receiver, + address[] memory rewards + ) external; + + /** + * @notice Claim rewards and send them to a receiver + * @param receiver The address to receive the rewards + * @param rewards The rewards to claim + */ + function claimRewards(address receiver, address[] memory rewards) external; + + /** + * @notice Claim rewards + * @param rewards The rewards to claim + */ + function claimRewardsToSelf(address[] memory rewards) external; + + /** + * @notice Get the total claimable rewards of the contract. + * @param reward The reward to claim + * @return uint256 The current balance + pending rewards from the `_incentivesController` + */ + function getTotalClaimableRewards(address reward) external view returns (uint256); + + /** + * @notice Get the total claimable rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The claimable amount of rewards in WAD + */ + function getClaimableRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The unclaimed rewards for a user in WAD + * @param user The address of the user + * @param reward The reward to claim + * @return uint256 The unclaimed amount of rewards in WAD + */ + function getUnclaimedRewards(address user, address reward) external view returns (uint256); + + /** + * @notice The underlying asset reward index in RAY + * @param reward The reward to claim + * @return uint256 The underlying asset reward index in RAY + */ + function getCurrentRewardsIndex(address reward) external view returns (uint256); + + /** + * @notice The aToken used inside the 4626 vault. + * @return IERC20 The aToken IERC20. + */ + function aToken() external view returns (IERC20); + + /** + * @notice The IERC20s that are currently rewarded to addresses of the vault via LM on incentivescontroller. + * @return IERC20 The IERC20s of the rewards. + */ + function rewardTokens() external view returns (address[] memory); + + /** + * @notice Fetches all rewardTokens from the incentivecontroller and registers the missing ones. + */ + function refreshRewardTokens() external; + + /** + * @notice Checks if the passed token is a registered reward. + * @return bool signaling if token is a registered reward. + */ + function isRegisteredRewardToken(address reward) external view returns (bool); +} diff --git a/src/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol b/src/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol index 2d5cd360..25b07174 100644 --- a/src/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol +++ b/src/periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.18; import {CapsEngine} from './libraries/CapsEngine.sol'; diff --git a/src/periphery/contracts/v3-config-engine/AaveV3Payload.sol b/src/periphery/contracts/v3-config-engine/AaveV3Payload.sol index 003d3d3e..57ed5837 100644 --- a/src/periphery/contracts/v3-config-engine/AaveV3Payload.sol +++ b/src/periphery/contracts/v3-config-engine/AaveV3Payload.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; import {Address} from 'solidity-utils/contracts/oz-common/Address.sol'; diff --git a/src/periphery/contracts/v3-config-engine/EngineFlags.sol b/src/periphery/contracts/v3-config-engine/EngineFlags.sol index de54c40d..822292a6 100644 --- a/src/periphery/contracts/v3-config-engine/EngineFlags.sol +++ b/src/periphery/contracts/v3-config-engine/EngineFlags.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.18; library EngineFlags { diff --git a/src/periphery/contracts/v3-config-engine/README.md b/src/periphery/contracts/v3-config-engine/README.md new file mode 100644 index 00000000..8a1ccf7c --- /dev/null +++ b/src/periphery/contracts/v3-config-engine/README.md @@ -0,0 +1,63 @@ +## Aave v3 config engine + +![Config Engine Diagram](../../../../resources/configs-engine.svg) + +## What is the AaveV3ConfigEngine? + +The `AaveV3ConfigEngine` is a helper smart contract to abstract good practices when doing "admin" interactions with the Aave v3 protocol, but built on top, without touching the core contracts. + +At the same time, it defines a new interface oriented to simplify developer experience when coding proposal payloads: the `AaveV3ConfigEngine` is built from our experience supervising governance payloads review, for actions like full cycle of listing assets, modify caps (supply/borrow), changing collateral or borrow related parameters and changing the price feeds of assets. + +_Note: In an effort for unification and streamlining deployment procedures, the config engine has been migrated from the [aave-helpers](https://github.com/bgd-labs/aave-helpers) repository to this location._ + +## How to use the engine? + +The engine is not designed to be used directly when writing a payload, but through abstract contracts that we will call `Base Aave v3 Payloads`. + +This [aave-helpers](https://github.com/bgd-labs/aave-helpers) repository contains `Base Aave v3 Payloads` for all the Aave v3 instances, under the hood powered by this config engine and aave-address-book, and abstracting all the complexity: ordering of execution of actions, extra validations, deciding when to keep a current configured parameter and how to get it, etc. + +As base for any payload, you only need to inherit from the corresponding (per pool) `Base Aave v3 Payload`, for example inheriting from `AaveV3PayloadEthereum` in the case of Ethereum, `AaveV3PayloadAvalanche` in the case of Avalanche, and so on. + +If you want just to do one or multiple listings, you only need to define the listing within a `newListings()` function, and the base payload will take care of executing it correctly for you. + +Do you want instead to update supply/borrow caps? Same approach as with the listings, you only need to define the update of caps within a `capsUpdates()` function, and the base payload will take care of the rest. + +Do you want to update the price-feed of an asset? You only need to define the update of price feed within a `priceFeedsUpdates()` function, and the base payload will take care of the rest. + +Change collateral-related parameters? Same approach as previous, you only need to define the update within a `collateralsUpdates()` function, and the base payload will take care of the rest. + +Change Borrow-related parameters? Same as previous, just define the update within a `borrowsUpdates()` function, and the base payload will take care of the rest. + +Change eMode category configuration? Same as previous, just define the update within a `eModeCategoriesUpdates()` function, and the base payload will take care of the rest. + +Change eMode category of a particular asset? Same as previous, just define the update within a `assetsEModeUpdates()` function, and the base payload will take care of the rest. + +### Internal aspects to consider + +- Frequently, at the same time that you want to do an update of parameters or listing, you also want to do something extra before or after. + The `Base Aave v3 Payload` defines `_preExecute()` and `_postExecute()` hook functions, that you can redefine on your payload and will the execute before and after all configs changes/listings you define. + +- The payload also allow you to group changes of parameters and listings, just by defining at the same time the aforementioned `newListings()`, `capsUpdate()` and/or `collateralsUpdates()` and so on. For reference, the execution ordering is the following: + 1. `_preExecute()` + 2. `eModeCategoriesUpdates()` + 3. `newListings()` + 4. `newListingsCustom()` + 5. `borrowsUpdates()` + 6. `collateralsUpdates()` + 7. `rateStrategiesUpdates()` + 8. `priceFeedsUpdates()` + 9. `assetsEModeUpdates()` + 10. `capsUpdates()` + 11. `_postExecute()` + +## Links to examples + +- [Simple mock listing on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockListing.sol) +- [Simple custom mock listing on Aave V3 with custom token impl](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockListingCustom.sol) +- [Mock e-mode category update on Aave V3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdate.sol) +- [Mock e-mode asset update on Aave V3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockAssetEModeUpdate.sol) +- [Mock caps updates (only supply, keeping current borrow cap) on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockCapUpdate.sol) +- [Mock collateral updates (changing some, keeping current values on others), on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdate.sol) +- [Mock borrow updates (changing some, keeping current values on others), on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdate.sol) +- [Mock rates updates (changing some, keeping current values on others), on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockRatesUpdate.sol) +- [Mock price feed updates on Aave v3](../../../../tests/periphery/v3-config-engine/mocks/AaveV3MockPriceFeedUpdate.sol) diff --git a/tests/AaveV3BatchDeployment.t.sol b/tests/AaveV3BatchDeployment.t.sol index 1e7dca5f..d5af5e2e 100644 --- a/tests/AaveV3BatchDeployment.t.sol +++ b/tests/AaveV3BatchDeployment.t.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import '../src/deployments/interfaces/IMarketReportTypes.sol'; -import {ConfigEngineDeployer} from './utils/ConfigEngineDeployer.sol'; import {AugustusRegistryMock} from './mocks/AugustusRegistryMock.sol'; import {MockParaSwapFeeClaimer} from 'aave-v3-periphery/contracts/mocks/swap/MockParaSwapFeeClaimer.sol'; @@ -15,20 +14,19 @@ import {IPoolAddressesProvider} from 'aave-v3-core/contracts/interfaces/IPoolAdd import {IAaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/IAaveV3ConfigEngine.sol'; import {IPool} from 'aave-v3-core/contracts/interfaces/IPool.sol'; import {AaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; +import {SequencerOracle} from 'aave-v3-core/contracts/mocks/oracle/SequencerOracle.sol'; contract AaveV3BatchDeployment is BatchTestProcedures { address public marketOwner; address public emergencyAdmin; Roles public roles; - MarketConfig public config; DeployFlags public flags; + MarketConfig config; MarketReport deployedContracts; address public weth9; - event ReportLog(MarketReport report); - function setUp() public { bytes32 emptySalt; weth9 = address(new WETH9()); @@ -44,6 +42,8 @@ contract AaveV3BatchDeployment is BatchTestProcedures { 8, address(new AugustusRegistryMock()), address(new MockParaSwapFeeClaimer()), + address(0), // l2SequencerUptimeFeed + 0, // l2PriceOracleSentinelGracePeriod 8080, emptySalt, weth9, @@ -63,9 +63,8 @@ contract AaveV3BatchDeployment is BatchTestProcedures { ); checkFullReport(flags, fullReport); - address engine = ConfigEngineDeployer.deployEngine(vm, fullReport); AaveV3TestListing testnetListingPayload = new AaveV3TestListing( - IAaveV3ConfigEngine(engine), + IAaveV3ConfigEngine(fullReport.configEngine), marketOwner, weth9, fullReport @@ -81,6 +80,8 @@ contract AaveV3BatchDeployment is BatchTestProcedures { function testAaveV3L2BatchDeploymentCheck() public { flags.l2 = true; + config.l2SequencerUptimeFeed = address(new SequencerOracle(poolAdmin)); + config.l2PriceOracleSentinelGracePeriod = 2 hours; MarketReport memory fullReport = deployAaveV3Testnet( marketOwner, @@ -92,9 +93,8 @@ contract AaveV3BatchDeployment is BatchTestProcedures { checkFullReport(flags, fullReport); - address engine = ConfigEngineDeployer.deployEngine(vm, fullReport); AaveV3TestListing testnetListingPayload = new AaveV3TestListing( - IAaveV3ConfigEngine(engine), + IAaveV3ConfigEngine(fullReport.configEngine), marketOwner, weth9, fullReport diff --git a/tests/AaveV3BatchTests.t.sol b/tests/AaveV3BatchTests.t.sol index f90652f8..e1969fed 100644 --- a/tests/AaveV3BatchTests.t.sol +++ b/tests/AaveV3BatchTests.t.sol @@ -11,6 +11,9 @@ import {AaveV3GettersBatchTwo} from '../src/deployments/projects/aave-v3-batched import {AaveV3PeripheryBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol'; import {AaveV3ParaswapBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3ParaswapBatch.sol'; import {AaveV3SetupBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol'; +import {AaveV3MiscBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3MiscBatch.sol'; +import {AaveV3HelpersBatchOne} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchOne.sol'; +import {AaveV3HelpersBatchTwo} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchTwo.sol'; import {WETH9} from 'aave-v3-core/contracts/dependencies/weth/WETH9.sol'; import {AugustusRegistryMock} from './mocks/AugustusRegistryMock.sol'; import {MockParaSwapFeeClaimer} from 'aave-v3-periphery/contracts/mocks/swap/MockParaSwapFeeClaimer.sol'; @@ -35,13 +38,15 @@ contract AaveV3BatchTests is BatchTestProcedures { PeripheryReport peripheryReportOne; ParaswapReport paraswapReportOne; + MiscReport miscReport; + ConfigEngineReport configEngineReport; + StaticATokenReport staticATokenReport; + AaveV3TokensBatch.TokensReport tokensReport; SetupReport setupReportTwo; AaveV3SetupBatch aaveV3SetupOne; - event ReportLog(MarketReport report); - function setUp() public { deployer = makeAddr('deployer'); marketOwner = makeAddr('marketOwner'); @@ -56,6 +61,8 @@ contract AaveV3BatchTests is BatchTestProcedures { 8, address(new AugustusRegistryMock()), address(new MockParaSwapFeeClaimer()), + address(0), // l2SequencerUptimeFeed + 0, // l2PriceOracleSentinelGracePeriod 8080, emptySalt, address(new WETH9()), @@ -65,20 +72,33 @@ contract AaveV3BatchTests is BatchTestProcedures { ); flags = DeployFlags(false); + // Etch the create2 factory + vm.etch( + 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); + ( marketReportOne, gettersReportOne, poolReportOne, peripheryReportOne, - paraswapReportOne, + miscReport, aaveV3SetupOne ) = deployCoreAndPeriphery(roles, config, flags, deployedContracts); - (, , gettersReportTwo, , setupReportTwo, , , ) = deployAndSetup( - roles, - config, - flags, - deployedContracts - ); + + ( + , + , + gettersReportTwo, + , + setupReportTwo, + , + miscReport, + tokensReport, + paraswapReportOne, + + ) = deployAndSetup(roles, config, flags, deployedContracts); } function testAaveV3FullBatchOrchestration() public { @@ -117,7 +137,7 @@ contract AaveV3BatchTests is BatchTestProcedures { new AaveV3PoolBatch(marketReportOne.poolAddressesProvider); } - function test2AaveV3L2PoolDeployment() public { + function test3AaveV3L2PoolDeployment() public { new AaveV3L2PoolBatch(marketReportOne.poolAddressesProvider); } @@ -130,7 +150,16 @@ contract AaveV3BatchTests is BatchTestProcedures { ); } - function test5PeripheralsRelease() public { + function test5MiscDeployment() public { + new AaveV3MiscBatch( + flags.l2, + marketReportOne.poolAddressesProvider, + config.l2SequencerUptimeFeed, + config.l2PriceOracleSentinelGracePeriod + ); + } + + function test6ParaswapRelease() public { new AaveV3ParaswapBatch( roles.poolAdmin, config, @@ -139,7 +168,7 @@ contract AaveV3BatchTests is BatchTestProcedures { ); } - function test6SetupMarket() public { + function test7SetupMarket() public { vm.prank(roles.marketOwner); aaveV3SetupOne.setupAaveV3Market( roles, @@ -148,11 +177,34 @@ contract AaveV3BatchTests is BatchTestProcedures { poolReportOne.poolConfiguratorImplementation, gettersReportOne.protocolDataProvider, peripheryReportOne.aaveOracle, - peripheryReportOne.rewardsControllerImplementation + peripheryReportOne.rewardsControllerImplementation, + miscReport.priceOracleSentinel ); } - function test7TokensMarket() public { + function test8TokensMarket() public { new AaveV3TokensBatch(setupReportTwo.poolProxy); } + + function test9ConfigEngineDeployment() public { + new AaveV3HelpersBatchOne( + setupReportTwo.poolProxy, + setupReportTwo.poolConfiguratorProxy, + miscReport.defaultInterestRateStrategy, + peripheryReportOne.aaveOracle, + setupReportTwo.rewardsControllerProxy, + peripheryReportOne.treasury, + tokensReport.aToken, + tokensReport.variableDebtToken, + tokensReport.stableDebtToken + ); + } + + function test10StaticATokenDeployment() public { + new AaveV3HelpersBatchTwo( + setupReportTwo.poolProxy, + setupReportTwo.rewardsControllerProxy, + peripheryReportOne.proxyAdmin + ); + } } diff --git a/tests/DeploymentsGasLimits.t.sol b/tests/DeploymentsGasLimits.t.sol index ba1082f5..28d5d8e4 100644 --- a/tests/DeploymentsGasLimits.t.sol +++ b/tests/DeploymentsGasLimits.t.sol @@ -11,9 +11,13 @@ import {AaveV3GettersBatchTwo} from '../src/deployments/projects/aave-v3-batched import {AaveV3PeripheryBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3PeripheryBatch.sol'; import {AaveV3ParaswapBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3ParaswapBatch.sol'; import {AaveV3SetupBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol'; +import {AaveV3MiscBatch} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3MiscBatch.sol'; +import {AaveV3HelpersBatchOne} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchOne.sol'; +import {AaveV3HelpersBatchTwo} from '../src/deployments/projects/aave-v3-batched/batches/AaveV3HelpersBatchTwo.sol'; import {WETH9} from 'aave-v3-core/contracts/dependencies/weth/WETH9.sol'; import {AugustusRegistryMock} from './mocks/AugustusRegistryMock.sol'; import {MockParaSwapFeeClaimer} from 'aave-v3-periphery/contracts/mocks/swap/MockParaSwapFeeClaimer.sol'; +import {SequencerOracle} from '../src/core/contracts/mocks/oracle/SequencerOracle.sol'; import {BatchTestProcedures} from './utils/BatchTestProcedures.sol'; contract DeploymentsGasLimits is BatchTestProcedures { @@ -30,6 +34,8 @@ contract DeploymentsGasLimits is BatchTestProcedures { PeripheryReport peripheryReportOne; ParaswapReport paraswapReportOne; + MiscReport miscReport; + AaveV3TokensBatch.TokensReport tokensReport; SetupReport setupReportTwo; @@ -51,6 +57,8 @@ contract DeploymentsGasLimits is BatchTestProcedures { 8, address(new AugustusRegistryMock()), // replace with mock of augustus registry address(new MockParaSwapFeeClaimer()), + address(new SequencerOracle(poolAdmin)), + 2 hours, // l2PriceOracleSentinelGracePeriod 8080, empty, address(new WETH9()), @@ -58,22 +66,35 @@ contract DeploymentsGasLimits is BatchTestProcedures { 0.0005e4, 0.0004e4 ); - flags = DeployFlags(false); + flags = DeployFlags(true); + + // Etch the create2 factory + vm.etch( + 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); ( marketReportOne, gettersReportOne, poolReportOne, peripheryReportOne, - paraswapReportOne, + miscReport, aaveV3SetupOne ) = deployCoreAndPeriphery(roles, config, flags, deployedContracts); - (, , gettersReportTwo, , setupReportTwo, , , ) = deployAndSetup( - roles, - config, - flags, - deployedContracts - ); + + ( + , + , + gettersReportTwo, + , + setupReportTwo, + , + miscReport, + tokensReport, + paraswapReportOne, + + ) = deployAndSetup(roles, config, flags, deployedContracts); } function test0AaveV3SetupDeployment() public { @@ -114,7 +135,16 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function test6ParaswapDeployment() public { + function test6MiscDeployment() public { + new AaveV3MiscBatch( + flags.l2, + marketReportOne.poolAddressesProvider, + config.l2SequencerUptimeFeed, + config.l2PriceOracleSentinelGracePeriod + ); + } + + function test7ParaswapDeployment() public { new AaveV3ParaswapBatch( roles.poolAdmin, config, @@ -123,7 +153,7 @@ contract DeploymentsGasLimits is BatchTestProcedures { ); } - function test7SetupMarket() public { + function test8SetupMarket() public { vm.prank(roles.marketOwner); aaveV3SetupOne.setupAaveV3Market( roles, @@ -132,14 +162,37 @@ contract DeploymentsGasLimits is BatchTestProcedures { poolReportOne.poolConfiguratorImplementation, gettersReportOne.protocolDataProvider, peripheryReportOne.aaveOracle, - peripheryReportOne.rewardsControllerImplementation + peripheryReportOne.rewardsControllerImplementation, + miscReport.priceOracleSentinel ); } - function test8TokensMarket() public { + function test9TokensMarket() public { new AaveV3TokensBatch(setupReportTwo.poolProxy); } + function test10ConfigEngineDeployment() public { + new AaveV3HelpersBatchOne( + setupReportTwo.poolProxy, + setupReportTwo.poolConfiguratorProxy, + miscReport.defaultInterestRateStrategy, + peripheryReportOne.aaveOracle, + setupReportTwo.rewardsControllerProxy, + peripheryReportOne.treasury, + tokensReport.aToken, + tokensReport.variableDebtToken, + tokensReport.stableDebtToken + ); + } + + function test11StaticATokenDeployment() public { + new AaveV3HelpersBatchTwo( + setupReportTwo.poolProxy, + setupReportTwo.rewardsControllerProxy, + peripheryReportOne.proxyAdmin + ); + } + function testCheckInitCodeSizeBatchs() public view { uint16 maxInitCodeSize = 49152; @@ -147,10 +200,13 @@ contract DeploymentsGasLimits is BatchTestProcedures { console.log('AaveV3L2PoolBatch', type(AaveV3L2PoolBatch).creationCode.length); console.log('AaveV3PoolBatch', type(AaveV3PoolBatch).creationCode.length); console.log('AaveV3PeripheryBatch', type(AaveV3PeripheryBatch).creationCode.length); + console.log('AaveV3MiscBatch', type(AaveV3MiscBatch).creationCode.length); console.log('AaveV3ParaswapBatch', type(AaveV3ParaswapBatch).creationCode.length); console.log('AaveV3GettersBatchOne', type(AaveV3GettersBatchOne).creationCode.length); console.log('AaveV3GettersBatchTwo', type(AaveV3GettersBatchTwo).creationCode.length); console.log('AaveV3TokensBatch', type(AaveV3TokensBatch).creationCode.length); + console.log('AaveV3HelpersBatchOne', type(AaveV3HelpersBatchOne).creationCode.length); + console.log('AaveV3HelpersBatchTwo', type(AaveV3HelpersBatchTwo).creationCode.length); assertLe( type(AaveV3SetupBatch).creationCode.length, @@ -172,6 +228,11 @@ contract DeploymentsGasLimits is BatchTestProcedures { maxInitCodeSize, 'AaveV3PeripheryBatch max init code size' ); + assertLe( + type(AaveV3MiscBatch).creationCode.length, + maxInitCodeSize, + 'AaveV3MiscBatch max init code size' + ); assertLe( type(AaveV3ParaswapBatch).creationCode.length, maxInitCodeSize, @@ -192,5 +253,15 @@ contract DeploymentsGasLimits is BatchTestProcedures { maxInitCodeSize, 'AaveV3TokensBatch max init code size' ); + assertLe( + type(AaveV3HelpersBatchOne).creationCode.length, + maxInitCodeSize, + 'AaveV3HelpersBatchOne max init code size' + ); + assertLe( + type(AaveV3HelpersBatchTwo).creationCode.length, + maxInitCodeSize, + 'AaveV3HelpersBatchTwo max init code size' + ); } } diff --git a/tests/core/PoolConfigurator.initReserves.t.sol b/tests/core/PoolConfigurator.initReserves.t.sol index 6b859094..d59ce527 100644 --- a/tests/core/PoolConfigurator.initReserves.t.sol +++ b/tests/core/PoolConfigurator.initReserves.t.sol @@ -33,7 +33,7 @@ contract PoolConfiguratorInitReservesTest is TestnetProcedures { t.variableDebtSymbol = 'varDebtMISC'; t.stableDebtName = 'Stable Debt Misc'; t.stableDebtSymbol = 'stableDebtMISC'; - t.rateStrategy = report.defaultInterestRateStrategyV2; + t.rateStrategy = report.defaultInterestRateStrategy; t.interestRateData = abi.encode( IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 80_00, @@ -265,7 +265,7 @@ contract PoolConfiguratorInitReservesTest is TestnetProcedures { t.variableDebtSymbol = 'varDebtMISC'; t.stableDebtName = 'Stable Debt Misc'; t.stableDebtSymbol = 'stableDebtMISC'; - t.rateStrategy = report.defaultInterestRateStrategyV2; + t.rateStrategy = report.defaultInterestRateStrategy; t.interestRateData = abi.encode( IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 80_00, diff --git a/tests/core/PoolConfigurator.pendingLTV.t.sol b/tests/core/PoolConfigurator.pendingLTV.t.sol index b76eb958..828db0d4 100644 --- a/tests/core/PoolConfigurator.pendingLTV.t.sol +++ b/tests/core/PoolConfigurator.pendingLTV.t.sol @@ -24,12 +24,7 @@ contract PoolConfiguratorPendingLtvTests is TestnetProcedures { function test_freezeReserve_ltvSetTo0() public { // check current ltv - ( - uint256 ltv, - uint256 liquidationThreshold, - uint256 liquidationBonus, - bool isFrozen - ) = _getReserveParams(); + (uint256 ltv, , , bool isFrozen) = _getReserveParams(); assertTrue(ltv > 0); assertEq(isFrozen, false); @@ -51,12 +46,7 @@ contract PoolConfiguratorPendingLtvTests is TestnetProcedures { function test_unfreezeReserve_pendingSetToLtv() public { // check ltv - ( - uint256 originalLtv, - uint256 liquidationThreshold, - uint256 liquidationBonus, - - ) = _getReserveParams(); + (uint256 originalLtv, , , ) = _getReserveParams(); // freeze reserve vm.startPrank(poolAdmin); @@ -154,7 +144,7 @@ contract PoolConfiguratorPendingLtvTests is TestnetProcedures { vm.stopPrank(); } - function _getReserveParams() internal returns (uint256, uint256, uint256, bool) { + function _getReserveParams() internal view returns (uint256, uint256, uint256, bool) { ( , uint256 ltv, diff --git a/tests/mocks/AaveV3TestListing.sol b/tests/mocks/AaveV3TestListing.sol index 95714b0f..2e783855 100644 --- a/tests/mocks/AaveV3TestListing.sol +++ b/tests/mocks/AaveV3TestListing.sol @@ -156,7 +156,7 @@ contract AaveV3TestListing is AaveV3Payload { } function getPoolContext() public pure override returns (IEngine.PoolContext memory) { - return IEngine.PoolContext({networkName: 'Ethereum Sepolia', networkAbbreviation: 'EthSep'}); + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); } function _postExecute() internal override { diff --git a/tests/periphery/static-a-token/StataOracle.t.sol b/tests/periphery/static-a-token/StataOracle.t.sol new file mode 100644 index 00000000..3d6b6622 --- /dev/null +++ b/tests/periphery/static-a-token/StataOracle.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {StataOracle} from '../../../src/periphery/contracts/static-a-token/StataOracle.sol'; +import {StaticATokenLM} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {BaseTest} from './TestBase.sol'; + +contract StataOracleTest is BaseTest { + StataOracle public oracle; + + function setUp() public override { + super.setUp(); + oracle = new StataOracle(contracts.poolAddressesProvider); + + vm.prank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 1_000_000); + } + + function test_assetPrice() public view { + uint256 stataPrice = oracle.getAssetPrice(address(staticATokenLM)); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(UNDERLYING); + assertGe(stataPrice, underlyingPrice); + assertEq(stataPrice, (underlyingPrice * staticATokenLM.convertToAssets(1e18)) / 1e18); + } + + function test_assetsPrices() public view { + address[] memory staticATokens = factory.getStaticATokens(); + uint256[] memory stataPrices = oracle.getAssetsPrices(staticATokens); + + for (uint256 i = 0; i < staticATokens.length; i++) { + address staticAToken = staticATokens[i]; + uint256 stataPrice = stataPrices[i]; + + address underlying = StaticATokenLM(staticAToken).asset(); + uint256 underlyingPrice = contracts.aaveOracle.getAssetPrice(underlying); + + assertGe(stataPrice, underlyingPrice); + assertEq( + stataPrice, + (underlyingPrice * StaticATokenLM(staticAToken).convertToAssets(1e18)) / 1e18 + ); + } + } + + function test_error(uint256 shares) public view { + vm.assume(shares <= staticATokenLM.maxMint(address(0))); + uint256 pricePerShare = oracle.getAssetPrice(address(staticATokenLM)); + uint256 pricePerAsset = contracts.aaveOracle.getAssetPrice(UNDERLYING); + uint256 assets = staticATokenLM.convertToAssets(shares); + + assertApproxEqAbs( + (pricePerShare * shares) / 1e18, + (pricePerAsset * assets) / 1e18, + (assets / 1e18) + 1 // there can be imprecision of 1 wei, which will accumulate for each asset + ); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenLM.t.sol b/tests/periphery/static-a-token/StaticATokenLM.t.sol new file mode 100644 index 00000000..543a0c21 --- /dev/null +++ b/tests/periphery/static-a-token/StaticATokenLM.t.sol @@ -0,0 +1,609 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; +import {IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {RayMathExplicitRounding} from '../../../src/periphery/contracts/libraries/RayMathExplicitRounding.sol'; +import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol'; +import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol'; +import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol'; +import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol'; +import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; +import {BaseTest, TestnetERC20} from './TestBase.sol'; + +contract StaticATokenLMTest is BaseTest { + using RayMathExplicitRounding for uint256; + + address public constant EMISSION_ADMIN = address(25); + + function setUp() public override { + super.setUp(); + + _configureLM(); + _openSupplyAndBorrowPositions(); + + vm.startPrank(user); + } + + function test_initializeShouldRevert() public { + address impl = factory.STATIC_A_TOKEN_IMPL(); + vm.expectRevert(); + IStaticATokenLM(impl).initialize(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8, 'hey', 'ho'); + } + + function test_getters() public view { + assertEq(staticATokenLM.name(), 'Static Aave Local WETH'); + assertEq(staticATokenLM.symbol(), 'stataLocWETH'); + + IERC20 aToken = staticATokenLM.aToken(); + assertEq(address(aToken), A_TOKEN); + + address underlyingAddress = address(staticATokenLM.asset()); + assertEq(underlyingAddress, UNDERLYING); + + IERC20Metadata underlying = IERC20Metadata(underlyingAddress); + assertEq(staticATokenLM.decimals(), underlying.decimals()); + + assertEq( + address(staticATokenLM.INCENTIVES_CONTROLLER()), + address(AToken(A_TOKEN).getIncentivesController()) + ); + } + + function test_convertersAndPreviews() public view { + uint128 amount = 5 ether; + uint256 shares = staticATokenLM.convertToShares(amount); + assertLe(shares, amount, 'SHARES LOWER'); + assertEq(shares, staticATokenLM.previewDeposit(amount), 'PREVIEW_DEPOSIT'); + assertLe(shares, staticATokenLM.previewWithdraw(amount), 'PREVIEW_WITHDRAW'); + uint256 assets = staticATokenLM.convertToAssets(amount); + assertGe(assets, shares, 'ASSETS GREATER'); + assertLe(assets, staticATokenLM.previewMint(amount), 'PREVIEW_MINT'); + assertEq(assets, staticATokenLM.previewRedeem(amount), 'PREVIEW_REDEEM'); + } + + // Redeem tests + function test_redeem() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); + assertEq(staticATokenLM.balanceOf(user), 0); + assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); + assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); + } + + function test_redeemAToken() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user)); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user, false); + assertEq(staticATokenLM.balanceOf(user), 0); + assertLe(IERC20(A_TOKEN).balanceOf(user), amountToDeposit); + assertApproxEqAbs(IERC20(A_TOKEN).balanceOf(user), amountToDeposit, 1); + } + + function test_redeemAllowance() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user)); + vm.stopPrank(); + vm.startPrank(user1); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); + assertEq(staticATokenLM.balanceOf(user), 0); + assertLe(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit); + assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user1), amountToDeposit, 1); + } + + function testFail_redeemOverflowAllowance() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + staticATokenLM.approve(user1, staticATokenLM.maxRedeem(user) / 2); + vm.stopPrank(); + vm.startPrank(user1); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user1, user); + assertEq(staticATokenLM.balanceOf(user), 0); + assertEq(IERC20(A_TOKEN).balanceOf(user1), amountToDeposit); + } + + function testFail_redeemAboveBalance() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user) + 1, user, user); + } + + // Withdraw tests + function test_withdraw() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + assertLe(staticATokenLM.maxWithdraw(user), amountToDeposit); + staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user), user, user); + assertEq(staticATokenLM.balanceOf(user), 0); + assertLe(IERC20(UNDERLYING).balanceOf(user), amountToDeposit); + assertApproxEqAbs(IERC20(UNDERLYING).balanceOf(user), amountToDeposit, 1); + } + + function testFail_withdrawAboveBalance() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + _fundUser(amountToDeposit, user1); + + _depositAToken(amountToDeposit, user); + _depositAToken(amountToDeposit, user1); + + assertEq(staticATokenLM.maxWithdraw(user), amountToDeposit); + staticATokenLM.withdraw(staticATokenLM.maxWithdraw(user) + 1, user, user); + } + + // mint + function test_mint() public { + vm.stopPrank(); + + // set supply cap to non-zero + vm.startPrank(poolAdmin); + contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 15_000); + vm.stopPrank(); + + vm.startPrank(user); + + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit); + uint256 shares = 1 ether; + staticATokenLM.mint(shares, user); + assertEq(shares, staticATokenLM.balanceOf(user)); + } + + function testFail_mintAboveBalance() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _underlyingToAToken(amountToDeposit, user); + IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); + staticATokenLM.mint(amountToDeposit, user); + } + + // test rewards + function test_collectAndUpdateRewards() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); + uint256 claimable = staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN); + staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN); + assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), claimable); + } + + function test_claimRewardsToSelf() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + + uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable); + assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); + } + + function test_claimRewards() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + + uint256 claimable = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + staticATokenLM.claimRewards(user, rewardTokens); + assertEq(claimable, IERC20(REWARD_TOKEN).balanceOf(user)); + assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); + assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); + } + + // should fail as user1 is not a valid claimer + function testFail_claimRewardsOnBehalfOf() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + + vm.stopPrank(); + vm.startPrank(user1); + + staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + staticATokenLM.claimRewardsOnBehalf(user, user1, rewardTokens); + } + + function test_depositATokenClaimWithdrawClaim() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + // deposit aweth + _depositAToken(amountToDeposit, user); + + // forward time + _skipBlocks(60); + + // claim + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); + uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); + assertGt(claimable0, 0); + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); + + // forward time + _skipBlocks(60); + + // redeem + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); + uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); + assertGt(claimable1, 0); + + // claim on behalf of other user + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); + assertEq(staticATokenLM.balanceOf(user), 0); + assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); + assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); + } + + function test_depositWETHClaimWithdrawClaim() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + // forward time + _skipBlocks(60); + + // claim + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); + uint256 claimable0 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable0); + assertGt(claimable0, 0); + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable0); + + // forward time + _skipBlocks(60); + + // redeem + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); + uint256 claimable1 = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), claimable1); + assertGt(claimable1, 0); + + // claim on behalf of other user + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimable1 + claimable0); + assertEq(staticATokenLM.balanceOf(user), 0); + assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); + assertGt(AToken(UNDERLYING).balanceOf(user), 5 ether); + } + + function test_transfer() public { + uint128 amountToDeposit = 10 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + // transfer to 2nd user + staticATokenLM.transfer(user1, amountToDeposit / 2); + assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); + + // forward time + _skipBlocks(60); + + // redeem for both + uint256 claimableUser = staticATokenLM.getClaimableRewards(user, REWARD_TOKEN); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user), user, user); + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), claimableUser); + vm.stopPrank(); + vm.startPrank(user1); + uint256 claimableUser1 = staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN); + staticATokenLM.redeem(staticATokenLM.maxRedeem(user1), user1, user1); + staticATokenLM.claimRewardsToSelf(rewardTokens); + assertEq(IERC20(REWARD_TOKEN).balanceOf(user1), claimableUser1); + assertGt(claimableUser1, 0); + + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); + assertEq(staticATokenLM.getClaimableRewards(user, REWARD_TOKEN), 0); + assertEq(staticATokenLM.getClaimableRewards(user1, REWARD_TOKEN), 0); + } + + // getUnclaimedRewards + function test_getUnclaimedRewards() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + uint256 shares = _depositAToken(amountToDeposit, user); + assertEq(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); + _skipBlocks(1000); + staticATokenLM.redeem(shares, user, user); + assertGt(staticATokenLM.getUnclaimedRewards(user, REWARD_TOKEN), 0); + } + + /** + * maxDeposit test + */ + function test_maxDeposit_freeze() public { + vm.stopPrank(); + vm.startPrank(roleList.marketOwner); + contracts.poolConfiguratorProxy.setReserveFreeze(UNDERLYING, true); + + uint256 max = staticATokenLM.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_paused() public { + vm.stopPrank(); + vm.startPrank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); + + uint256 max = staticATokenLM.maxDeposit(address(0)); + + assertEq(max, 0); + } + + function test_maxDeposit_noCap() public { + vm.stopPrank(); + vm.startPrank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 0); + + uint256 maxDeposit = staticATokenLM.maxDeposit(address(0)); + uint256 maxMint = staticATokenLM.maxMint(address(0)); + + assertEq(maxDeposit, type(uint256).max); + assertEq(maxMint, type(uint256).max); + } + + // should be 0 as supply is ~5k + function test_maxDeposit_5kCap() public { + vm.stopPrank(); + vm.startPrank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 5_000); + + uint256 max = staticATokenLM.maxDeposit(address(0)); + assertEq(max, 0); + } + + function test_maxDeposit_50kCap() public { + vm.stopPrank(); + vm.startPrank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setSupplyCap(UNDERLYING, 50_000); + + uint256 max = staticATokenLM.maxDeposit(address(0)); + DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(UNDERLYING); + assertEq( + max, + 50_000 * + (10 ** IERC20Metadata(UNDERLYING).decimals()) - + (IERC20Metadata(A_TOKEN).totalSupply() + + uint256(reserveData.accruedToTreasury).rayMulRoundUp(staticATokenLM.rate())) + ); + } + + /** + * maxRedeem test + */ + function test_maxRedeem_paused() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + vm.stopPrank(); + vm.startPrank(address(roleList.marketOwner)); + contracts.poolConfiguratorProxy.setReservePause(UNDERLYING, true); + + uint256 max = staticATokenLM.maxRedeem(address(user)); + + assertEq(max, 0); + } + + function test_maxRedeem_allAvailable() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + uint256 max = staticATokenLM.maxRedeem(address(user)); + + assertEq(max, staticATokenLM.balanceOf(user)); + } + + function test_maxRedeem_partAvailable() public { + uint128 amountToDeposit = 50 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + uint256 maxRedeemBefore = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); + uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); + + // create rich user + address borrowUser = address(99); + vm.startPrank(borrowUser); + deal(address(wbtc), borrowUser, 2_000e8); + wbtc.approve(address(POOL), 2_000e8); + POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); + + // borrow all available + POOL.borrow(UNDERLYING, underlyingBalanceBefore - (maxRedeemBefore / 2), 2, 0, borrowUser); + + uint256 maxRedeemAfter = staticATokenLM.previewRedeem(staticATokenLM.maxRedeem(address(user))); + assertApproxEqAbs(maxRedeemAfter, (maxRedeemBefore / 2), 1); + } + + function test_maxRedeem_nonAvailable() public { + uint128 amountToDeposit = 50 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + vm.stopPrank(); + + uint256 underlyingBalanceBefore = IERC20Metadata(UNDERLYING).balanceOf(A_TOKEN); + // create rich user + address borrowUser = address(99); + vm.startPrank(borrowUser); + deal(address(wbtc), borrowUser, 2_000e8); + wbtc.approve(address(POOL), 2_000e8); + POOL.deposit(address(wbtc), 2_000e8, borrowUser, 0); + + // borrow all available + contracts.poolProxy.borrow(UNDERLYING, underlyingBalanceBefore, 2, 0, borrowUser); + + uint256 maxRedeemAfter = staticATokenLM.maxRedeem(address(user)); + assertEq(maxRedeemAfter, 0); + } + + function test_permit() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: staticATokenLM.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + staticATokenLM.PERMIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + + assertEq(staticATokenLM.allowance(permit.owner, spender), permit.value); + } + + function test_permit_expired() public { + // as the default timestamp is 0, we move ahead in time a bit + vm.warp(10 days); + + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: spender, + value: 1 ether, + nonce: staticATokenLM.nonces(user), + deadline: block.timestamp - 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + staticATokenLM.PERMIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert('PERMIT_DEADLINE_EXPIRED'); + staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } + + function test_permit_invalidSigner() public { + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: address(424242), + spender: spender, + value: 1 ether, + nonce: staticATokenLM.nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + staticATokenLM.PERMIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, permitDigest); + + vm.expectRevert('INVALID_SIGNER'); + staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s); + } + + function _configureLM() internal { + PullRewardsTransferStrategy strat = new PullRewardsTransferStrategy( + report.rewardsControllerProxy, + EMISSION_ADMIN, + EMISSION_ADMIN + ); + + vm.startPrank(poolAdmin); + contracts.emissionManager.setEmissionAdmin(REWARD_TOKEN, EMISSION_ADMIN); + vm.stopPrank(); + + vm.startPrank(EMISSION_ADMIN); + IERC20(REWARD_TOKEN).approve(address(strat), 10_000 ether); + vm.stopPrank(); + + vm.startPrank(OWNER); + TestnetERC20(REWARD_TOKEN).mint(EMISSION_ADMIN, 10_000 ether); + vm.stopPrank(); + + RewardsDataTypes.RewardsConfigInput[] memory config = new RewardsDataTypes.RewardsConfigInput[]( + 1 + ); + config[0] = RewardsDataTypes.RewardsConfigInput( + 0.00385 ether, + 10_000 ether, + uint32(block.timestamp + 30 days), + A_TOKEN, + REWARD_TOKEN, + ITransferStrategyBase(strat), + IEACAggregatorProxy(address(2)) + ); + + vm.prank(EMISSION_ADMIN); + contracts.emissionManager.configureAssets(config); + + staticATokenLM.refreshRewardTokens(); + } + + function _openSupplyAndBorrowPositions() internal { + // this is to open borrow positions so that the aToken balance increases + address whale = address(79); + vm.startPrank(whale); + _fundUser(5_000 ether, whale); + + weth.approve(address(POOL), 5_000 ether); + POOL.deposit(address(weth), 5_000 ether, whale, 0); + + POOL.borrow(address(weth), 1_000 ether, 2, 0, whale); + vm.stopPrank(); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol b/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol new file mode 100644 index 00000000..62dd690a --- /dev/null +++ b/tests/periphery/static-a-token/StaticATokenMetaTransactions.t.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IERC20WithPermit} from 'solidity-utils/contracts/oz-common/interfaces/IERC20WithPermit.sol'; +import {StaticATokenLM, IStaticATokenLM, IERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {SigUtils} from '../../utils/SigUtils.sol'; +import {BaseTest, IAToken, IRewardsController, DataTypes} from './TestBase.sol'; + +contract StaticATokenMetaTransactions is BaseTest { + function setUp() public override { + super.setUp(); + + // Testing meta transactions with USDX as WETH does not support permit + DataTypes.ReserveDataLegacy memory reserveDataUSDX = contracts.poolProxy.getReserveData( + address(usdx) + ); + UNDERLYING = address(usdx); + A_TOKEN = reserveDataUSDX.aTokenAddress; + + staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); + + vm.startPrank(user); + } + + function test_validateDomainSeparator() public view { + address[] memory staticATokens = factory.getStaticATokens(); + + for (uint256 i = 0; i < staticATokens.length; i++) { + bytes32 separator1 = StaticATokenLM(staticATokens[i]).DOMAIN_SEPARATOR(); + for (uint256 j = 0; j < staticATokens.length; j++) { + if (i != j) { + bytes32 separator2 = StaticATokenLM(staticATokens[j]).DOMAIN_SEPARATOR(); + assertNotEq(separator1, separator2, 'DOMAIN_SEPARATOR_MUST_BE_UNIQUE'); + } + } + } + } + + function test_metaDepositATokenUnderlyingNoPermit() public { + uint128 amountToDeposit = 5e6; + deal(UNDERLYING, user, amountToDeposit); + IERC20(UNDERLYING).approve(address(staticATokenLM), 1e6); + IStaticATokenLM.PermitParams memory permitParams; + + // generate combined permit + SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ + owner: user, + spender: spender, + value: 1e6, + referralCode: 0, + fromUnderlying: true, + nonce: staticATokenLM.nonces(user), + deadline: block.timestamp + 1 days, + permit: permitParams + }); + bytes32 digest = SigUtils.getTypedDepositHash( + depositPermit, + staticATokenLM.METADEPOSIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); + + IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); + + uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + staticATokenLM.metaDeposit( + depositPermit.owner, + depositPermit.spender, + depositPermit.value, + depositPermit.referralCode, + depositPermit.fromUnderlying, + depositPermit.deadline, + permitParams, + sigParams + ); + + assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + } + + function test_metaDepositATokenUnderlying() public { + uint128 amountToDeposit = 5e6; + deal(UNDERLYING, user, amountToDeposit); + + // permit for aToken deposit + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(staticATokenLM), + value: 1e6, + nonce: IERC20WithPermit(UNDERLYING).nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + staticATokenLM.PERMIT_TYPEHASH(), + IERC20WithPermit(UNDERLYING).DOMAIN_SEPARATOR() + ); + + (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); + + IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + pV, + pR, + pS + ); + + // generate combined permit + SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ + owner: user, + spender: spender, + value: permit.value, + referralCode: 0, + fromUnderlying: true, + nonce: staticATokenLM.nonces(user), + deadline: permit.deadline, + permit: permitParams + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + userPrivateKey, + SigUtils.getTypedDepositHash( + depositPermit, + staticATokenLM.METADEPOSIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ) + ); + + IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); + + uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + uint256 shares = staticATokenLM.metaDeposit( + depositPermit.owner, + depositPermit.spender, + depositPermit.value, + depositPermit.referralCode, + depositPermit.fromUnderlying, + depositPermit.deadline, + permitParams, + sigParams + ); + assertEq(shares, previewDeposit); + assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + } + + function test_metaDepositAToken() public { + uint128 amountToDeposit = 5e6; + _fundUser(amountToDeposit, user); + _underlyingToAToken(amountToDeposit, user); + + // permit for aToken deposit + SigUtils.Permit memory permit = SigUtils.Permit({ + owner: user, + spender: address(staticATokenLM), + value: 1e6, + nonce: IERC20WithPermit(A_TOKEN).nonces(user), + deadline: block.timestamp + 1 days + }); + + bytes32 permitDigest = SigUtils.getTypedDataHash( + permit, + staticATokenLM.PERMIT_TYPEHASH(), + IERC20WithPermit(A_TOKEN).DOMAIN_SEPARATOR() + ); + + (uint8 pV, bytes32 pR, bytes32 pS) = vm.sign(userPrivateKey, permitDigest); + + IStaticATokenLM.PermitParams memory permitParams = IStaticATokenLM.PermitParams( + permit.owner, + permit.spender, + permit.value, + permit.deadline, + pV, + pR, + pS + ); + + // generate combined permit + SigUtils.DepositPermit memory depositPermit = SigUtils.DepositPermit({ + owner: user, + spender: spender, + value: permit.value, + referralCode: 0, + fromUnderlying: false, + nonce: staticATokenLM.nonces(user), + deadline: permit.deadline, + permit: permitParams + }); + bytes32 digest = SigUtils.getTypedDepositHash( + depositPermit, + staticATokenLM.METADEPOSIT_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); + + IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); + + uint256 previewDeposit = staticATokenLM.previewDeposit(depositPermit.value); + + staticATokenLM.metaDeposit( + depositPermit.owner, + depositPermit.spender, + depositPermit.value, + depositPermit.referralCode, + depositPermit.fromUnderlying, + depositPermit.deadline, + permitParams, + sigParams + ); + + assertEq(staticATokenLM.balanceOf(depositPermit.spender), previewDeposit); + } + + function test_metaWithdraw() public { + uint128 amountToDeposit = 5e6; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + SigUtils.WithdrawPermit memory permit = SigUtils.WithdrawPermit({ + owner: user, + spender: spender, + staticAmount: 0, + dynamicAmount: 1e6, + toUnderlying: false, + nonce: staticATokenLM.nonces(user), + deadline: block.timestamp + 1 days + }); + bytes32 digest = SigUtils.getTypedWithdrawHash( + permit, + staticATokenLM.METAWITHDRAWAL_TYPEHASH(), + staticATokenLM.DOMAIN_SEPARATOR() + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey, digest); + + IStaticATokenLM.SignatureParams memory sigParams = IStaticATokenLM.SignatureParams(v, r, s); + + staticATokenLM.metaWithdraw( + permit.owner, + permit.spender, + permit.staticAmount, + permit.dynamicAmount, + permit.toUnderlying, + permit.deadline, + sigParams + ); + + assertEq(IERC20(A_TOKEN).balanceOf(permit.spender), permit.dynamicAmount); + } +} diff --git a/tests/periphery/static-a-token/StaticATokenNoLM.t.sol b/tests/periphery/static-a-token/StaticATokenNoLM.t.sol new file mode 100644 index 00000000..84ddbd33 --- /dev/null +++ b/tests/periphery/static-a-token/StaticATokenNoLM.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {BaseTest, IERC20} from './TestBase.sol'; + +/** + * Testing the static token wrapper on a pool that never had LM enabled + * This is a slightly different assumption than a pool that doesn't have LM enabled any more as incentivesController.rewardTokens() will have length=0 + */ +contract StaticATokenNoLMTest is BaseTest { + function setUp() public override { + super.setUp(); + + vm.startPrank(user); + } + + // test rewards + function test_collectAndUpdateRewardsWithLMDisabled() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); + assertEq(staticATokenLM.getTotalClaimableRewards(REWARD_TOKEN), 0); + assertEq(staticATokenLM.collectAndUpdateRewards(REWARD_TOKEN), 0); + assertEq(IERC20(REWARD_TOKEN).balanceOf(address(staticATokenLM)), 0); + } + + function test_claimRewardsToSelfWithLMDisabled() public { + uint128 amountToDeposit = 5 ether; + _fundUser(amountToDeposit, user); + + _depositAToken(amountToDeposit, user); + + _skipBlocks(60); + + try staticATokenLM.getClaimableRewards(user, REWARD_TOKEN) {} catch Error( + string memory reason + ) { + require(keccak256(bytes(reason)) == keccak256(bytes('9'))); + } + + try staticATokenLM.claimRewardsToSelf(rewardTokens) {} catch Error(string memory reason) { + require(keccak256(bytes(reason)) == keccak256(bytes('9'))); + } + assertEq(IERC20(REWARD_TOKEN).balanceOf(user), 0); + } +} diff --git a/tests/periphery/static-a-token/TestBase.sol b/tests/periphery/static-a-token/TestBase.sol new file mode 100644 index 00000000..058ad713 --- /dev/null +++ b/tests/periphery/static-a-token/TestBase.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +import {IRewardsController} from '../../../src/periphery/contracts/rewards/interfaces/IRewardsController.sol'; +import {TransparentUpgradeableProxy} from 'solidity-utils/contracts/transparent-proxy/TransparentUpgradeableProxy.sol'; +import {ITransparentProxyFactory} from 'solidity-utils/contracts/transparent-proxy/TransparentProxyFactory.sol'; +import {IPool} from '../../../src/core/contracts/interfaces/IPool.sol'; +import {StaticATokenFactory} from '../../../src/periphery/contracts/static-a-token/StaticATokenFactory.sol'; +import {StaticATokenLM, IStaticATokenLM, IERC20, IERC20Metadata, ERC20} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol'; +import {IAToken} from '../../../src/core/contracts/interfaces/IAToken.sol'; +import {TestnetProcedures, TestnetERC20} from '../../utils/TestnetProcedures.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol'; + +abstract contract BaseTest is TestnetProcedures { + address constant OWNER = address(1234); + + address public user; + address public user1; + address internal spender; + + uint256 internal userPrivateKey; + uint256 internal spenderPrivateKey; + + StaticATokenLM public staticATokenLM; + address public proxyAdmin; + ITransparentProxyFactory public proxyFactory; + StaticATokenFactory public factory; + + address[] rewardTokens; + + address public UNDERLYING; + address public A_TOKEN; + address public REWARD_TOKEN; + IPool public POOL; + + function setUp() public virtual { + userPrivateKey = 0xA11CE; + spenderPrivateKey = 0xB0B0; + user = address(vm.addr(userPrivateKey)); + user1 = address(vm.addr(2)); + spender = vm.addr(spenderPrivateKey); + + initTestEnvironment(); + DataTypes.ReserveDataLegacy memory reserveDataWETH = contracts.poolProxy.getReserveData( + tokenList.weth + ); + + UNDERLYING = address(weth); + REWARD_TOKEN = address(new TestnetERC20('LM Reward ERC20', 'RWD', 18, OWNER)); + A_TOKEN = reserveDataWETH.aTokenAddress; + POOL = contracts.poolProxy; + + rewardTokens.push(REWARD_TOKEN); + + proxyFactory = ITransparentProxyFactory(report.transparentProxyFactory); + proxyAdmin = report.proxyAdmin; + + factory = StaticATokenFactory(report.staticATokenFactoryProxy); + factory.createStaticATokens(POOL.getReservesList()); + + staticATokenLM = StaticATokenLM(factory.getStaticAToken(UNDERLYING)); + } + + function _fundUser(uint128 amountToDeposit, address targetUser) internal { + deal(UNDERLYING, targetUser, amountToDeposit); + } + + function _skipBlocks(uint128 blocks) internal { + vm.roll(block.number + blocks); + vm.warp(block.timestamp + blocks * 12); // assuming a block is around 12seconds + } + + function _underlyingToAToken(uint256 amountToDeposit, address targetUser) internal { + IERC20(UNDERLYING).approve(address(POOL), amountToDeposit); + POOL.deposit(UNDERLYING, amountToDeposit, targetUser, 0); + } + + function _depositAToken(uint256 amountToDeposit, address targetUser) internal returns (uint256) { + _underlyingToAToken(amountToDeposit, targetUser); + IERC20(A_TOKEN).approve(address(staticATokenLM), amountToDeposit); + return staticATokenLM.deposit(amountToDeposit, targetUser, 10, false); + } + + function testAdmin() public { + vm.stopPrank(); + vm.startPrank(proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(staticATokenLM))).admin(), proxyAdmin); + assertEq(TransparentUpgradeableProxy(payable(address(factory))).admin(), proxyAdmin); + vm.stopPrank(); + } +} diff --git a/tests/periphery/v3-config-engine/AaveV3ConfigEngineTest.t.sol b/tests/periphery/v3-config-engine/AaveV3ConfigEngineTest.t.sol new file mode 100644 index 00000000..0454771d --- /dev/null +++ b/tests/periphery/v3-config-engine/AaveV3ConfigEngineTest.t.sol @@ -0,0 +1,670 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {IAaveV3ConfigEngine} from '../../../src/periphery/contracts/v3-config-engine/IAaveV3ConfigEngine.sol'; +import {AaveV3MockListing} from './mocks/AaveV3MockListing.sol'; +import {AaveV3MockListingCustom} from './mocks/AaveV3MockListingCustom.sol'; +import {AaveV3MockCapUpdate} from './mocks/AaveV3MockCapUpdate.sol'; +import {AaveV3MockCollateralUpdate} from './mocks/AaveV3MockCollateralUpdate.sol'; +import {AaveV3MockCollateralUpdateNoChange} from './mocks/AaveV3MockCollateralUpdateNoChange.sol'; +import {AaveV3MockCollateralUpdateWrongBonus, AaveV3MockCollateralUpdateCorrectBonus} from './mocks/AaveV3MockCollateralUpdateWrongBonus.sol'; +import {AaveV3MockBorrowUpdate} from './mocks/AaveV3MockBorrowUpdate.sol'; +import {AaveV3MockBorrowUpdateNoChange} from './mocks/AaveV3MockBorrowUpdateNoChange.sol'; +import {AaveV3MockRatesUpdate} from './mocks/AaveV3MockRatesUpdate.sol'; +import {AaveV3MockPriceFeedUpdate} from './mocks/AaveV3MockPriceFeedUpdate.sol'; +import {AaveV3MockEModeCategoryUpdate, AaveV3MockEModeCategoryUpdateEdgeBonus} from './mocks/AaveV3MockEModeCategoryUpdate.sol'; +import {AaveV3MockEModeCategoryUpdateNoChange} from './mocks/AaveV3MockEModeCategoryUpdateNoChange.sol'; +import {AaveV3MockAssetEModeUpdate} from './mocks/AaveV3MockAssetEModeUpdate.sol'; + +import {ATokenInstance} from '../../../src/core/instances/ATokenInstance.sol'; +import {StableDebtTokenInstance} from '../../../src/core/instances/StableDebtTokenInstance.sol'; +import {VariableDebtTokenInstance} from '../../../src/core/instances/VariableDebtTokenInstance.sol'; +import {TestnetProcedures, AaveV3ConfigEngine} from '../../utils/TestnetProcedures.sol'; +import {TestnetERC20} from '../../../src/periphery/contracts/mocks/testnet-helpers/TestnetERC20.sol'; +import {MockAggregator} from '../../../src/core/contracts/mocks/oracle/CLAggregators/MockAggregator.sol'; +import {IPool, IPoolAddressesProvider} from '../../utils/ProtocolV3TestBase.sol'; +import {DataTypes} from '../../../src/core/contracts/protocol/libraries/types/DataTypes.sol'; +import {ProtocolV3TestBase, IDefaultInterestRateStrategyV2, ReserveConfig, ReserveTokens, DataTypes as DataTypeOld} from '../../utils/ProtocolV3TestBase.sol'; + +contract AaveV3ConfigEngineTest is TestnetProcedures, ProtocolV3TestBase { + using stdStorage for StdStorage; + address configEngine; + + function setUp() public { + initTestEnvironment(); + configEngine = report.configEngine; + } + + event CollateralConfigurationChanged( + address indexed asset, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus + ); + + event EModeCategoryAdded( + uint8 indexed categoryId, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + address oracle, + string label + ); + + function testListings() public { + address asset = address(new TestnetERC20('1INCH', '1INCH', 18, address(this))); + + address feed = address(new MockAggregator(int256(25e8))); + AaveV3MockListing payload = new AaveV3MockListing(asset, feed, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineListing', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineListing', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineListing', 'postTestEngineListing'); + + ReserveConfig memory expectedAssetConfig = ReserveConfig({ + symbol: '1INCH', + underlying: asset, + aToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + variableDebtToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + stableDebtToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + decimals: 18, + ltv: 82_50, + liquidationThreshold: 86_00, + liquidationBonus: 105_00, + liquidationProtocolFee: 10_00, + reserveFactor: 10_00, + usageAsCollateralEnabled: true, + borrowingEnabled: true, + interestRateStrategy: AaveV3ConfigEngine(configEngine).DEFAULT_INTEREST_RATE_STRATEGY(), + stableBorrowRateEnabled: false, + isPaused: false, + isActive: true, + isFrozen: false, + isSiloed: false, + isBorrowableInIsolation: false, + isFlashloanable: false, + supplyCap: 85_000, + borrowCap: 60_000, + debtCeiling: 0, + eModeCategory: 0 + }); + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + + _noReservesConfigsChangesApartNewListings(allConfigsBefore, allConfigsAfter); + + _validateReserveTokensImpls( + _findReserveConfigBySymbol(allConfigsAfter, '1INCH'), + ReserveTokens({ + aToken: address(contracts.aToken), + stableDebtToken: address(contracts.stableDebtToken), + variableDebtToken: address(contracts.variableDebtToken) + }) + ); + + _validateAssetSourceOnOracle( + IPoolAddressesProvider(address(contracts.poolAddressesProvider)), + asset, + feed + ); + + _validateInterestRateStrategy( + asset, + contracts.protocolDataProvider.getInterestRateStrategyAddress(asset), + AaveV3ConfigEngine(configEngine).DEFAULT_INTEREST_RATE_STRATEGY(), + IDefaultInterestRateStrategyV2.InterestRateDataRay({ + optimalUsageRatio: _bpsToRay(payload.newListings()[0].rateStrategyParams.optimalUsageRatio), + baseVariableBorrowRate: _bpsToRay( + payload.newListings()[0].rateStrategyParams.baseVariableBorrowRate + ), + variableRateSlope1: _bpsToRay( + payload.newListings()[0].rateStrategyParams.variableRateSlope1 + ), + variableRateSlope2: _bpsToRay( + payload.newListings()[0].rateStrategyParams.variableRateSlope2 + ) + }) + ); + } + + function testListingsCustom() public { + address asset = address(new TestnetERC20('PSP', 'PSP', 18, address(this))); + + address feed = address(new MockAggregator(int256(15e8))); + address aTokenImpl = address(new ATokenInstance(contracts.poolProxy)); + address vTokenImpl = address(new VariableDebtTokenInstance(contracts.poolProxy)); + address sTokenImpl = address(new StableDebtTokenInstance(contracts.poolProxy)); + + AaveV3MockListingCustom payload = new AaveV3MockListingCustom( + asset, + feed, + configEngine, + aTokenImpl, + vTokenImpl, + sTokenImpl + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineListingCustom', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineListingCustom', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineListingCustom', 'postTestEngineListingCustom'); + + ReserveConfig memory expectedAssetConfig = ReserveConfig({ + symbol: 'PSP', + underlying: asset, + aToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + variableDebtToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + stableDebtToken: address(0), // Mock, as they don't get validated, because of the "dynamic" deployment on proposal execution + decimals: 18, + ltv: 82_50, + liquidationThreshold: 86_00, + liquidationBonus: 105_00, + liquidationProtocolFee: 10_00, + reserveFactor: 10_00, + usageAsCollateralEnabled: true, + borrowingEnabled: true, + interestRateStrategy: AaveV3ConfigEngine(configEngine).DEFAULT_INTEREST_RATE_STRATEGY(), + stableBorrowRateEnabled: false, + isPaused: false, + isActive: true, + isFrozen: false, + isSiloed: false, + isBorrowableInIsolation: false, + isFlashloanable: false, + supplyCap: 85_000, + borrowCap: 60_000, + debtCeiling: 0, + eModeCategory: 0 + }); + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + + _noReservesConfigsChangesApartNewListings(allConfigsBefore, allConfigsAfter); + + _validateReserveTokensImpls( + _findReserveConfigBySymbol(allConfigsAfter, 'PSP'), + ReserveTokens({ + aToken: aTokenImpl, + stableDebtToken: sTokenImpl, + variableDebtToken: vTokenImpl + }) + ); + + _validateAssetSourceOnOracle( + IPoolAddressesProvider(address(contracts.poolAddressesProvider)), + asset, + feed + ); + + _validateInterestRateStrategy( + asset, + contracts.protocolDataProvider.getInterestRateStrategyAddress(asset), + AaveV3ConfigEngine(configEngine).DEFAULT_INTEREST_RATE_STRATEGY(), + IDefaultInterestRateStrategyV2.InterestRateDataRay({ + optimalUsageRatio: _bpsToRay( + payload.newListingsCustom()[0].base.rateStrategyParams.optimalUsageRatio + ), + baseVariableBorrowRate: _bpsToRay( + payload.newListingsCustom()[0].base.rateStrategyParams.baseVariableBorrowRate + ), + variableRateSlope1: _bpsToRay( + payload.newListingsCustom()[0].base.rateStrategyParams.variableRateSlope1 + ), + variableRateSlope2: _bpsToRay( + payload.newListingsCustom()[0].base.rateStrategyParams.variableRateSlope2 + ) + }) + ); + } + + function testCapsUpdate() public { + // this asset has been listed before + address asset = tokenList.usdx; + AaveV3MockCapUpdate payload = new AaveV3MockCapUpdate(asset, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineCaps', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineCaps', + IPool(address(contracts.poolProxy)) + ); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + + diffReports('preTestEngineCaps', 'postTestEngineCaps'); + + expectedAssetConfig.supplyCap = 1_000_000; + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + function testCollateralsUpdates() public { + // this asset has been listed before + address asset = tokenList.usdx; + AaveV3MockCollateralUpdate payload = new AaveV3MockCollateralUpdate(asset, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineCollateral', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineCollateral', + IPool(address(contracts.poolProxy)) + ); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + + diffReports('preTestEngineCollateral', 'postTestEngineCollateral'); + + expectedAssetConfig.ltv = 62_00; + expectedAssetConfig.liquidationThreshold = 72_00; + expectedAssetConfig.liquidationBonus = 106_00; // 100_00 + 6_00 + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + // TODO manage this after testFail* deprecation. + // This should not be necessary, but there seems there is no other way + // of validating that when all collateral params are KEEP_CURRENT, the config + // engine doesn't call the POOL_CONFIGURATOR. + // So the solution is expecting the event emitted on the POOL_CONFIGURATOR, + // and as this doesn't happen, expect the failure of the test + function testFailCollateralsUpdatesNoChange() public { + // this asset has been listed before + address asset = tokenList.usdx; + AaveV3MockCollateralUpdateNoChange payload = new AaveV3MockCollateralUpdateNoChange( + asset, + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineCollateralNoChange', + IPool(address(contracts.poolProxy)) + ); + + vm.expectEmit(); + emit CollateralConfigurationChanged( + allConfigsBefore[0].underlying, + allConfigsBefore[0].ltv, + allConfigsBefore[0].liquidationThreshold, + allConfigsBefore[0].liquidationBonus + ); + payload.execute(); + } + + // Same as testFailCollateralsUpdatesNoChange, but this time should work, as we are not expecting any event emitted + function testCollateralsUpdatesNoChange() public { + // this asset has been listed before + address asset = tokenList.usdx; + AaveV3MockCollateralUpdateNoChange payload = new AaveV3MockCollateralUpdateNoChange( + asset, + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineCollateralNoChange', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineCollateralNoChange', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineCollateralNoChange', 'postTestEngineCollateralNoChange'); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + function testCollateralUpdateWrongBonus() public { + address asset = tokenList.usdx; + AaveV3MockCollateralUpdateWrongBonus payload = new AaveV3MockCollateralUpdateWrongBonus( + asset, + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + vm.expectRevert(bytes('INVALID_LT_LB_RATIO')); + payload.execute(); + } + + function testCollateralUpdateCorrectBonus() public { + address asset = tokenList.usdx; + AaveV3MockCollateralUpdateCorrectBonus payload = new AaveV3MockCollateralUpdateCorrectBonus( + asset, + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineCollateralEdgeBonus', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineCollateralEdgeBonus', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineCollateralEdgeBonus', 'postTestEngineCollateralEdgeBonus'); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + expectedAssetConfig.ltv = 62_00; + expectedAssetConfig.liquidationThreshold = 90_00; + expectedAssetConfig.liquidationBonus = 111_00; // 100_00 + 11_00 + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + function testBorrowsUpdates() public { + address asset = tokenList.usdx; + AaveV3MockBorrowUpdate payload = new AaveV3MockBorrowUpdate(asset, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineBorrow', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineBorrow', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineBorrow', 'postTestEngineBorrow'); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + expectedAssetConfig.reserveFactor = 15_00; + expectedAssetConfig.borrowingEnabled = true; + expectedAssetConfig.isFlashloanable = false; + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + function testBorrowUpdatesNoChange() public { + address asset = tokenList.usdx; + AaveV3MockBorrowUpdateNoChange payload = new AaveV3MockBorrowUpdateNoChange( + asset, + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + ReserveConfig[] memory allConfigsBefore = createConfigurationSnapshot( + 'preTestEngineBorrowNoChange', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + ReserveConfig[] memory allConfigsAfter = createConfigurationSnapshot( + 'postTestEngineBorrowNoChange', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineBorrowNoChange', 'postTestEngineBorrowNoChange'); + + ReserveConfig memory expectedAssetConfig = _findReserveConfig(allConfigsBefore, asset); + + _validateReserveConfig(expectedAssetConfig, allConfigsAfter); + } + + function testRateStrategiesUpdates() public { + address asset = tokenList.usdx; + AaveV3MockRatesUpdate payload = new AaveV3MockRatesUpdate(asset, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + createConfigurationSnapshot('preTestEngineRates', IPool(address(contracts.poolProxy))); + + payload.execute(); + + createConfigurationSnapshot('postTestEngineRates', IPool(address(contracts.poolProxy))); + + diffReports('preTestEngineRates', 'postTestEngineRates'); + + _validateInterestRateStrategy( + asset, + contracts.protocolDataProvider.getInterestRateStrategyAddress(asset), + AaveV3ConfigEngine(configEngine).DEFAULT_INTEREST_RATE_STRATEGY(), + IDefaultInterestRateStrategyV2.InterestRateDataRay({ + optimalUsageRatio: _bpsToRay(payload.rateStrategiesUpdates()[0].params.optimalUsageRatio), + baseVariableBorrowRate: _bpsToRay( + payload.rateStrategiesUpdates()[0].params.baseVariableBorrowRate + ), + variableRateSlope1: _bpsToRay(payload.rateStrategiesUpdates()[0].params.variableRateSlope1), + variableRateSlope2: _bpsToRay(payload.rateStrategiesUpdates()[0].params.variableRateSlope2) + }) + ); + } + + function testPriceFeedsUpdates() public { + address asset = tokenList.usdx; + address newFeed = address(new MockAggregator(int256(1.05e8))); + AaveV3MockPriceFeedUpdate payload = new AaveV3MockPriceFeedUpdate(asset, newFeed, configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + createConfigurationSnapshot('preTestEnginePriceFeed', IPool(address(contracts.poolProxy))); + + payload.execute(); + + createConfigurationSnapshot('postTestEnginePriceFeed', IPool(address(contracts.poolProxy))); + + diffReports('preTestEnginePriceFeed', 'postTestEnginePriceFeed'); + + _validateAssetSourceOnOracle( + IPoolAddressesProvider(address(contracts.poolAddressesProvider)), + asset, + newFeed + ); + } + + function testEModeCategoryUpdates() public { + AaveV3MockEModeCategoryUpdate payload = new AaveV3MockEModeCategoryUpdate(configEngine); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + contracts.poolProxy.getEModeCategoryData(1); + + createConfigurationSnapshot( + 'preTestEngineEModeCategoryUpdate', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + createConfigurationSnapshot( + 'postTestEngineEModeCategoryUpdate', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineEModeCategoryUpdate', 'postTestEngineEModeCategoryUpdate'); + + DataTypeOld.EModeCategory memory prevEmodeCategoryData; + prevEmodeCategoryData.ltv = 97_40; + prevEmodeCategoryData.liquidationThreshold = 97_60; + prevEmodeCategoryData.liquidationBonus = 101_50; // 100_00 + 1_50 + prevEmodeCategoryData.priceSource = address(0); + prevEmodeCategoryData.label = 'ETH Correlated'; + + _validateEmodeCategory( + IPoolAddressesProvider(address(contracts.poolAddressesProvider)), + 1, + prevEmodeCategoryData + ); + } + + function testEModeCategoryUpdatesWrongBonus() public { + AaveV3MockEModeCategoryUpdateEdgeBonus payload = new AaveV3MockEModeCategoryUpdateEdgeBonus( + configEngine + ); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + vm.expectRevert(bytes('INVALID_LT_LB_RATIO')); + payload.execute(); + } + + // TODO manage this after testFail* deprecation. + function testFailEModeCategoryUpdatesNoChange() public { + AaveV3MockEModeCategoryUpdateNoChange payload = new AaveV3MockEModeCategoryUpdateNoChange( + configEngine + ); + + DataTypes.EModeCategory memory eModeCategoryDataBefore = contracts + .poolProxy + .getEModeCategoryData(1); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + vm.expectEmit(true, true, true, true); + emit EModeCategoryAdded( + 1, + eModeCategoryDataBefore.ltv, + eModeCategoryDataBefore.liquidationThreshold, + eModeCategoryDataBefore.liquidationBonus, + eModeCategoryDataBefore.priceSource, + eModeCategoryDataBefore.label + ); + payload.execute(); + } + + // Same as testFailEModeCategoryUpdatesNoChange, but this time should work, as we are not expecting any event emitted + function testEModeCategoryUpdatesNoChange() public { + AaveV3MockEModeCategoryUpdateNoChange payload = new AaveV3MockEModeCategoryUpdateNoChange( + configEngine + ); + + DataTypes.EModeCategory memory eModeCategoryDataBefore = contracts + .poolProxy + .getEModeCategoryData(1); + + vm.prank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + + createConfigurationSnapshot( + 'preTestEngineEModeCategoryNoChange', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + createConfigurationSnapshot( + 'postTestEngineEModeCategoryNoChange', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineEModeCategoryNoChange', 'postTestEngineEModeCategoryNoChange'); + + DataTypeOld.EModeCategory memory prevEmodeCategoryData; + prevEmodeCategoryData.ltv = eModeCategoryDataBefore.ltv; + prevEmodeCategoryData.liquidationThreshold = eModeCategoryDataBefore.liquidationThreshold; + prevEmodeCategoryData.liquidationBonus = eModeCategoryDataBefore.liquidationBonus; + prevEmodeCategoryData.priceSource = eModeCategoryDataBefore.priceSource; + prevEmodeCategoryData.label = eModeCategoryDataBefore.label; + + _validateEmodeCategory( + IPoolAddressesProvider(address(contracts.poolAddressesProvider)), + 1, + prevEmodeCategoryData + ); + } + + function testAssetEModeUpdates() public { + address asset = tokenList.usdx; + + AaveV3MockEModeCategoryUpdate payloadToAddEMode = new AaveV3MockEModeCategoryUpdate( + configEngine + ); + AaveV3MockAssetEModeUpdate payload = new AaveV3MockAssetEModeUpdate(asset, configEngine); + + vm.startPrank(roleList.marketOwner); + contracts.aclManager.addPoolAdmin(address(payload)); + contracts.aclManager.addPoolAdmin(address(payloadToAddEMode)); + vm.stopPrank(); + + payloadToAddEMode.execute(); + + createConfigurationSnapshot( + 'preTestEngineAssetEModeUpdate', + IPool(address(contracts.poolProxy)) + ); + + payload.execute(); + + createConfigurationSnapshot( + 'postTestEngineAssetEModeUpdate', + IPool(address(contracts.poolProxy)) + ); + + diffReports('preTestEngineAssetEModeUpdate', 'postTestEngineAssetEModeUpdate'); + + assertEq(contracts.protocolDataProvider.getReserveEModeCategory(asset), 1); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockAssetEModeUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockAssetEModeUpdate.sol new file mode 100644 index 00000000..c92bb35a --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockAssetEModeUpdate.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock asset e-mode update, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockAssetEModeUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function assetsEModeUpdates() public view override returns (IEngine.AssetEModeUpdate[] memory) { + IEngine.AssetEModeUpdate[] memory eModeUpdate = new IEngine.AssetEModeUpdate[](1); + + eModeUpdate[0] = IEngine.AssetEModeUpdate({asset: ASSET_ADDRESS, eModeCategory: 1}); + + return eModeUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdate.sol new file mode 100644 index 00000000..9ddd3883 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdate.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock borrow update, to be able to test + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @dev Inheriting directly from AaveV3Payload for being able to inject a custom engine + * @author BGD Labs + */ +contract AaveV3MockBorrowUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function borrowsUpdates() public view override returns (IEngine.BorrowUpdate[] memory) { + IEngine.BorrowUpdate[] memory borrowsUpdate = new IEngine.BorrowUpdate[](1); + + borrowsUpdate[0] = IEngine.BorrowUpdate({ + asset: ASSET_ADDRESS, + enabledToBorrow: EngineFlags.ENABLED, + flashloanable: EngineFlags.DISABLED, + stableRateModeEnabled: EngineFlags.KEEP_CURRENT, + borrowableInIsolation: EngineFlags.KEEP_CURRENT, + withSiloedBorrowing: EngineFlags.KEEP_CURRENT, + reserveFactor: 15_00 + }); + + return borrowsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdateNoChange.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdateNoChange.sol new file mode 100644 index 00000000..f2dc7c34 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockBorrowUpdateNoChange.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock borrow update with no change, to be able to test + * @author BGD Labs + */ +contract AaveV3MockBorrowUpdateNoChange is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function borrowsUpdates() public view override returns (IEngine.BorrowUpdate[] memory) { + IEngine.BorrowUpdate[] memory borrowsUpdate = new IEngine.BorrowUpdate[](1); + + borrowsUpdate[0] = IEngine.BorrowUpdate({ + asset: ASSET_ADDRESS, + enabledToBorrow: EngineFlags.KEEP_CURRENT, + flashloanable: EngineFlags.KEEP_CURRENT, + stableRateModeEnabled: EngineFlags.KEEP_CURRENT, + borrowableInIsolation: EngineFlags.KEEP_CURRENT, + withSiloedBorrowing: EngineFlags.KEEP_CURRENT, + reserveFactor: EngineFlags.KEEP_CURRENT + }); + + return borrowsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockCapUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockCapUpdate.sol new file mode 100644 index 00000000..faa17850 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockCapUpdate.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock caps update, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockCapUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function capsUpdates() public view override returns (IEngine.CapsUpdate[] memory) { + IEngine.CapsUpdate[] memory capsUpdate = new IEngine.CapsUpdate[](1); + + capsUpdate[0] = IEngine.CapsUpdate({ + asset: ASSET_ADDRESS, + supplyCap: 1_000_000, + borrowCap: EngineFlags.KEEP_CURRENT + }); + + return capsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdate.sol new file mode 100644 index 00000000..0a409ace --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdate.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock collateral update, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockCollateralUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function collateralsUpdates() public view override returns (IEngine.CollateralUpdate[] memory) { + IEngine.CollateralUpdate[] memory collateralsUpdate = new IEngine.CollateralUpdate[](1); + + collateralsUpdate[0] = IEngine.CollateralUpdate({ + asset: ASSET_ADDRESS, + ltv: 62_00, + liqThreshold: 72_00, + liqBonus: 6_00, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + return collateralsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateNoChange.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateNoChange.sol new file mode 100644 index 00000000..a70285f4 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateNoChange.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock collateral update with no changes, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockCollateralUpdateNoChange is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function collateralsUpdates() public view override returns (IEngine.CollateralUpdate[] memory) { + IEngine.CollateralUpdate[] memory collateralsUpdate = new IEngine.CollateralUpdate[](1); + + collateralsUpdate[0] = IEngine.CollateralUpdate({ + asset: ASSET_ADDRESS, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + return collateralsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateWrongBonus.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateWrongBonus.sol new file mode 100644 index 00000000..50727042 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockCollateralUpdateWrongBonus.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contracts for a mock collateral update, with wrong LT/LB ratio + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockCollateralUpdateWrongBonus is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function collateralsUpdates() public view override returns (IEngine.CollateralUpdate[] memory) { + IEngine.CollateralUpdate[] memory collateralsUpdate = new IEngine.CollateralUpdate[](1); + + collateralsUpdate[0] = IEngine.CollateralUpdate({ + asset: ASSET_ADDRESS, + ltv: 62_00, + liqThreshold: 90_00, + liqBonus: 12_00, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + return collateralsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Avalanche', networkAbbreviation: 'Ava'}); + } +} + +/** + * @dev Smart contracts for a mock collateral update, with correct (but edge) LT/LB ratio + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockCollateralUpdateCorrectBonus is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function collateralsUpdates() public view override returns (IEngine.CollateralUpdate[] memory) { + IEngine.CollateralUpdate[] memory collateralsUpdate = new IEngine.CollateralUpdate[](1); + + collateralsUpdate[0] = IEngine.CollateralUpdate({ + asset: ASSET_ADDRESS, + ltv: 62_00, + liqThreshold: 90_00, + liqBonus: 11_00, + debtCeiling: EngineFlags.KEEP_CURRENT, + liqProtocolFee: EngineFlags.KEEP_CURRENT + }); + + return collateralsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdate.sol new file mode 100644 index 00000000..63a540a3 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdate.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock emode category update, to be able to test + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @dev Inheriting directly from AaveV3Payload for being able to inject a custom engine + * @author BGD Labs + */ +contract AaveV3MockEModeCategoryUpdate is AaveV3Payload { + constructor(address customEngine) AaveV3Payload(IEngine(customEngine)) {} + + function eModeCategoriesUpdates() + public + pure + override + returns (IEngine.EModeCategoryUpdate[] memory) + { + IEngine.EModeCategoryUpdate[] memory eModeUpdates = new IEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IEngine.EModeCategoryUpdate({ + eModeCategory: 1, + ltv: 97_40, + liqThreshold: 97_60, + liqBonus: 1_50, + priceSource: address(0), + label: 'ETH Correlated' + }); + + return eModeUpdates; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Polygon', networkAbbreviation: 'Pol'}); + } +} + +/** + * @dev Smart contract for a mock emode category update, to be able to test + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @dev Inheriting directly from AaveV3Payload for being able to inject a custom engine + * @author BGD Labs + */ +contract AaveV3MockEModeCategoryUpdateEdgeBonus is AaveV3Payload { + constructor(address customEngine) AaveV3Payload(IEngine(customEngine)) {} + + function eModeCategoriesUpdates() + public + pure + override + returns (IEngine.EModeCategoryUpdate[] memory) + { + IEngine.EModeCategoryUpdate[] memory eModeUpdates = new IEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IEngine.EModeCategoryUpdate({ + eModeCategory: 1, + ltv: 97_40, + liqThreshold: 97_60, + liqBonus: 2_50, + priceSource: EngineFlags.KEEP_CURRENT_ADDRESS, + label: EngineFlags.KEEP_CURRENT_STRING + }); + + return eModeUpdates; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdateNoChange.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdateNoChange.sol new file mode 100644 index 00000000..a3bbc237 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockEModeCategoryUpdateNoChange.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock e-mode category update with no changes, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockEModeCategoryUpdateNoChange is AaveV3Payload { + constructor(address customEngine) AaveV3Payload(IEngine(customEngine)) {} + + function eModeCategoriesUpdates() + public + pure + override + returns (IEngine.EModeCategoryUpdate[] memory) + { + IEngine.EModeCategoryUpdate[] memory eModeUpdates = new IEngine.EModeCategoryUpdate[](1); + + eModeUpdates[0] = IEngine.EModeCategoryUpdate({ + eModeCategory: 1, + ltv: EngineFlags.KEEP_CURRENT, + liqThreshold: EngineFlags.KEEP_CURRENT, + liqBonus: EngineFlags.KEEP_CURRENT, + priceSource: EngineFlags.KEEP_CURRENT_ADDRESS, + label: EngineFlags.KEEP_CURRENT_STRING + }); + + return eModeUpdates; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockListing.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockListing.sol new file mode 100644 index 00000000..576e3248 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockListing.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock listing, to be able to test without having a v3 instance on Local + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @dev Inheriting directly from AaveV3Payload for being able to inject a custom engine + * @author BGD Labs + */ +contract AaveV3MockListing is AaveV3Payload { + address public immutable ASSET_ADDRESS; + address public immutable ASSET_FEED; + + constructor( + address assetAddress, + address assetFeed, + address customEngine + ) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + ASSET_FEED = assetFeed; + } + + function newListings() public view override returns (IEngine.Listing[] memory) { + IEngine.Listing[] memory listings = new IEngine.Listing[](1); + + listings[0] = IEngine.Listing({ + asset: ASSET_ADDRESS, + assetSymbol: '1INCH', + priceFeed: ASSET_FEED, + rateStrategyParams: IEngine.InterestRateInputData({ + optimalUsageRatio: 80_00, + baseVariableBorrowRate: 25, // 0.25% + variableRateSlope1: 3_00, + variableRateSlope2: 75_00 + }), + enabledToBorrow: EngineFlags.ENABLED, + stableRateModeEnabled: EngineFlags.DISABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.DISABLED, + ltv: 82_50, + liqThreshold: 86_00, + liqBonus: 5_00, + reserveFactor: 10_00, + supplyCap: 85_000, + borrowCap: 60_000, + debtCeiling: 0, + liqProtocolFee: 10_00, + eModeCategory: 0 + }); + + return listings; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockListingCustom.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockListingCustom.sol new file mode 100644 index 00000000..6eb3981f --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockListingCustom.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock custom listing update, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockListingCustom is AaveV3Payload { + address public immutable ASSET_ADDRESS; + address public immutable ASSET_FEED; + + address public immutable A_TOKEN_IMPL; + address public immutable V_TOKEN_IMPL; + address public immutable S_TOKEN_IMPL; + + constructor( + address assetAddress, + address assetFeed, + address customEngine, + address aTokenImpl, + address vTokenImpl, + address sTokenImpl + ) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + ASSET_FEED = assetFeed; + A_TOKEN_IMPL = aTokenImpl; + V_TOKEN_IMPL = vTokenImpl; + S_TOKEN_IMPL = sTokenImpl; + } + + function newListingsCustom() + public + view + override + returns (IEngine.ListingWithCustomImpl[] memory) + { + IEngine.ListingWithCustomImpl[] memory listingsCustom = new IEngine.ListingWithCustomImpl[](1); + + listingsCustom[0] = IEngine.ListingWithCustomImpl( + IEngine.Listing({ + asset: ASSET_ADDRESS, + assetSymbol: 'PSP', + priceFeed: ASSET_FEED, + rateStrategyParams: IEngine.InterestRateInputData({ + optimalUsageRatio: 80_00, + baseVariableBorrowRate: 25, // 0.25% + variableRateSlope1: 3_00, + variableRateSlope2: 75_00 + }), + enabledToBorrow: EngineFlags.ENABLED, + stableRateModeEnabled: EngineFlags.DISABLED, + borrowableInIsolation: EngineFlags.DISABLED, + withSiloedBorrowing: EngineFlags.DISABLED, + flashloanable: EngineFlags.DISABLED, + ltv: 82_50, + liqThreshold: 86_00, + liqBonus: 5_00, + reserveFactor: 10_00, + supplyCap: 85_000, + borrowCap: 60_000, + debtCeiling: 0, + liqProtocolFee: 10_00, + eModeCategory: 0 + }), + IEngine.TokenImplementations({ + aToken: A_TOKEN_IMPL, + vToken: V_TOKEN_IMPL, + sToken: S_TOKEN_IMPL + }) + ); + + return listingsCustom; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockPriceFeedUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockPriceFeedUpdate.sol new file mode 100644 index 00000000..45997e59 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockPriceFeedUpdate.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock price feed update, to be able to test + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @dev Inheriting directly from AaveV3Payload for being able to inject a custom engine + * @author BGD Labs + */ +contract AaveV3MockPriceFeedUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + address public immutable ASSET_FEED; + + constructor( + address assetAddress, + address feed, + address customEngine + ) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + ASSET_FEED = feed; + } + + function priceFeedsUpdates() public view override returns (IEngine.PriceFeedUpdate[] memory) { + IEngine.PriceFeedUpdate[] memory priceFeedsUpdate = new IEngine.PriceFeedUpdate[](1); + + priceFeedsUpdate[0] = IEngine.PriceFeedUpdate({asset: ASSET_ADDRESS, priceFeed: ASSET_FEED}); + + return priceFeedsUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/periphery/v3-config-engine/mocks/AaveV3MockRatesUpdate.sol b/tests/periphery/v3-config-engine/mocks/AaveV3MockRatesUpdate.sol new file mode 100644 index 00000000..a4444058 --- /dev/null +++ b/tests/periphery/v3-config-engine/mocks/AaveV3MockRatesUpdate.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../../../../src/periphery/contracts/v3-config-engine/AaveV3Payload.sol'; + +/** + * @dev Smart contract for a mock rate strategy params update, for testing purposes + * IMPORTANT Parameters are pseudo-random, DON'T USE THIS ANYHOW IN PRODUCTION + * @author BGD Labs + */ +contract AaveV3MockRatesUpdate is AaveV3Payload { + address public immutable ASSET_ADDRESS; + + constructor(address assetAddress, address customEngine) AaveV3Payload(IEngine(customEngine)) { + ASSET_ADDRESS = assetAddress; + } + + function rateStrategiesUpdates() + public + view + override + returns (IEngine.RateStrategyUpdate[] memory) + { + IEngine.RateStrategyUpdate[] memory ratesUpdate = new IEngine.RateStrategyUpdate[](1); + + ratesUpdate[0] = IEngine.RateStrategyUpdate({ + asset: ASSET_ADDRESS, + params: IEngine.InterestRateInputData({ + optimalUsageRatio: 50_00, + baseVariableBorrowRate: 30, // 0.30% + variableRateSlope1: 4_00, + variableRateSlope2: 76_00 + }) + }); + + return ratesUpdate; + } + + function getPoolContext() public pure override returns (IEngine.PoolContext memory) { + return IEngine.PoolContext({networkName: 'Local', networkAbbreviation: 'Loc'}); + } +} diff --git a/tests/utils/BatchTestProcedures.sol b/tests/utils/BatchTestProcedures.sol index b91f0e96..c704f4de 100644 --- a/tests/utils/BatchTestProcedures.sol +++ b/tests/utils/BatchTestProcedures.sol @@ -6,6 +6,7 @@ import '../../src/deployments/interfaces/IMarketReportTypes.sol'; import {DeployUtils} from '../../src/deployments/contracts/utilities/DeployUtils.sol'; import {AaveV3GettersBatchOne} from '../../src/deployments/projects/aave-v3-batched/batches/AaveV3GettersBatchOne.sol'; import {AaveV3GettersBatchTwo} from '../../src/deployments/projects/aave-v3-batched/batches/AaveV3GettersBatchTwo.sol'; +import {AaveV3TokensBatch} from '../../src/deployments/projects/aave-v3-batched/batches/AaveV3TokensBatch.sol'; import {AaveV3SetupBatch} from '../../src/deployments/projects/aave-v3-batched/batches/AaveV3SetupBatch.sol'; import {FfiUtils} from '../../src/deployments/contracts/utilities/FfiUtils.sol'; import {DefaultMarketInput} from '../../src/deployments/inputs/DefaultMarketInput.sol'; @@ -62,7 +63,7 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput AaveV3GettersBatchOne.GettersReportBatchOne memory gettersReport1, PoolReport memory poolReport, PeripheryReport memory peripheryReport, - ParaswapReport memory paraswapReport, + MiscReport memory miscReport, AaveV3SetupBatch setupContract ) { @@ -91,21 +92,14 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput address(setupContract) ); - paraswapReport = AaveV3BatchOrchestration._deployParaswapAdapters( - roles, - config, + miscReport = AaveV3BatchOrchestration._deployMisc( + flags.l2, initialReport.poolAddressesProvider, - peripheryReport.treasury + config.l2SequencerUptimeFeed, + config.l2PriceOracleSentinelGracePeriod ); - return ( - initialReport, - gettersReport1, - poolReport, - peripheryReport, - paraswapReport, - setupContract - ); + return (initialReport, gettersReport1, poolReport, peripheryReport, miscReport, setupContract); } function deployAndSetup( @@ -122,6 +116,8 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput PoolReport memory poolReport, SetupReport memory setupReport, PeripheryReport memory peripheryReport, + MiscReport memory miscReport, + AaveV3TokensBatch.TokensReport memory tokensReport, ParaswapReport memory paraswapReport, AaveV3SetupBatch setupContract ) @@ -131,7 +127,7 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput gettersReport1, poolReport, peripheryReport, - paraswapReport, + miscReport, setupContract ) = deployCoreAndPeriphery(roles, config, flags, deployedContracts); @@ -143,7 +139,15 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput poolReport.poolConfiguratorImplementation, gettersReport1.protocolDataProvider, peripheryReport.aaveOracle, - peripheryReport.rewardsControllerImplementation + peripheryReport.rewardsControllerImplementation, + miscReport.priceOracleSentinel + ); + + paraswapReport = AaveV3BatchOrchestration._deployParaswapAdapters( + roles, + config, + initialReport.poolAddressesProvider, + peripheryReport.treasury ); gettersReport2 = AaveV3BatchOrchestration._deployGettersBatch2( @@ -153,6 +157,8 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput flags.l2 ); + tokensReport = AaveV3BatchOrchestration._deployTokens(setupReport.poolProxy); + return ( initialReport, gettersReport1, @@ -160,6 +166,8 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput poolReport, setupReport, peripheryReport, + miscReport, + tokensReport, paraswapReport, setupContract ); @@ -174,10 +182,7 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput assertTrue(r.poolConfiguratorImplementation != address(0), 'r.poolConfiguratorImplementation'); assertTrue(r.protocolDataProvider != address(0), 'report.protocolDataProvider'); assertTrue(r.aaveOracle != address(0), 'report.aaveOracle'); - assertTrue( - r.defaultInterestRateStrategyV2 != address(0), - 'report.defaultInterestRateStrategyV2' - ); + assertTrue(r.defaultInterestRateStrategy != address(0), 'report.defaultInterestRateStrategy'); assertTrue(r.aclManager != address(0), 'report.aclManager'); assertTrue(r.treasury != address(0), 'report.treasury'); assertTrue(r.proxyAdmin != address(0), 'report.proxyAdmin'); @@ -193,6 +198,7 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput if (flags.l2) { assertTrue(r.l2Encoder != address(0), 'report.l2Encoder'); + assertTrue(r.priceOracleSentinel != address(0), 'report.priceOracleSentinel'); } assertTrue(r.aToken != address(0), 'report.aToken'); @@ -204,6 +210,14 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput 'r.rewardsControllerImplementation' ); assertTrue(r.rewardsControllerProxy != address(0), 'report.rewardsControllerProxy'); + assertTrue(r.configEngine != address(0), 'report.configEngine'); + assertTrue( + r.staticATokenFactoryImplementation != address(0), + 'report.staticATokenFactoryImplementation' + ); + assertTrue(r.staticATokenFactoryProxy != address(0), 'report.staticATokenFactoryProxy'); + assertTrue(r.staticATokenImplementation != address(0), 'report.staticATokenImplementation'); + assertTrue(r.transparentProxyFactory != address(0), 'report.transparentProxyFactory'); } function deployAaveV3Testnet( @@ -215,6 +229,12 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput ) internal returns (MarketReport memory testReport) { detectFoundryLibrariesAndDelete(); + // Etch the create2 factory + vm.etch( + 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); + vm.startPrank(deployer); MarketReport memory deployReport = AaveV3BatchOrchestration.deployAaveV3( deployer, @@ -266,7 +286,7 @@ contract BatchTestProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput t.variableDebtSymbol = _concatStr('varDebtMISC ', x); t.stableDebtName = _concatStr('Stable Debt Misc ', x); t.stableDebtSymbol = _concatStr('stableDebtMISC ', x); - t.rateStrategy = r.defaultInterestRateStrategyV2; + t.rateStrategy = r.defaultInterestRateStrategy; t.interestRateData = abi.encode( IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 80_00, diff --git a/tests/utils/ConfigEngineDeployer.sol b/tests/utils/ConfigEngineDeployer.sol deleted file mode 100644 index 1c25da03..00000000 --- a/tests/utils/ConfigEngineDeployer.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.0; - -import '../../src/deployments/interfaces/IMarketReportTypes.sol'; -import {Vm} from 'forge-std/Vm.sol'; -import {Create2Utils} from '../../src/deployments/contracts/utilities/Create2Utils.sol'; -import {AaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/AaveV3ConfigEngine.sol'; -import {IAaveV3ConfigEngine} from 'aave-v3-periphery/contracts/v3-config-engine/IAaveV3ConfigEngine.sol'; -import {IPoolAddressesProvider} from '../../src/core/contracts/interfaces/IPoolAddressesProvider.sol'; -import {IPool} from '../../src/core/contracts/interfaces/IPool.sol'; -import {IPoolConfigurator} from '../../src/core/contracts/interfaces/IPoolConfigurator.sol'; -import {IAaveOracle} from '../../src/core/contracts/interfaces/IAaveOracle.sol'; -import {CapsEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/CapsEngine.sol'; -import {BorrowEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/BorrowEngine.sol'; -import {CollateralEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/CollateralEngine.sol'; -import {RateEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/RateEngine.sol'; -import {PriceFeedEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/PriceFeedEngine.sol'; -import {EModeEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/EModeEngine.sol'; -import {ListingEngine} from 'aave-v3-periphery/contracts/v3-config-engine/libraries/ListingEngine.sol'; - -library ConfigEngineDeployer { - function deployEngine(Vm vm, MarketReport memory report) internal returns (address) { - // Etch the create2 factory in the local env - vm.etch( - 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7, - hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' - ); - IAaveV3ConfigEngine.EngineLibraries memory engineLibraries = IAaveV3ConfigEngine - .EngineLibraries({ - listingEngine: Create2Utils._create2Deploy('v1', type(ListingEngine).creationCode), - eModeEngine: Create2Utils._create2Deploy('v1', type(EModeEngine).creationCode), - borrowEngine: Create2Utils._create2Deploy('v1', type(BorrowEngine).creationCode), - collateralEngine: Create2Utils._create2Deploy('v1', type(CollateralEngine).creationCode), - priceFeedEngine: Create2Utils._create2Deploy('v1', type(PriceFeedEngine).creationCode), - rateEngine: Create2Utils._create2Deploy('v1', type(RateEngine).creationCode), - capsEngine: Create2Utils._create2Deploy('v1', type(CapsEngine).creationCode) - }); - - IAaveV3ConfigEngine.EngineConstants memory engineConstants = IAaveV3ConfigEngine - .EngineConstants({ - pool: IPool(report.poolProxy), - poolConfigurator: IPoolConfigurator(report.poolConfiguratorProxy), - defaultInterestRateStrategy: report.defaultInterestRateStrategyV2, - oracle: IAaveOracle(report.aaveOracle), - rewardsController: report.rewardsControllerProxy, - collector: report.treasury - }); - - return - address( - new AaveV3ConfigEngine( - report.aToken, - report.variableDebtToken, - report.stableDebtToken, - engineConstants, - engineLibraries - ) - ); - } -} diff --git a/tests/utils/ProtocolV3TestBase.sol b/tests/utils/ProtocolV3TestBase.sol index 8f4aabad..efe4b083 100644 --- a/tests/utils/ProtocolV3TestBase.sol +++ b/tests/utils/ProtocolV3TestBase.sol @@ -110,18 +110,6 @@ struct LocalVars { ReserveConfig[] configs; } -struct InterestStrategyValues { - address addressesProvider; - uint256 optimalUsageRatio; - uint256 optimalStableToTotalDebtRatio; - uint256 baseStableBorrowRate; - uint256 stableRateSlope1; - uint256 stableRateSlope2; - uint256 baseVariableBorrowRate; - uint256 variableRateSlope1; - uint256 variableRateSlope2; -} - /** * only applicable to harmony at this point */ @@ -615,7 +603,7 @@ contract ProtocolV3TestBase is DiffUtils { address reserve, address interestRateStrategyAddress, address expectedStrategy, - InterestStrategyValues memory expectedStrategyValues + IDefaultInterestRateStrategyV2.InterestRateDataRay memory expectedStrategyValues ) internal view { IDefaultInterestRateStrategyV2 strategy = IDefaultInterestRateStrategyV2( interestRateStrategyAddress @@ -630,10 +618,6 @@ contract ProtocolV3TestBase is DiffUtils { strategy.getOptimalUsageRatio(reserve) == expectedStrategyValues.optimalUsageRatio, '_validateInterestRateStrategy() : INVALID_OPTIMAL_RATIO' ); - require( - address(strategy.ADDRESSES_PROVIDER()) == expectedStrategyValues.addressesProvider, - '_validateInterestRateStrategy() : INVALID_ADDRESSES_PROVIDER' - ); require( strategy.getBaseVariableBorrowRate(reserve) == expectedStrategyValues.baseVariableBorrowRate, '_validateInterestRateStrategy() : INVALID_BASE_VARIABLE_BORROW' diff --git a/tests/utils/SigUtils.sol b/tests/utils/SigUtils.sol new file mode 100644 index 00000000..8c64e400 --- /dev/null +++ b/tests/utils/SigUtils.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.10; + +import {IStaticATokenLM} from '../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol'; + +library SigUtils { + struct Permit { + address owner; + address spender; + uint256 value; + uint256 nonce; + uint256 deadline; + } + + struct WithdrawPermit { + address owner; + address spender; + uint256 staticAmount; + uint256 dynamicAmount; + bool toUnderlying; + uint256 nonce; + uint256 deadline; + } + + struct DepositPermit { + address owner; + address spender; + uint256 value; + uint16 referralCode; + bool fromUnderlying; + uint256 nonce; + uint256 deadline; + IStaticATokenLM.PermitParams permit; + } + + // computes the hash of a permit + function getStructHash(Permit memory _permit, bytes32 typehash) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + typehash, + _permit.owner, + _permit.spender, + _permit.value, + _permit.nonce, + _permit.deadline + ) + ); + } + + function getWithdrawHash( + WithdrawPermit memory permit, + bytes32 typehash + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + typehash, + permit.owner, + permit.spender, + permit.staticAmount, + permit.dynamicAmount, + permit.toUnderlying, + permit.nonce, + permit.deadline + ) + ); + } + + function getDepositHash( + DepositPermit memory permit, + bytes32 typehash + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + typehash, + permit.owner, + permit.spender, + permit.value, + permit.referralCode, + permit.fromUnderlying, + permit.nonce, + permit.deadline, + permit.permit + ) + ); + } + + // computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer + function getTypedDataHash( + Permit memory permit, + bytes32 typehash, + bytes32 domainSeparator + ) public pure returns (bytes32) { + return + keccak256(abi.encodePacked('\x19\x01', domainSeparator, getStructHash(permit, typehash))); + } + + function getTypedWithdrawHash( + WithdrawPermit memory permit, + bytes32 typehash, + bytes32 domainSeparator + ) public pure returns (bytes32) { + return + keccak256(abi.encodePacked('\x19\x01', domainSeparator, getWithdrawHash(permit, typehash))); + } + + function getTypedDepositHash( + DepositPermit memory permit, + bytes32 typehash, + bytes32 domainSeparator + ) public pure returns (bytes32) { + return + keccak256(abi.encodePacked('\x19\x01', domainSeparator, getDepositHash(permit, typehash))); + } +} diff --git a/tests/utils/TestnetProcedures.sol b/tests/utils/TestnetProcedures.sol index ab6ee5e3..cbf778e3 100644 --- a/tests/utils/TestnetProcedures.sol +++ b/tests/utils/TestnetProcedures.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.0; import 'forge-std/Test.sol'; import '../../src/deployments/interfaces/IMarketReportTypes.sol'; -import {ConfigEngineDeployer} from './ConfigEngineDeployer.sol'; import {DeployUtils} from '../../src/deployments/contracts/utilities/DeployUtils.sol'; import {FfiUtils} from '../../src/deployments/contracts/utilities/FfiUtils.sol'; import {DefaultMarketInput} from '../../src/deployments/inputs/DefaultMarketInput.sol'; @@ -111,9 +110,14 @@ contract TestnetProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput { MarketReport memory deployedContracts ) = _getMarketInput(poolAdmin); roleList = roles; - flags.l2 = l2; + // Etch the create2 factory + vm.etch( + 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7, + hex'7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3' + ); + (report, tokenList) = deployAaveV3TestnetAssets( poolAdmin, roles, @@ -218,10 +222,8 @@ contract TestnetProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput { MarketReport memory r = deployAaveV3Testnet(deployer, roles, config, flags, deployedContracts); - address engine = ConfigEngineDeployer.deployEngine(vm, r); - AaveV3TestListing testnetListingPayload = new AaveV3TestListing( - IAaveV3ConfigEngine(engine), + IAaveV3ConfigEngine(r.configEngine), roles.poolAdmin, assetsList.weth, r @@ -298,7 +300,7 @@ contract TestnetProcedures is Test, DeployUtils, FfiUtils, DefaultMarketInput { t.variableDebtSymbol = _concatStr('varDebtMISC ', x); t.stableDebtName = _concatStr('Stable Debt Misc ', x); t.stableDebtSymbol = _concatStr('stableDebtMISC ', x); - t.rateStrategy = r.defaultInterestRateStrategyV2; + t.rateStrategy = r.defaultInterestRateStrategy; t.interestRateData = abi.encode( IDefaultInterestRateStrategyV2.InterestRateData({ optimalUsageRatio: 80_00,