diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol index 459c038e..1f329e0a 100644 --- a/contracts/adapters/FlashLiquidationAdapter.sol +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -23,13 +23,15 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { struct LiquidationParams { address collateralAsset; - address debtAsset; + address borrowedAsset; address user; uint256 debtToCover; bool useEthPath; } struct LiquidationCallLocalVars { + uint256 initFlashBorrowedBalance; + uint256 diffFlashBorrowedBalance; uint256 initCollateralBalance; uint256 diffCollateralBalance; uint256 flashLoanDebt; @@ -53,10 +55,10 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { * @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 debtAsset The asset that must be covered + * 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 debtAsset at Uniswap + * bool useEthPath Use WETH as connector path between the collateralAsset and borrowedAsset at Uniswap */ function executeOperation( address[] calldata assets, @@ -69,11 +71,11 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { LiquidationParams memory decodedParams = _decodeParams(params); - require(assets.length == 1 && assets[0] == decodedParams.debtAsset, 'INCONSISTENT_PARAMS'); + require(assets.length == 1 && assets[0] == decodedParams.borrowedAsset, 'INCONSISTENT_PARAMS'); _liquidateAndSwap( decodedParams.collateralAsset, - decodedParams.debtAsset, + decodedParams.borrowedAsset, decodedParams.user, decodedParams.debtToCover, decodedParams.useEthPath, @@ -88,52 +90,64 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { /** * @dev * @param collateralAsset The collateral asset to release and will be exchanged to pay the flash loan premium - * @param debtAsset The asset that must be covered + * @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 coverAmount Amount of asset requested at the flash loan to liquidate the user position + * @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 debtAsset, + address borrowedAsset, address user, uint256 debtToCover, bool useEthPath, - uint256 coverAmount, + uint256 flashBorrowedAmount, // 1000 uint256 premium, address initiator ) internal { LiquidationCallLocalVars memory vars; vars.initCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)); - vars.flashLoanDebt = coverAmount.add(premium); + if (collateralAsset != borrowedAsset) { + vars.initFlashBorrowedBalance = IERC20(borrowedAsset).balanceOf(address(this)); + } + vars.flashLoanDebt = flashBorrowedAmount.add(premium); // 1010 // Approve LendingPool to use debt token for liquidation - IERC20(debtAsset).approve(address(LENDING_POOL), debtToCover); + IERC20(borrowedAsset).approve(address(LENDING_POOL), debtToCover); // Liquidate the user position and release the underlying collateral - LENDING_POOL.liquidationCall(collateralAsset, debtAsset, user, debtToCover, false); + LENDING_POOL.liquidationCall(collateralAsset, borrowedAsset, user, debtToCover, false); // Discover the liquidated tokens - vars.diffCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)).sub( - vars.initCollateralBalance - ); + uint256 collateralBalanceAfter = IERC20(collateralAsset).balanceOf(address(this)); - // Swap released collateral into the debt asset, to repay the flash loan - vars.soldAmount = _swapTokensForExactTokens( - collateralAsset, - debtAsset, - vars.diffCollateralBalance, - vars.flashLoanDebt, - useEthPath - ); + vars.diffCollateralBalance = collateralBalanceAfter.sub(vars.initCollateralBalance); + + if (collateralAsset != borrowedAsset) { + // Discover flash loan balance + uint256 flashBorrowedAssetAfter = IERC20(borrowedAsset).balanceOf(address(this)); + + vars.diffFlashBorrowedBalance = flashBorrowedAssetAfter.sub( + vars.initFlashBorrowedBalance.sub(flashBorrowedAmount) + ); + // 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(debtAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); - - vars.remainingTokens = vars.diffCollateralBalance.sub(vars.soldAmount); + IERC20(borrowedAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); // Transfer remaining tokens to initiator if (vars.remainingTokens > 0) { @@ -145,21 +159,21 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { * @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 debtAsset The asset that must be covered and will be exchanged to pay the flash loan premium + * 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 debtAsset at Uniswap + * 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 debtAsset, + address borrowedAsset, address user, uint256 debtToCover, bool useEthPath ) = abi.decode(params, (address, address, address, uint256, bool)); - return LiquidationParams(collateralAsset, debtAsset, user, debtToCover, useEthPath); + return LiquidationParams(collateralAsset, borrowedAsset, user, debtToCover, useEthPath); } } 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/test/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts index ab5a0bd1..063c6930 100644 --- a/test/uniswapAdapters.flashLiquidation.spec.ts +++ b/test/uniswapAdapters.flashLiquidation.spec.ts @@ -7,7 +7,7 @@ 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 } from '../helpers/misc-utils'; +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'; @@ -91,6 +91,84 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); }; + 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(); }); @@ -212,22 +290,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs(weth.address, dai.address, expectedSwap.toString(), flashLoanDebt); + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); // Expect LiquidationCall event - await expect(Promise.resolve(tx)) - .to.emit(pool, 'LiquidationCall') - .withArgs( - weth.address, - dai.address, - borrower.address, - amountToLiquidate.toString(), - expectedCollateralLiquidated.toString(), - flashLiquidationAdapter.address, - false - ); + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); const userReserveDataAfter = await getUserData( pool, @@ -255,6 +321,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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' @@ -294,6 +374,87 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); + 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(); @@ -367,7 +528,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .flashLoan( flashLiquidationAdapter.address, [dai.address], - [amountToLiquidate], + [flashLoanDebt], [0], borrower.address, params, @@ -375,27 +536,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs( - weth.address, - dai.address, - expectedCollateralLiquidated.toString(), - flashLoanDebt - ); + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); // Expect LiquidationCall event - await expect(Promise.resolve(tx)) - .to.emit(pool, 'LiquidationCall') - .withArgs( - weth.address, - dai.address, - borrower.address, - amountToLiquidate.toString(), - expectedCollateralLiquidated.toString(), - flashLiquidationAdapter.address, - false - ); + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); const userReserveDataAfter = await getUserData( pool, @@ -423,6 +567,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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' @@ -440,13 +595,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { '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) @@ -512,7 +660,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div(100) .decimalPlaces(0, BigNumber.ROUND_DOWN); - const flashLoanDebt = new BigNumber(extraAmount.toString()).multipliedBy(1.0009).toFixed(0); + 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 ( @@ -542,17 +692,22 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs( - weth.address, - dai.address, - expectedCollateralLiquidated.toString(), - flashLoanDebt - ); + 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.' + ); }); });