Merge branch '183-flash-liquidation-adapter' into 'master'

Flash liquidation adapter

Closes #183

See merge request aave-tech/protocol-v2!212
This commit is contained in:
Ernesto Boado 2021-02-01 14:55:03 +00:00
commit f48c8d7395
25 changed files with 1346 additions and 192 deletions

View File

@ -386,6 +386,7 @@ abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapt
}
uint256 bestAmountOut;
try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, simplePath) returns (
uint256[] memory resultAmounts
) {

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -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 <ContractType extends Contract>(
): Promise<ContractType> => {
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]
);
};

View File

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

View File

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

View File

@ -70,6 +70,7 @@ export enum eContractid {
MockUniswapV2Router02 = 'MockUniswapV2Router02',
UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter',
UniswapRepayAdapter = 'UniswapRepayAdapter',
FlashLiquidationAdapter = 'FlashLiquidationAdapter',
}
/*

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

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -208,6 +208,15 @@
},
"expected": "revert",
"revertMessage": "The collateral balance is 0"
},
{
"name": "withdraw",
"args": {
"reserve": "DAI",
"amount": "1000",
"user": "1"
},
"expected": "success"
}
]
},

View File

@ -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"

View File

@ -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"
},

View File

@ -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,

View File

@ -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(

View File

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

View File

@ -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<StableDebtToken>(
eContractid.StableDebtToken,
daiStableDebtTokenAddress
const daiVariableDebtContract = await getContract<VariableDebtToken>(
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<StableDebtToken>(
const daiVariableDebtContract = await getContract<StableDebtToken>(
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'
);
});
});
});

View File

@ -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 () => {