mirror of
https://github.com/Instadapp/aave-protocol-v2.git
synced 2024-07-29 21:47:30 +00:00
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:
commit
f48c8d7395
|
@ -386,6 +386,7 @@ abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapt
|
|||
}
|
||||
|
||||
uint256 bestAmountOut;
|
||||
|
||||
try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, simplePath) returns (
|
||||
uint256[] memory resultAmounts
|
||||
) {
|
||||
|
|
184
contracts/adapters/FlashLiquidationAdapter.sol
Normal file
184
contracts/adapters/FlashLiquidationAdapter.sol
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
7
helpers/tenderly-utils.ts
Normal file
7
helpers/tenderly-utils.ts
Normal 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');
|
|
@ -70,6 +70,7 @@ export enum eContractid {
|
|||
MockUniswapV2Router02 = 'MockUniswapV2Router02',
|
||||
UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter',
|
||||
UniswapRepayAdapter = 'UniswapRepayAdapter',
|
||||
FlashLiquidationAdapter = 'FlashLiquidationAdapter',
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "protocol-v2",
|
||||
"version": "1.0.0",
|
||||
"name": "@aave/protocol-v2",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -208,6 +208,15 @@
|
|||
},
|
||||
"expected": "revert",
|
||||
"revertMessage": "The collateral balance is 0"
|
||||
},
|
||||
{
|
||||
"name": "withdraw",
|
||||
"args": {
|
||||
"reserve": "DAI",
|
||||
"amount": "1000",
|
||||
"user": "1"
|
||||
},
|
||||
"expected": "success"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
850
test/uniswapAdapters.flashLiquidation.spec.ts
Normal file
850
test/uniswapAdapters.flashLiquidation.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user