Add new edge case when flash liquidation same asset. Add tests.

This commit is contained in:
David Racero 2021-01-29 18:09:06 +01:00
parent d464b0d592
commit 37ac8b5297
3 changed files with 252 additions and 83 deletions

View File

@ -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);
}
}

4
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "protocol-v2",
"version": "1.0.0",
"name": "@aave/protocol-v2",
"version": "1.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -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.'
);
});
});