aave-protocol-v2/test/uniswapAdapters.flashLiquidation.spec.ts

696 lines
24 KiB
TypeScript

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 } 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
);
};
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')
.withArgs(weth.address, dai.address, expectedSwap.toString(), flashLoanDebt);
// 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
);
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
);
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 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],
[amountToLiquidate],
[0],
borrower.address,
params,
0
);
// Expect Swapped event
await expect(Promise.resolve(tx))
.to.emit(flashLiquidationAdapter, 'Swapped')
.withArgs(
weth.address,
dai.address,
expectedCollateralLiquidated.toString(),
flashLoanDebt
);
// 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
);
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
);
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'
);
// 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(extraAmount.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')
.withArgs(
weth.address,
dai.address,
expectedCollateralLiquidated.toString(),
flashLoanDebt
);
// Expect LiquidationCall event
await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall');
});
});
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');
});
});
});
});