diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 5a866dba..a36bfe2f 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -386,6 +386,7 @@ abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapt } uint256 bestAmountOut; + try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, simplePath) returns ( uint256[] memory resultAmounts ) { diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol new file mode 100644 index 00000000..d488ee7b --- /dev/null +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {Helpers} from '../protocol/libraries/helpers/Helpers.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IAToken} from '../interfaces/IAToken.sol'; +import {ReserveConfiguration} from '../protocol/libraries/configuration/ReserveConfiguration.sol'; + +/** + * @title UniswapLiquiditySwapAdapter + * @notice Uniswap V2 Adapter to swap liquidity. + * @author Aave + **/ +contract FlashLiquidationAdapter is BaseUniswapAdapter { + using ReserveConfiguration for DataTypes.ReserveConfigurationMap; + uint256 internal constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 5000; + + struct LiquidationParams { + address collateralAsset; + address borrowedAsset; + address user; + uint256 debtToCover; + bool useEthPath; + } + + struct LiquidationCallLocalVars { + uint256 initFlashBorrowedBalance; + uint256 diffFlashBorrowedBalance; + uint256 initCollateralBalance; + uint256 diffCollateralBalance; + uint256 flashLoanDebt; + uint256 soldAmount; + uint256 remainingTokens; + uint256 borrowedAssetLeftovers; + } + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter, + address wethAddress + ) public BaseUniswapAdapter(addressesProvider, uniswapRouter, wethAddress) {} + + /** + * @dev Liquidate a non-healthy position collateral-wise, with a Health Factor below 1, using Flash Loan and Uniswap to repay flash loan premium. + * - The caller (liquidator) with a flash loan covers `debtToCover` amount of debt of the user getting liquidated, and receives + * a proportionally amount of the `collateralAsset` plus a bonus to cover market risk minus the flash loan premium. + * @param assets Address of asset to be swapped + * @param amounts Amount of the asset to be swapped + * @param premiums Fee of the flash loan + * @param initiator Address of the caller + * @param params Additional variadic field to include extra params. Expected parameters: + * address collateralAsset The collateral asset to release and will be exchanged to pay the flash loan premium + * address borrowedAsset The asset that must be covered + * address user The user address with a Health Factor below 1 + * uint256 debtToCover The amount of debt to cover + * bool useEthPath Use WETH as connector path between the collateralAsset and borrowedAsset at Uniswap + */ + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override returns (bool) { + require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); + + LiquidationParams memory decodedParams = _decodeParams(params); + + require(assets.length == 1 && assets[0] == decodedParams.borrowedAsset, 'INCONSISTENT_PARAMS'); + + _liquidateAndSwap( + decodedParams.collateralAsset, + decodedParams.borrowedAsset, + decodedParams.user, + decodedParams.debtToCover, + decodedParams.useEthPath, + amounts[0], + premiums[0], + initiator + ); + + return true; + } + + /** + * @dev + * @param collateralAsset The collateral asset to release and will be exchanged to pay the flash loan premium + * @param borrowedAsset The asset that must be covered + * @param user The user address with a Health Factor below 1 + * @param debtToCover The amount of debt to coverage, can be max(-1) to liquidate all possible debt + * @param useEthPath true if the swap needs to occur using ETH in the routing, false otherwise + * @param flashBorrowedAmount Amount of asset requested at the flash loan to liquidate the user position + * @param premium Fee of the requested flash loan + * @param initiator Address of the caller + */ + function _liquidateAndSwap( + address collateralAsset, + address borrowedAsset, + address user, + uint256 debtToCover, + bool useEthPath, + uint256 flashBorrowedAmount, + uint256 premium, + address initiator + ) internal { + LiquidationCallLocalVars memory vars; + vars.initCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)); + if (collateralAsset != borrowedAsset) { + vars.initFlashBorrowedBalance = IERC20(borrowedAsset).balanceOf(address(this)); + + // Track leftover balance to rescue funds in case of external transfers into this contract + vars.borrowedAssetLeftovers = vars.initFlashBorrowedBalance.sub(flashBorrowedAmount); + } + vars.flashLoanDebt = flashBorrowedAmount.add(premium); + + // Approve LendingPool to use debt token for liquidation + IERC20(borrowedAsset).approve(address(LENDING_POOL), debtToCover); + + // Liquidate the user position and release the underlying collateral + LENDING_POOL.liquidationCall(collateralAsset, borrowedAsset, user, debtToCover, false); + + // Discover the liquidated tokens + uint256 collateralBalanceAfter = IERC20(collateralAsset).balanceOf(address(this)); + + // Track only collateral released, not current asset balance of the contract + vars.diffCollateralBalance = collateralBalanceAfter.sub(vars.initCollateralBalance); + + if (collateralAsset != borrowedAsset) { + // Discover flash loan balance after the liquidation + uint256 flashBorrowedAssetAfter = IERC20(borrowedAsset).balanceOf(address(this)); + + // Use only flash loan borrowed assets, not current asset balance of the contract + vars.diffFlashBorrowedBalance = flashBorrowedAssetAfter.sub(vars.borrowedAssetLeftovers); + + // Swap released collateral into the debt asset, to repay the flash loan + vars.soldAmount = _swapTokensForExactTokens( + collateralAsset, + borrowedAsset, + vars.diffCollateralBalance, + vars.flashLoanDebt.sub(vars.diffFlashBorrowedBalance), + useEthPath + ); + vars.remainingTokens = vars.diffCollateralBalance.sub(vars.soldAmount); + } else { + vars.remainingTokens = vars.diffCollateralBalance.sub(premium); + } + + // Allow repay of flash loan + IERC20(borrowedAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); + + // Transfer remaining tokens to initiator + if (vars.remainingTokens > 0) { + IERC20(collateralAsset).transfer(initiator, vars.remainingTokens); + } + } + + /** + * @dev Decodes the information encoded in the flash loan params + * @param params Additional variadic field to include extra params. Expected parameters: + * address collateralAsset The collateral asset to claim + * address borrowedAsset The asset that must be covered and will be exchanged to pay the flash loan premium + * address user The user address with a Health Factor below 1 + * uint256 debtToCover The amount of debt to cover + * bool useEthPath Use WETH as connector path between the collateralAsset and borrowedAsset at Uniswap + * @return LiquidationParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal pure returns (LiquidationParams memory) { + ( + address collateralAsset, + address borrowedAsset, + address user, + uint256 debtToCover, + bool useEthPath + ) = abi.decode(params, (address, address, address, uint256, bool)); + + return LiquidationParams(collateralAsset, borrowedAsset, user, debtToCover, useEthPath); + } +} diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 5d06494d..b1c95337 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -234,6 +234,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter { * uint8 v V param for the permit signature * bytes32 r R param for the permit signature * bytes32 s S param for the permit signature + * bool useEthPath use WETH path route * @return RepayParams struct containing decoded params */ function _decodeParams(bytes memory params) internal pure returns (RepayParams memory) { diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index 345c910a..40c7154e 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -442,6 +442,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage receiveAToken ) ); + require(success, Errors.LP_LIQUIDATION_CALL_FAILED); (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 4d1f0617..fb284b35 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -49,6 +49,7 @@ import { WalletBalanceProviderFactory, WETH9MockedFactory, WETHGatewayFactory, + FlashLiquidationAdapterFactory, } from '../types'; import { withSaveAndVerify, @@ -525,3 +526,14 @@ export const deployUniswapRepayAdapter = async ( args, verify ); + +export const deployFlashLiquidationAdapter = async ( + args: [tEthereumAddress, tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new FlashLiquidationAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.FlashLiquidationAdapter, + args, + verify + ); diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 04ac662a..6504cceb 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -29,6 +29,7 @@ import { WalletBalanceProviderFactory, WETH9MockedFactory, WETHGatewayFactory, + FlashLiquidationAdapterFactory, } from '../types'; import { IERC20DetailedFactory } from '../types/IERC20DetailedFactory'; import { MockTokenMap } from './contracts-helpers'; @@ -354,3 +355,11 @@ export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => (await getDb().get(`${eContractid.UniswapRepayAdapter}.${DRE.network.name}`).value()).address, await getFirstSigner() ); + +export const getFlashLiquidationAdapter = async (address?: tEthereumAddress) => + await FlashLiquidationAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.FlashLiquidationAdapter}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index e18de87d..6fce99b6 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -17,6 +17,7 @@ import { Artifact } from 'hardhat/types'; import { Artifact as BuidlerArtifact } from '@nomiclabs/buidler/types'; import { verifyContract } from './etherscan-verification'; import { getIErc20Detailed } from './contracts-getters'; +import { usingTenderly } from './tenderly-utils'; export type MockTokenMap = { [symbol: string]: MintableERC20 }; @@ -90,7 +91,8 @@ export const withSaveAndVerify = async ( ): Promise => { await waitForTx(instance.deployTransaction); await registerContractInJsonDb(id, instance); - if (DRE.network.name.includes('tenderly')) { + if (usingTenderly()) { + console.log('doing verify of', id); await (DRE as any).tenderlyRPC.verify({ name: id, address: instance.address, @@ -286,3 +288,16 @@ export const buildRepayAdapterParams = ( [collateralAsset, collateralAmount, rateMode, permitAmount, deadline, v, r, s, useEthPath] ); }; + +export const buildFlashLiquidationAdapterParams = ( + collateralAsset: tEthereumAddress, + debtAsset: tEthereumAddress, + user: tEthereumAddress, + debtToCover: BigNumberish, + useEthPath: boolean +) => { + return ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'address', 'uint256', 'bool'], + [collateralAsset, debtAsset, user, debtToCover, useEthPath] + ); +}; diff --git a/helpers/misc-utils.ts b/helpers/misc-utils.ts index 6d7bb185..56be2cdf 100644 --- a/helpers/misc-utils.ts +++ b/helpers/misc-utils.ts @@ -17,9 +17,8 @@ export const stringToBigNumber = (amount: string): BigNumber => new BigNumber(am export const getDb = () => low(new FileSync('./deployed-contracts.json')); -export let DRE: - | HardhatRuntimeEnvironment - | BuidlerRuntimeEnvironment = {} as HardhatRuntimeEnvironment; +export let DRE: HardhatRuntimeEnvironment | BuidlerRuntimeEnvironment; + export const setDRE = (_DRE: HardhatRuntimeEnvironment | BuidlerRuntimeEnvironment) => { DRE = _DRE; }; diff --git a/helpers/tenderly-utils.ts b/helpers/tenderly-utils.ts new file mode 100644 index 00000000..27445608 --- /dev/null +++ b/helpers/tenderly-utils.ts @@ -0,0 +1,7 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DRE } from './misc-utils'; + +export const usingTenderly = () => + DRE && + ((DRE as HardhatRuntimeEnvironment).network.name.includes('tenderly') || + process.env.TENDERLY === 'true'); diff --git a/helpers/types.ts b/helpers/types.ts index 7b2e6662..bc1e31e6 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -70,6 +70,7 @@ export enum eContractid { MockUniswapV2Router02 = 'MockUniswapV2Router02', UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter', UniswapRepayAdapter = 'UniswapRepayAdapter', + FlashLiquidationAdapter = 'FlashLiquidationAdapter', } /* diff --git a/package-lock.json b/package-lock.json index dfdeb1a5..3ae83800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "protocol-v2", - "version": "1.0.0", + "name": "@aave/protocol-v2", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a2f504fc..d8db5d38 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "compile": "SKIP_LOAD=true hardhat compile", "console:fork": "MAINNET_FORK=true hardhat console", "test": "TS_NODE_TRANSPILE_ONLY=1 hardhat test ./test/*.spec.ts", - "test-scenarios": "npm run test -- test/__setup.spec.ts test/scenario.spec.ts", + "test-scenarios": "npx hardhat test test/__setup.spec.ts test/scenario.spec.ts", "test-repay-with-collateral": "hardhat test test/__setup.spec.ts test/repay-with-collateral.spec.ts", "test-liquidate-with-collateral": "hardhat test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", "test-liquidate-underlying": "hardhat test test/__setup.spec.ts test/liquidation-underlying.spec.ts", diff --git a/tasks/full/2_lending_pool.ts b/tasks/full/2_lending_pool.ts index f9898ec5..db0daabb 100644 --- a/tasks/full/2_lending_pool.ts +++ b/tasks/full/2_lending_pool.ts @@ -1,8 +1,5 @@ import { task } from 'hardhat/config'; -import { - getEthersSignersAddresses, - insertContractAddressInDb, -} from '../../helpers/contracts-helpers'; +import { insertContractAddressInDb } from '../../helpers/contracts-helpers'; import { deployATokensAndRatesHelper, deployLendingPool, @@ -16,10 +13,11 @@ import { getLendingPool, getLendingPoolConfiguratorProxy, } from '../../helpers/contracts-getters'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; task('full:deploy-lending-pool', 'Deploy lending pool for dev enviroment') .addFlag('verify', 'Verify contracts at Etherscan') - .setAction(async ({ verify }, DRE) => { + .setAction(async ({ verify }, DRE: HardhatRuntimeEnvironment) => { try { await DRE.run('set-DRE'); diff --git a/tasks/migrations/aave.mainnet.ts b/tasks/migrations/aave.mainnet.ts index 76c32578..96bcbbb8 100644 --- a/tasks/migrations/aave.mainnet.ts +++ b/tasks/migrations/aave.mainnet.ts @@ -1,15 +1,13 @@ import { task } from 'hardhat/config'; -import { ExternalProvider } from '@ethersproject/providers'; import { checkVerification } from '../../helpers/etherscan-verification'; import { ConfigNames } from '../../helpers/configuration'; -import { EthereumNetworkNames } from '../../helpers/types'; import { printContracts } from '../../helpers/misc-utils'; +import { usingTenderly } from '../../helpers/tenderly-utils'; task('aave:mainnet', 'Deploy development enviroment') .addFlag('verify', 'Verify contracts at Etherscan') .setAction(async ({ verify }, DRE) => { const POOL_NAME = ConfigNames.Aave; - const network = DRE.network.name; await DRE.run('set-DRE'); // Prevent loss of gas verifying all the needed ENVs for Etherscan verification @@ -17,13 +15,6 @@ task('aave:mainnet', 'Deploy development enviroment') checkVerification(); } - if (network.includes('tenderly')) { - console.log('- Setting up Tenderly provider'); - await DRE.tenderlyRPC.initializeFork(); - const provider = new DRE.ethers.providers.Web3Provider(DRE.tenderlyRPC as any); - DRE.ethers.provider = provider; - } - console.log('Migration started\n'); console.log('1. Deploy address provider'); @@ -50,7 +41,7 @@ task('aave:mainnet', 'Deploy development enviroment') await DRE.run('verify:tokens', { pool: POOL_NAME }); } - if (network.includes('tenderly')) { + if (usingTenderly()) { const postDeployHead = DRE.tenderlyRPC.getHead(); console.log('Tenderly UUID', postDeployHead); } diff --git a/tasks/misc/set-bre.ts b/tasks/misc/set-bre.ts index 612225f9..e2aeba12 100644 --- a/tasks/misc/set-bre.ts +++ b/tasks/misc/set-bre.ts @@ -1,8 +1,25 @@ import { task } from 'hardhat/config'; -import { setDRE } from '../../helpers/misc-utils'; +import { DRE, setDRE } from '../../helpers/misc-utils'; +import { EthereumNetworkNames } from '../../helpers/types'; +import { usingTenderly } from '../../helpers/tenderly-utils'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; task(`set-DRE`, `Inits the DRE, to have access to all the plugins' objects`).setAction( async (_, _DRE) => { + if (DRE) { + return; + } + if ( + (_DRE as HardhatRuntimeEnvironment).network.name.includes('tenderly') || + process.env.TENDERLY === 'true' + ) { + console.log('- Setting up Tenderly provider'); + await _DRE.tenderlyRPC.initializeFork(); + console.log('- Initialized Tenderly fork'); + const provider = new _DRE.ethers.providers.Web3Provider(_DRE.tenderlyRPC as any); + _DRE.ethers.provider = provider; + } + setDRE(_DRE); return _DRE; } diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index de3478af..37ff9cfc 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -25,6 +25,7 @@ import { deployMockUniswapRouter, deployUniswapLiquiditySwapAdapter, deployUniswapRepayAdapter, + deployFlashLiquidationAdapter, } from '../helpers/contracts-deployments'; import { Signer } from 'ethers'; import { TokenContractId, eContractid, tEthereumAddress, AavePools } from '../helpers/types'; @@ -232,29 +233,19 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await waitForTx( await addressesProvider.setLendingPoolCollateralManager(collateralManager.address) ); - - const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); - await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); + await deployMockFlashLoanReceiver(addressesProvider.address); const mockUniswapRouter = await deployMockUniswapRouter(); - await insertContractAddressInDb(eContractid.MockUniswapV2Router02, mockUniswapRouter.address); - const UniswapLiquiditySwapAdapter = await deployUniswapLiquiditySwapAdapter([ + const adapterParams: [string, string, string] = [ addressesProvider.address, mockUniswapRouter.address, mockTokens.WETH.address, - ]); - await insertContractAddressInDb( - eContractid.UniswapLiquiditySwapAdapter, - UniswapLiquiditySwapAdapter.address - ); + ]; - const UniswapRepayAdapter = await deployUniswapRepayAdapter([ - addressesProvider.address, - mockUniswapRouter.address, - mockTokens.WETH.address, - ]); - await insertContractAddressInDb(eContractid.UniswapRepayAdapter, UniswapRepayAdapter.address); + await deployUniswapLiquiditySwapAdapter(adapterParams); + await deployUniswapRepayAdapter(adapterParams); + await deployFlashLiquidationAdapter(adapterParams); await deployWalletBalancerProvider(); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 5395c903..4a75e54d 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -13,6 +13,7 @@ import { getWETHGateway, getUniswapLiquiditySwapAdapter, getUniswapRepayAdapter, + getFlashLiquidationAdapter, } from '../../helpers/contracts-getters'; import { eEthereumNetwork, tEthereumAddress } from '../../helpers/types'; import { LendingPool } from '../../types/LendingPool'; @@ -36,6 +37,9 @@ import { WETH9Mocked } from '../../types/WETH9Mocked'; import { WETHGateway } from '../../types/WETHGateway'; import { solidity } from 'ethereum-waffle'; import { AaveConfig } from '../../markets/aave'; +import { FlashLiquidationAdapter } from '../../types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { usingTenderly } from '../../helpers/tenderly-utils'; chai.use(bignumberChai()); chai.use(almostEqual()); @@ -63,13 +67,12 @@ export interface TestEnv { uniswapRepayAdapter: UniswapRepayAdapter; registry: LendingPoolAddressesProviderRegistry; wethGateway: WETHGateway; + flashLiquidationAdapter: FlashLiquidationAdapter; } let buidlerevmSnapshotId: string = '0x1'; const setBuidlerevmSnapshotId = (id: string) => { - if (DRE.network.name === 'hardhat') { - buidlerevmSnapshotId = id; - } + buidlerevmSnapshotId = id; }; const testEnv: TestEnv = { @@ -88,6 +91,7 @@ const testEnv: TestEnv = { addressesProvider: {} as LendingPoolAddressesProvider, uniswapLiquiditySwapAdapter: {} as UniswapLiquiditySwapAdapter, uniswapRepayAdapter: {} as UniswapRepayAdapter, + flashLiquidationAdapter: {} as FlashLiquidationAdapter, registry: {} as LendingPoolAddressesProviderRegistry, wethGateway: {} as WETHGateway, } as TestEnv; @@ -153,16 +157,35 @@ export async function initializeMakeSuite() { testEnv.uniswapLiquiditySwapAdapter = await getUniswapLiquiditySwapAdapter(); testEnv.uniswapRepayAdapter = await getUniswapRepayAdapter(); + testEnv.flashLiquidationAdapter = await getFlashLiquidationAdapter(); } +const setSnapshot = async () => { + const hre = DRE as HardhatRuntimeEnvironment; + if (usingTenderly()) { + setBuidlerevmSnapshotId((await hre.tenderlyRPC.getHead()) || '0x1'); + return; + } + setBuidlerevmSnapshotId(await evmSnapshot()); +}; + +const revertHead = async () => { + const hre = DRE as HardhatRuntimeEnvironment; + if (usingTenderly()) { + await hre.tenderlyRPC.setHead(buidlerevmSnapshotId); + return; + } + await evmRevert(buidlerevmSnapshotId); +}; + export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { describe(name, () => { before(async () => { - setBuidlerevmSnapshotId(await evmSnapshot()); + await setSnapshot(); }); tests(testEnv); after(async () => { - await evmRevert(buidlerevmSnapshotId); + await revertHead(); }); }); } diff --git a/test/helpers/scenarios/borrow-repay-stable.json b/test/helpers/scenarios/borrow-repay-stable.json index 87e7de0b..3f472387 100644 --- a/test/helpers/scenarios/borrow-repay-stable.json +++ b/test/helpers/scenarios/borrow-repay-stable.json @@ -208,6 +208,15 @@ }, "expected": "revert", "revertMessage": "The collateral balance is 0" + }, + { + "name": "withdraw", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "1" + }, + "expected": "success" } ] }, diff --git a/test/helpers/scenarios/credit-delegation.json b/test/helpers/scenarios/credit-delegation.json index a0aecf1b..0d15d7f1 100644 --- a/test/helpers/scenarios/credit-delegation.json +++ b/test/helpers/scenarios/credit-delegation.json @@ -10,7 +10,7 @@ "args": { "reserve": "WETH", "amount": "1000", - "user": "0" + "user": "3" }, "expected": "success" }, @@ -18,7 +18,7 @@ "name": "approve", "args": { "reserve": "WETH", - "user": "0" + "user": "3" }, "expected": "success" }, @@ -27,6 +27,32 @@ "args": { "reserve": "WETH", "amount": "1000", + "user": "3" + }, + "expected": "success" + }, + { + "name": "mint", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "approve", + "args": { + "reserve": "DAI", + "user": "0" + }, + "expected": "success" + }, + { + "name": "deposit", + "args": { + "reserve": "DAI", + "amount": "1000", "user": "0" }, "expected": "success" diff --git a/test/helpers/scenarios/rebalance-stable-rate.json b/test/helpers/scenarios/rebalance-stable-rate.json index 70ea820a..8c7e6c19 100644 --- a/test/helpers/scenarios/rebalance-stable-rate.json +++ b/test/helpers/scenarios/rebalance-stable-rate.json @@ -8,7 +8,7 @@ { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1", "borrowRateMode": "variable" @@ -19,12 +19,12 @@ ] }, { - "description": "User 0 deposits 1000 DAI, user 1 deposits 5 ETH, borrows 600 DAI at a variable rate, user 0 rebalances user 1 (revert expected)", + "description": "User 0 deposits 1000 USDC, user 1 deposits 5 ETH, borrows 600 DAI at a variable rate, user 0 rebalances user 1 (revert expected)", "actions": [ { "name": "mint", "args": { - "reserve": "DAI", + "reserve": "USDC", "amount": "1000", "user": "0" }, @@ -33,7 +33,7 @@ { "name": "approve", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0" }, "expected": "success" @@ -41,7 +41,7 @@ { "name": "deposit", "args": { - "reserve": "DAI", + "reserve": "USDC", "amount": "1000", "user": "0" }, @@ -51,7 +51,7 @@ "name": "mint", "args": { "reserve": "WETH", - "amount": "5", + "amount": "7", "user": "1" }, "expected": "success" @@ -69,7 +69,7 @@ "args": { "reserve": "WETH", - "amount": "5", + "amount": "7", "user": "1" }, "expected": "success" @@ -77,18 +77,17 @@ { "name": "borrow", "args": { - "reserve": "DAI", + "reserve": "USDC", "amount": "250", "borrowRateMode": "stable", - "user": "1", - "timeTravel": "365" + "user": "1" }, "expected": "success" }, { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1" }, @@ -103,18 +102,17 @@ { "name": "borrow", "args": { - "reserve": "DAI", + "reserve": "USDC", "amount": "200", - "borrowRateMode": "stable", - "user": "1", - "timeTravel": "365" + "borrowRateMode": "variable", + "user": "1" }, "expected": "success" }, { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1" }, @@ -129,18 +127,17 @@ { "name": "borrow", "args": { - "reserve": "DAI", + "reserve": "USDC", "amount": "200", - "borrowRateMode": "stable", - "user": "1", - "timeTravel": "365" + "borrowRateMode": "variable", + "user": "1" }, "expected": "success" }, { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1" }, @@ -155,18 +152,17 @@ { "name": "borrow", "args": { - "reserve": "DAI", - "amount": "100", - "borrowRateMode": "stable", - "user": "1", - "timeTravel": "365" + "reserve": "USDC", + "amount": "280", + "borrowRateMode": "variable", + "user": "1" }, "expected": "success" }, { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1" }, @@ -175,75 +171,24 @@ } ] }, - { - "description": "User 2 deposits ETH and borrows the remaining DAI, causing the stable rates to rise (usage ratio = 94%). User 0 tries to rebalance user 1 (revert expected)", - "actions": [ - { - "name": "mint", - "args": { - "reserve": "WETH", - "amount": "5", - "user": "2" - }, - "expected": "success" - }, - { - "name": "approve", - "args": { - "reserve": "WETH", - "user": "2" - }, - "expected": "success" - }, - { - "name": "deposit", - "args": { - "reserve": "WETH", - "amount": "5", - "user": "2" - }, - "expected": "success" - }, - { - "name": "borrow", - "args": { - "reserve": "DAI", - "amount": "190", - "borrowRateMode": "variable", - "user": "2" - }, - "expected": "success" - }, - { - "name": "rebalanceStableBorrowRate", - "args": { - "reserve": "DAI", - "user": "0", - "target": "1" - }, - "expected": "revert", - "revertMessage": "Interest rate rebalance conditions were not met" - } - ] - }, { - "description": "User 2 borrows the remaining DAI (usage ratio = 100%). User 0 rebalances user 1", + "description": "User 0 borrows the remaining USDC (usage ratio = 100%). User 0 rebalances user 1", "actions": [ { "name": "borrow", "args": { - "reserve": "DAI", - "amount": "60", + "reserve": "USDC", + "amount": "20", "borrowRateMode": "variable", - "user": "2" + "user": "1" }, "expected": "success" }, { "name": "rebalanceStableBorrowRate", "args": { - "reserve": "DAI", + "reserve": "USDC", "user": "0", "target": "1" }, diff --git a/test/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 5db28ec2..a4db008f 100644 --- a/test/helpers/utils/calculations.ts +++ b/test/helpers/utils/calculations.ts @@ -996,7 +996,7 @@ export const calcExpectedReserveDataAfterStableRateRebalance = ( //removing the stable liquidity at the old rate - const avgRateBefore = calcExpectedAverageStableBorrowRate( + const avgRateBefore = calcExpectedAverageStableBorrowRateRebalance( reserveDataBeforeAction.averageStableBorrowRate, expectedReserveData.totalStableDebt, userStableDebt.negated(), @@ -1004,7 +1004,7 @@ export const calcExpectedReserveDataAfterStableRateRebalance = ( ); // adding it again at the new rate - expectedReserveData.averageStableBorrowRate = calcExpectedAverageStableBorrowRate( + expectedReserveData.averageStableBorrowRate = calcExpectedAverageStableBorrowRateRebalance( avgRateBefore, expectedReserveData.totalStableDebt.minus(userStableDebt), userStableDebt, @@ -1044,6 +1044,8 @@ export const calcExpectedUserDataAfterStableRateRebalance = ( ): UserReserveData => { const expectedUserData = { ...userDataBeforeAction }; + expectedUserData.principalStableDebt = userDataBeforeAction.principalStableDebt; + expectedUserData.principalVariableDebt = calcExpectedVariableDebtTokenBalance( reserveDataBeforeAction, userDataBeforeAction, @@ -1056,12 +1058,18 @@ export const calcExpectedUserDataAfterStableRateRebalance = ( txTimestamp ); + expectedUserData.currentVariableDebt = calcExpectedVariableDebtTokenBalance( + reserveDataBeforeAction, + userDataBeforeAction, + txTimestamp + ); + expectedUserData.stableRateLastUpdated = txTimestamp; expectedUserData.principalVariableDebt = userDataBeforeAction.principalVariableDebt; - expectedUserData.stableBorrowRate = reserveDataBeforeAction.stableBorrowRate; - + // Stable rate after burn + expectedUserData.stableBorrowRate = expectedDataAfterAction.averageStableBorrowRate; expectedUserData.liquidityRate = expectedDataAfterAction.liquidityRate; expectedUserData.currentATokenBalance = calcExpectedATokenBalance( @@ -1104,7 +1112,7 @@ const calcExpectedAverageStableBorrowRate = ( ) => { const weightedTotalBorrows = avgStableRateBefore.multipliedBy(totalStableDebtBefore); const weightedAmountBorrowed = rate.multipliedBy(amountChanged); - const totalBorrowedStable = totalStableDebtBefore.plus(new BigNumber(amountChanged)); + const totalBorrowedStable = totalStableDebtBefore.plus(amountChanged); if (totalBorrowedStable.eq(0)) return new BigNumber('0'); @@ -1114,6 +1122,24 @@ const calcExpectedAverageStableBorrowRate = ( .decimalPlaces(0, BigNumber.ROUND_DOWN); }; +const calcExpectedAverageStableBorrowRateRebalance = ( + avgStableRateBefore: BigNumber, + totalStableDebtBefore: BigNumber, + amountChanged: BigNumber, + rate: BigNumber +) => { + const weightedTotalBorrows = avgStableRateBefore.rayMul(totalStableDebtBefore); + const weightedAmountBorrowed = rate.rayMul(amountChanged.wadToRay()); + const totalBorrowedStable = totalStableDebtBefore.plus(amountChanged.wadToRay()); + + if (totalBorrowedStable.eq(0)) return new BigNumber('0'); + + return weightedTotalBorrows + .plus(weightedAmountBorrowed) + .rayDiv(totalBorrowedStable) + .decimalPlaces(0, BigNumber.ROUND_DOWN); +}; + export const calcExpectedVariableDebtTokenBalance = ( reserveData: ReserveData, userData: UserReserveData, diff --git a/test/mainnet/check-list.spec.ts b/test/mainnet/check-list.spec.ts index d949884b..99020fa2 100644 --- a/test/mainnet/check-list.spec.ts +++ b/test/mainnet/check-list.spec.ts @@ -15,7 +15,7 @@ const UNISWAP_ROUTER = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'; makeSuite('Mainnet Check list', (testEnv: TestEnv) => { const zero = BigNumber.from('0'); const depositSize = parseEther('5'); - + const daiSize = parseEther('10000'); it('Deposit WETH', async () => { const { users, wethGateway, aWETH, pool } = testEnv; @@ -99,7 +99,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Borrow stable WETH and Full Repay with ETH', async () => { - const { users, wethGateway, aWETH, weth, pool, helpersContract } = testEnv; + const { users, wethGateway, aWETH, dai, aDai, weth, pool, helpersContract } = testEnv; const borrowSize = parseEther('1'); const repaySize = borrowSize.add(borrowSize.mul(5).div(100)); const user = users[1]; @@ -110,13 +110,15 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { const stableDebtToken = await getStableDebtToken(stableDebtTokenAddress); - // Deposit with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); + // Deposit 10000 DAI + await dai.connect(user.signer).mint(daiSize); + await dai.connect(user.signer).approve(pool.address, daiSize); + await pool.connect(user.signer).deposit(dai.address, daiSize, user.address, '0'); - const aTokensBalance = await aWETH.balanceOf(user.address); + const aTokensBalance = await aDai.balanceOf(user.address); expect(aTokensBalance).to.be.gt(zero); - expect(aTokensBalance).to.be.gte(depositSize); + expect(aTokensBalance).to.be.gte(daiSize); // Borrow WETH with WETH as collateral await waitForTx( diff --git a/test/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts new file mode 100644 index 00000000..063c6930 --- /dev/null +++ b/test/uniswapAdapters.flashLiquidation.spec.ts @@ -0,0 +1,850 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + buildFlashLiquidationAdapterParams, +} from '../helpers/contracts-helpers'; +import { getMockUniswapRouter } from '../helpers/contracts-getters'; +import { deployFlashLiquidationAdapter } from '../helpers/contracts-deployments'; +import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; +import BigNumber from 'bignumber.js'; +import { DRE, evmRevert, evmSnapshot, increaseTime, waitForTx } from '../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { ProtocolErrors, RateMode } from '../helpers/types'; +import { APPROVAL_AMOUNT_LENDING_POOL, MAX_UINT_AMOUNT, oneEther } from '../helpers/constants'; +import { getUserData } from './helpers/utils/helpers'; +import { calcExpectedStableDebtTokenBalance } from './helpers/utils/calculations'; +const { expect } = require('chai'); + +makeSuite('Uniswap adapters', (testEnv: TestEnv) => { + let mockUniswapRouter: MockUniswapV2Router02; + let evmSnapshotId: string; + const { INVALID_HF, LP_LIQUIDATION_CALL_FAILED } = ProtocolErrors; + + before(async () => { + mockUniswapRouter = await getMockUniswapRouter(); + }); + + const depositAndHFBelowOne = async () => { + const { dai, weth, users, pool, oracle } = testEnv; + const depositor = users[0]; + const borrower = users[1]; + + //mints DAI to depositor + await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); + + //approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + //user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); + //user 2 deposits 1 ETH + const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); + + //mints WETH to borrower + await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); + + //approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); + + //user 2 borrows + + const userGlobalDataBefore = await pool.getUserAccountData(borrower.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const amountDAIToBorrow = await convertToCurrencyDecimals( + dai.address, + new BigNumber(userGlobalDataBefore.availableBorrowsETH.toString()) + .div(daiPrice.toString()) + .multipliedBy(0.95) + .toFixed(0) + ); + + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address); + + const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); + + expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.equal( + '8250', + INVALID_HF + ); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.18).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(borrower.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( + oneEther.toFixed(0), + INVALID_HF + ); + }; + + const depositSameAssetAndHFBelowOne = async () => { + const { dai, weth, users, pool, oracle } = testEnv; + const depositor = users[0]; + const borrower = users[1]; + + //mints DAI to depositor + await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); + + //approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + //user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); + //user 2 deposits 1 ETH + const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); + + //mints WETH to borrower + await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); + + //approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); + + //user 2 borrows + + const userGlobalDataBefore = await pool.getUserAccountData(borrower.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const amountDAIToBorrow = await convertToCurrencyDecimals( + dai.address, + new BigNumber(userGlobalDataBefore.availableBorrowsETH.toString()) + .div(daiPrice.toString()) + .multipliedBy(0.8) + .toFixed(0) + ); + await waitForTx( + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address) + ); + + const userGlobalDataBefore2 = await pool.getUserAccountData(borrower.address); + + const amountWETHToBorrow = new BigNumber(userGlobalDataBefore2.availableBorrowsETH.toString()) + .multipliedBy(0.8) + .toFixed(0); + + await pool + .connect(borrower.signer) + .borrow(weth.address, amountWETHToBorrow, RateMode.Variable, '0', borrower.address); + + const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); + + expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.equal( + '8250', + INVALID_HF + ); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.18).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(borrower.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( + oneEther.toFixed(0), + INVALID_HF + ); + }; + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + + describe('Flash Liquidation Adapter', () => { + before('Before LendingPool liquidation: set config', () => { + BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); + }); + + after('After LendingPool liquidation: reset config', () => { + BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); + }); + + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const { addressesProvider, weth } = testEnv; + await deployFlashLiquidationAdapter([ + addressesProvider.address, + mockUniswapRouter.address, + weth.address, + ]); + }); + + it('should revert if not valid addresses provider', async () => { + const { weth } = testEnv; + expect( + deployFlashLiquidationAdapter([ + mockUniswapRouter.address, + mockUniswapRouter.address, + weth.address, + ]) + ).to.be.reverted; + }); + }); + + describe('executeOperation: succesfully liquidateCall and swap via Flash Loan with profits', () => { + it('Liquidates the borrow with profit', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); + + const { + dai, + weth, + users, + pool, + oracle, + helpersContract, + flashLiquidationAdapter, + } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const collateralDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await helpersContract.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(collateralDecimals)) + .div( + new BigNumber(collateralPrice.toString()).times( + new BigNumber(10).pow(principalDecimals) + ) + ) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + const expectedProfit = ethers.BigNumber.from(expectedCollateralLiquidated.toString()).sub( + expectedSwap + ); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ); + + // Expect Swapped event + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); + + const userReserveDataAfter = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + const liquidatorWethBalanceAfter = await weth.balanceOf(liquidator.address); + + const daiReserveDataAfter = await helpersContract.getReserveData(dai.address); + const ethReserveDataAfter = await helpersContract.getReserveData(weth.address); + + if (!tx.blockNumber) { + expect(false, 'Invalid block number'); + return; + } + const txTimestamp = new BigNumber( + (await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp + ); + + const stableDebtBeforeTx = calcExpectedStableDebtTokenBalance( + userReserveDataBefore.principalStableDebt, + userReserveDataBefore.stableBorrowRate, + userReserveDataBefore.stableRateLastUpdated, + txTimestamp + ); + + const collateralAssetContractBalance = await weth.balanceOf( + flashLiquidationAdapter.address + ); + const borrowAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + + expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( + stableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), + 'Invalid user debt after liquidation' + ); + + //the liquidity index of the principal reserve needs to be bigger than the index before + expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( + daiReserveDataBefore.liquidityIndex.toString(), + 'Invalid liquidity index' + ); + + //the principal APY after a liquidation needs to be lower than the APY before + expect(daiReserveDataAfter.liquidityRate.toString()).to.be.bignumber.lt( + daiReserveDataBefore.liquidityRate.toString(), + 'Invalid liquidity APY' + ); + + expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( + new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) + .plus(flashLoanDebt) + .toFixed(0), + 'Invalid principal available liquidity' + ); + + expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( + new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) + .minus(expectedCollateralLiquidated) + .toFixed(0), + 'Invalid collateral available liquidity' + ); + + // Profit after flash loan liquidation + expect(liquidatorWethBalanceAfter).to.be.equal( + liquidatorWethBalanceBefore.add(expectedProfit), + 'Invalid expected WETH profit' + ); + }); + }); + + describe('executeOperation: succesfully liquidateCall with same asset via Flash Loan, but no swap needed', () => { + it('Liquidates the borrow with profit', async () => { + await depositSameAssetAndHFBelowOne(); + await increaseTime(100); + + const { weth, users, pool, oracle, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const assetPrice = await oracle.getAssetPrice(weth.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + weth.address, + borrower.address + ); + + const assetDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentVariableDebt.div(2).toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(assetPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(assetDecimals)) + .div(new BigNumber(assetPrice.toString()).times(new BigNumber(10).pow(assetDecimals))) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + weth.address, + borrower.address, + amountToLiquidate, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [weth.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ); + + // Dont expect Swapped event due is same asset + await expect(Promise.resolve(tx)).to.not.emit(flashLiquidationAdapter, 'Swapped'); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)) + .to.emit(pool, 'LiquidationCall') + .withArgs( + weth.address, + weth.address, + borrower.address, + amountToLiquidate.toString(), + expectedCollateralLiquidated.toString(), + flashLiquidationAdapter.address, + false + ); + + const borrowAssetContractBalance = await weth.balanceOf(flashLiquidationAdapter.address); + + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + }); + }); + + describe('executeOperation: succesfully liquidateCall and swap via Flash Loan without profits', () => { + it('Liquidates the borrow', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); + + const { + dai, + weth, + users, + pool, + oracle, + helpersContract, + flashLiquidationAdapter, + } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const collateralDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await helpersContract.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(collateralDecimals)) + .div( + new BigNumber(collateralPrice.toString()).times( + new BigNumber(10).pow(principalDecimals) + ) + ) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await ( + await mockUniswapRouter.setAmountToSwap( + weth.address, + expectedCollateralLiquidated.toString() + ) + ).wait(); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [flashLoanDebt], + [0], + borrower.address, + params, + 0 + ); + + // Expect Swapped event + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); + + const userReserveDataAfter = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + const liquidatorWethBalanceAfter = await weth.balanceOf(liquidator.address); + + const daiReserveDataAfter = await helpersContract.getReserveData(dai.address); + const ethReserveDataAfter = await helpersContract.getReserveData(weth.address); + + if (!tx.blockNumber) { + expect(false, 'Invalid block number'); + return; + } + const txTimestamp = new BigNumber( + (await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp + ); + + const stableDebtBeforeTx = calcExpectedStableDebtTokenBalance( + userReserveDataBefore.principalStableDebt, + userReserveDataBefore.stableBorrowRate, + userReserveDataBefore.stableRateLastUpdated, + txTimestamp + ); + + const collateralAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + const borrowAssetContractBalance = await weth.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( + stableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), + 'Invalid user debt after liquidation' + ); + + //the liquidity index of the principal reserve needs to be bigger than the index before + expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( + daiReserveDataBefore.liquidityIndex.toString(), + 'Invalid liquidity index' + ); + + //the principal APY after a liquidation needs to be lower than the APY before + expect(daiReserveDataAfter.liquidityRate.toString()).to.be.bignumber.lt( + daiReserveDataBefore.liquidityRate.toString(), + 'Invalid liquidity APY' + ); + + expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( + new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) + .minus(expectedCollateralLiquidated) + .toFixed(0), + 'Invalid collateral available liquidity' + ); + + // Net Profit == 0 after flash loan liquidation + expect(liquidatorWethBalanceAfter).to.be.equal( + liquidatorWethBalanceBefore, + 'Invalid expected WETH profit' + ); + }); + }); + + describe('executeOperation: succesfully liquidateCall all available debt and swap via Flash Loan ', () => { + it('Liquidates the borrow', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); + + const { + dai, + weth, + users, + pool, + oracle, + helpersContract, + flashLiquidationAdapter, + } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const collateralDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await helpersContract.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + const extraAmount = new BigNumber(amountToLiquidate).times('1.15').toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(collateralDecimals)) + .div( + new BigNumber(collateralPrice.toString()).times( + new BigNumber(10).pow(principalDecimals) + ) + ) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await ( + await mockUniswapRouter.setAmountToSwap( + weth.address, + expectedCollateralLiquidated.toString() + ) + ).wait(); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + MAX_UINT_AMOUNT, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [extraAmount], + [0], + borrower.address, + params, + 0 + ); + + // Expect Swapped event + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); + + const collateralAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + const borrowAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + }); + }); + + describe('executeOperation: invalid params', async () => { + it('Revert if debt asset is different than requested flash loan token', async () => { + await depositAndHFBelowOne(); + + const { dai, weth, users, pool, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + + // Wrong debt asset + const params = buildFlashLiquidationAdapterParams( + weth.address, + weth.address, // intentionally bad + borrower.address, + amountToLiquidate, + false + ); + await expect( + pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + + it('Revert if debt asset amount to liquidate is greater than requested flash loan', async () => { + await depositAndHFBelowOne(); + + const { dai, weth, users, pool, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2); + + // Correct params + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate.toString(), + false + ); + // Bad flash loan params: requested DAI amount below amountToLiquidate + await expect( + pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate.div(2).toString()], + [0], + borrower.address, + params, + 0 + ) + ).to.be.revertedWith(LP_LIQUIDATION_CALL_FAILED); + }); + + it('Revert if requested multiple assets', async () => { + await depositAndHFBelowOne(); + + const { dai, weth, users, pool, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2); + + // Correct params + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate.toString(), + false + ); + // Bad flash loan params: requested multiple assets + await expect( + pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address, weth.address], + [10, 10], + [0], + borrower.address, + params, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + }); + }); +}); diff --git a/test/uniswapAdapters.repay.spec.ts b/test/uniswapAdapters.repay.spec.ts index fbae1d00..c271917e 100644 --- a/test/uniswapAdapters.repay.spec.ts +++ b/test/uniswapAdapters.repay.spec.ts @@ -4,14 +4,10 @@ import { getContract, buildPermitParams, getSignatureFromTypedData, - buildLiquiditySwapParams, buildRepayAdapterParams, } from '../helpers/contracts-helpers'; import { getMockUniswapRouter } from '../helpers/contracts-getters'; -import { - deployUniswapLiquiditySwapAdapter, - deployUniswapRepayAdapter, -} from '../helpers/contracts-deployments'; +import { deployUniswapRepayAdapter } from '../helpers/contracts-deployments'; import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; import { Zero } from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; @@ -21,6 +17,7 @@ import { eContractid } from '../helpers/types'; import { StableDebtToken } from '../types/StableDebtToken'; import { BUIDLEREVM_CHAINID } from '../helpers/buidler-constants'; import { MAX_UINT_AMOUNT } from '../helpers/constants'; +import { VariableDebtToken } from '../types'; const { parseEther } = ethers.utils; const { expect } = require('chai'); @@ -801,32 +798,34 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); - it('should correctly repay debt using the same asset as collateral', async () => { + it('should correctly repay debt via flash loan using the same asset as collateral', async () => { const { users, pool, aDai, dai, uniswapRepayAdapter, helpersContract } = testEnv; const user = users[0].signer; const userAddress = users[0].address; // Add deposit for user - await dai.mint(parseEther('20')); - await dai.approve(pool.address, parseEther('20')); - await pool.deposit(dai.address, parseEther('20'), userAddress, 0); + await dai.mint(parseEther('30')); + await dai.approve(pool.address, parseEther('30')); + await pool.deposit(dai.address, parseEther('30'), userAddress, 0); const amountCollateralToSwap = parseEther('10'); const debtAmount = parseEther('10'); // Open user Debt - await pool.connect(user).borrow(dai.address, debtAmount, 1, 0, userAddress); + await pool.connect(user).borrow(dai.address, debtAmount, 2, 0, userAddress); - const daiStableDebtTokenAddress = ( + const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) - ).stableDebtTokenAddress; + ).variableDebtTokenAddress; - const daiStableDebtContract = await getContract( - eContractid.StableDebtToken, - daiStableDebtTokenAddress + const daiVariableDebtContract = await getContract( + eContractid.VariableDebtToken, + daiVariableDebtTokenAddress ); - const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); const flashLoanDebt = new BigNumber(amountCollateralToSwap.toString()) .multipliedBy(1.0009) @@ -839,7 +838,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, amountCollateralToSwap, - 1, + 2, 0, 0, 0, @@ -861,18 +860,30 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); - const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); const userADaiBalance = await aDai.balanceOf(userAddress); const adapterADaiBalance = await aDai.balanceOf(uniswapRepayAdapter.address); const userDaiBalance = await dai.balanceOf(userAddress); - expect(adapterADaiBalance).to.be.eq(Zero); - expect(adapterDaiBalance).to.be.eq(Zero); - expect(userDaiStableDebtAmountBefore).to.be.gte(debtAmount); - expect(userDaiStableDebtAmount).to.be.lt(debtAmount); - expect(userADaiBalance).to.be.lt(userADaiBalanceBefore); - expect(userADaiBalance).to.be.gte(userADaiBalanceBefore.sub(flashLoanDebt)); - expect(userDaiBalance).to.be.eq(userDaiBalanceBefore); + expect(adapterADaiBalance).to.be.eq(Zero, 'adapter aDAI balance should be zero'); + expect(adapterDaiBalance).to.be.eq(Zero, 'adapter DAI balance should be zero'); + expect(userDaiVariableDebtAmountBefore).to.be.gte( + debtAmount, + ' user DAI variable debt before should be gte debtAmount' + ); + expect(userDaiVariableDebtAmount).to.be.lt( + debtAmount, + 'user dai variable debt amount should be lt debt amount' + ); + expect(userADaiBalance).to.be.lt( + userADaiBalanceBefore, + 'user aDAI balance should be lt aDAI prior balance' + ); + expect(userADaiBalance).to.be.gte( + userADaiBalanceBefore.sub(flashLoanDebt), + 'user aDAI balance should be gte aDAI prior balance sub flash loan debt' + ); + expect(userDaiBalance).to.be.eq(userDaiBalanceBefore, 'user dai balance eq prior balance'); }); }); @@ -1380,27 +1391,29 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userAddress = users[0].address; // Add deposit for user - await dai.mint(parseEther('20')); - await dai.approve(pool.address, parseEther('20')); - await pool.deposit(dai.address, parseEther('20'), userAddress, 0); + await dai.mint(parseEther('30')); + await dai.approve(pool.address, parseEther('30')); + await pool.deposit(dai.address, parseEther('30'), userAddress, 0); const amountCollateralToSwap = parseEther('4'); const debtAmount = parseEther('3'); // Open user Debt - await pool.connect(user).borrow(dai.address, debtAmount, 1, 0, userAddress); + await pool.connect(user).borrow(dai.address, debtAmount, 2, 0, userAddress); - const daiStableDebtTokenAddress = ( + const daiVariableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) - ).stableDebtTokenAddress; + ).variableDebtTokenAddress; - const daiStableDebtContract = await getContract( + const daiVariableDebtContract = await getContract( eContractid.StableDebtToken, - daiStableDebtTokenAddress + daiVariableDebtTokenAddress ); - const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); await aDai.connect(user).approve(uniswapRepayAdapter.address, amountCollateralToSwap); const userADaiBalanceBefore = await aDai.balanceOf(userAddress); @@ -1411,7 +1424,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { dai.address, amountCollateralToSwap, amountCollateralToSwap, - 1, + 2, { amount: 0, deadline: 0, @@ -1423,18 +1436,33 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); - const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); const userADaiBalance = await aDai.balanceOf(userAddress); const adapterADaiBalance = await aDai.balanceOf(uniswapRepayAdapter.address); const userDaiBalance = await dai.balanceOf(userAddress); - expect(adapterADaiBalance).to.be.eq(Zero); - expect(adapterDaiBalance).to.be.eq(Zero); - expect(userDaiStableDebtAmountBefore).to.be.gte(debtAmount); - expect(userDaiStableDebtAmount).to.be.lt(debtAmount); - expect(userADaiBalance).to.be.lt(userADaiBalanceBefore); - expect(userADaiBalance).to.be.gte(userADaiBalanceBefore.sub(amountCollateralToSwap)); - expect(userDaiBalance).to.be.eq(userDaiBalanceBefore); + expect(adapterADaiBalance).to.be.eq(Zero, 'adapter aADAI should be zero'); + expect(adapterDaiBalance).to.be.eq(Zero, 'adapter DAI should be zero'); + expect(userDaiVariableDebtAmountBefore).to.be.gte( + debtAmount, + 'user dai variable debt before should be gte debtAmount' + ); + expect(userDaiVariableDebtAmount).to.be.lt( + debtAmount, + 'current user dai variable debt amount should be less than debtAmount' + ); + expect(userADaiBalance).to.be.lt( + userADaiBalanceBefore, + 'current user aDAI balance should be less than prior balance' + ); + expect(userADaiBalance).to.be.gte( + userADaiBalanceBefore.sub(amountCollateralToSwap), + 'current user aDAI balance should be gte user balance sub swapped collateral' + ); + expect(userDaiBalance).to.be.eq( + userDaiBalanceBefore, + 'user DAI balance should remain equal' + ); }); }); }); diff --git a/test/weth-gateway.spec.ts b/test/weth-gateway.spec.ts index f564183b..eadf9703 100644 --- a/test/weth-gateway.spec.ts +++ b/test/weth-gateway.spec.ts @@ -12,11 +12,17 @@ const { expect } = require('chai'); makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => { const zero = BigNumber.from('0'); const depositSize = parseEther('5'); - - it('Deposit WETH', async () => { - const { users, wethGateway, aWETH, pool } = testEnv; + const daiSize = parseEther('10000'); + it('Deposit WETH via WethGateway and DAI', async () => { + const { users, wethGateway, aWETH } = testEnv; const user = users[1]; + const depositor = users[0]; + + // Deposit liquidity with native ETH + await wethGateway + .connect(depositor.signer) + .depositETH(depositor.address, '0', { value: depositSize }); // Deposit with native ETH await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); @@ -96,10 +102,16 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Borrow stable WETH and Full Repay with ETH', async () => { - const { users, wethGateway, aWETH, weth, pool, helpersContract } = testEnv; + const { users, wethGateway, aDai, weth, dai, pool, helpersContract } = testEnv; const borrowSize = parseEther('1'); const repaySize = borrowSize.add(borrowSize.mul(5).div(100)); const user = users[1]; + const depositor = users[0]; + + // Deposit with native ETH + await wethGateway + .connect(depositor.signer) + .depositETH(depositor.address, '0', { value: depositSize }); const { stableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address @@ -107,13 +119,15 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => const stableDebtToken = await getStableDebtToken(stableDebtTokenAddress); - // Deposit with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); + // Deposit 10000 DAI + await dai.connect(user.signer).mint(daiSize); + await dai.connect(user.signer).approve(pool.address, daiSize); + await pool.connect(user.signer).deposit(dai.address, daiSize, user.address, '0'); - const aTokensBalance = await aWETH.balanceOf(user.address); + const aTokensBalance = await aDai.balanceOf(user.address); expect(aTokensBalance).to.be.gt(zero); - expect(aTokensBalance).to.be.gte(depositSize); + expect(aTokensBalance).to.be.gte(daiSize); // Borrow WETH with WETH as collateral await waitForTx( @@ -133,6 +147,10 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => const debtBalanceAfterRepay = await stableDebtToken.balanceOf(user.address); expect(debtBalanceAfterRepay).to.be.eq(zero); + + // Withdraw DAI + await aDai.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool.connect(user.signer).withdraw(dai.address, MAX_UINT_AMOUNT, user.address); }); it('Borrow variable WETH and Full Repay with ETH', async () => {