From b432008d06a41b321d4b7c87c34ad40332f99cc0 Mon Sep 17 00:00:00 2001 From: David Racero Date: Mon, 11 Jan 2021 17:40:25 +0100 Subject: [PATCH 1/7] Added flash liquidation adapter first iteration --- contracts/adapters/BaseUniswapAdapter.sol | 545 ++++++++++++++++++ .../adapters/FlashLiquidationAdapter.sol | 254 ++++++++ .../interfaces/IBaseUniswapAdapter.sol | 92 +++ contracts/interfaces/IERC20WithPermit.sol | 16 + contracts/interfaces/IUniswapV2Router02.sol | 30 + 5 files changed, 937 insertions(+) create mode 100644 contracts/adapters/BaseUniswapAdapter.sol create mode 100644 contracts/adapters/FlashLiquidationAdapter.sol create mode 100644 contracts/adapters/interfaces/IBaseUniswapAdapter.sol create mode 100644 contracts/interfaces/IERC20WithPermit.sol create mode 100644 contracts/interfaces/IUniswapV2Router02.sol diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol new file mode 100644 index 00000000..67c0a3d5 --- /dev/null +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol'; +import {SafeMath} from '../dependencies/openzeppelin/contracts/SafeMath.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {SafeERC20} from '../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {Ownable} from '../dependencies/openzeppelin/contracts/Ownable.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {FlashLoanReceiverBase} from '../flashloan/base/FlashLoanReceiverBase.sol'; +import {IBaseUniswapAdapter} from './interfaces/IBaseUniswapAdapter.sol'; + +/** + * @title BaseUniswapAdapter + * @notice Implements the logic for performing assets swaps in Uniswap V2 + * @author Aave + **/ +abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapter, Ownable { + using SafeMath for uint256; + using PercentageMath for uint256; + using SafeERC20 for IERC20; + + // Max slippage percent allowed + uint256 public constant override MAX_SLIPPAGE_PERCENT = 3000; // 30% + // FLash Loan fee set in lending pool + uint256 public constant override FLASHLOAN_PREMIUM_TOTAL = 9; + // USD oracle asset address + address public constant override USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; + + // address public constant WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; mainnet + // address public constant WETH_ADDRESS = 0xd0a1e359811322d97991e03f863a0c30c2cf029c; kovan + + address public immutable override WETH_ADDRESS; + IPriceOracleGetter public immutable override ORACLE; + IUniswapV2Router02 public immutable override UNISWAP_ROUTER; + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter, + address wethAddress + ) public FlashLoanReceiverBase(addressesProvider) { + ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle()); + UNISWAP_ROUTER = uniswapRouter; + WETH_ADDRESS = wethAddress; + } + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices + * @param amountIn Amount of reserveIn + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount out of the reserveOut + * @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function getAmountsOut( + uint256 amountIn, + address reserveIn, + address reserveOut, + bool withFlash + ) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ) + { + AmountCalc memory results = _getAmountsOutData(reserveIn, reserveOut, amountIn, withFlash); + + return ( + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd, + results.path + ); + } + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount and the prices + * @param amountOut Amount of reserveOut + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount in of the reserveIn + * @return uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function getAmountsIn( + uint256 amountOut, + address reserveIn, + address reserveOut, + bool withFlash + ) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ) + { + AmountCalc memory results = _getAmountsInData(reserveIn, reserveOut, amountOut, withFlash); + + return ( + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd, + results.path + ); + } + + /** + * @dev Swaps an exact `amountToSwap` of an asset to another + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap Exact amount of `assetToSwapFrom` to be swapped + * @param minAmountOut the min amount of `assetToSwapTo` to be received from the swap + * @return the amount received from the swap + */ + function _swapExactTokensForTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountOut, + bool useEthPath + ) internal returns (uint256) { + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); + + uint256 expectedMinAmountOut = + amountToSwap + .mul(fromAssetPrice.mul(10**toAssetDecimals)) + .div(toAssetPrice.mul(10**fromAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(MAX_SLIPPAGE_PERCENT)); + + require(expectedMinAmountOut < minAmountOut, 'minAmountOut exceed max slippage'); + + IERC20(assetToSwapFrom).approve(address(UNISWAP_ROUTER), amountToSwap); + + address[] memory path; + if (useEthPath) { + path = new address[](3); + path[0] = assetToSwapFrom; + path[1] = WETH_ADDRESS; + path[2] = assetToSwapTo; + } else { + path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + } + uint256[] memory amounts = + UNISWAP_ROUTER.swapExactTokensForTokens( + amountToSwap, + minAmountOut, + path, + address(this), + block.timestamp + ); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]); + + return amounts[amounts.length - 1]; + } + + /** + * @dev Receive an exact amount `amountToReceive` of `assetToSwapTo` tokens for as few `assetToSwapFrom` tokens as + * possible. + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param maxAmountToSwap Max amount of `assetToSwapFrom` allowed to be swapped + * @param amountToReceive Exact amount of `assetToSwapTo` to receive + * @return the amount swapped + */ + function _swapTokensForExactTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 maxAmountToSwap, + uint256 amountToReceive, + bool useEthPath + ) internal returns (uint256) { + address[] memory path; + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); + + uint256 expectedMaxAmountToSwap = + amountToReceive + .mul(toAssetPrice.mul(10**fromAssetDecimals)) + .div(fromAssetPrice.mul(10**toAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.add(MAX_SLIPPAGE_PERCENT)); + + require(maxAmountToSwap < expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage'); + + IERC20(assetToSwapFrom).approve(address(UNISWAP_ROUTER), maxAmountToSwap); + + if (useEthPath) { + path = new address[](3); + path[0] = assetToSwapFrom; + path[1] = WETH_ADDRESS; + path[2] = assetToSwapTo; + } else { + path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + } + uint256[] memory amounts = + UNISWAP_ROUTER.swapTokensForExactTokens( + amountToReceive, + maxAmountToSwap, + path, + address(this), + block.timestamp + ); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]); + + return amounts[0]; + } + + /** + * @dev Get the price of the asset from the oracle denominated in eth + * @param asset address + * @return eth price for the asset + */ + function _getPrice(address asset) internal view returns (uint256) { + return ORACLE.getAssetPrice(asset); + } + + /** + * @dev Get the decimals of an asset + * @return number of decimals of the asset + */ + function _getDecimals(address asset) internal view returns (uint256) { + return IERC20Detailed(asset).decimals(); + } + + /** + * @dev Get the aToken associated to the asset + * @return address of the aToken + */ + function _getReserveData(address asset) internal view returns (DataTypes.ReserveData memory) { + return LENDING_POOL.getReserveData(asset); + } + + /** + * @dev Pull the ATokens from the user + * @param reserve address of the asset + * @param reserveAToken address of the aToken of the reserve + * @param user address + * @param amount of tokens to be transferred to the contract + * @param permitSignature struct containing the permit signature + */ + function _pullAToken( + address reserve, + address reserveAToken, + address user, + uint256 amount, + PermitSignature memory permitSignature + ) internal { + if (_usePermit(permitSignature)) { + IERC20WithPermit(reserveAToken).permit( + user, + address(this), + permitSignature.amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + + // transfer from user to adapter + IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); + + // withdraw reserve + LENDING_POOL.withdraw(reserve, amount, address(this)); + } + + /** + * @dev Tells if the permit method should be called by inspecting if there is a valid signature. + * If signature params are set to 0, then permit won't be called. + * @param signature struct containing the permit signature + * @return whether or not permit should be called + */ + function _usePermit(PermitSignature memory signature) internal pure returns (bool) { + return + !(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0); + } + + /** + * @dev Calculates the value denominated in USD + * @param reserve Address of the reserve + * @param amount Amount of the reserve + * @param decimals Decimals of the reserve + * @return whether or not permit should be called + */ + function _calcUsdValue( + address reserve, + uint256 amount, + uint256 decimals + ) internal view returns (uint256) { + uint256 ethUsdPrice = _getPrice(USD_ADDRESS); + uint256 reservePrice = _getPrice(reserve); + + return amount.mul(reservePrice).div(10**decimals).mul(ethUsdPrice).div(10**18); + } + + struct AmountOutVars { + uint256 finalAmountIn; + address[] simplePath; + uint256[] amountsWithoutWeth; + uint256[] amountsWithWeth; + address[] pathWithWeth; + } + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountIn Amount of reserveIn + * @return Struct containing the following information: + * uint256 Amount out of the reserveOut + * uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * uint256 In amount of reserveIn value denominated in USD (8 decimals) + * uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function _getAmountsOutData( + address reserveIn, + address reserveOut, + uint256 amountIn, + bool withFlash + ) internal view returns (AmountCalc memory) { + AmountOutVars memory vars; + // Subtract flash loan fee + vars.finalAmountIn = amountIn.sub( + withFlash ? amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000) : 0 + ); + + vars.simplePath = new address[](2); + vars.simplePath[0] = reserveIn; + vars.simplePath[1] = reserveOut; + + vars.pathWithWeth = new address[](3); + if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) { + vars.pathWithWeth[0] = reserveIn; + vars.pathWithWeth[1] = WETH_ADDRESS; + vars.pathWithWeth[2] = reserveOut; + + try UNISWAP_ROUTER.getAmountsOut(vars.finalAmountIn, vars.pathWithWeth) returns ( + uint256[] memory resultsWithWeth + ) { + vars.amountsWithWeth = resultsWithWeth; + } catch { + vars.amountsWithWeth = new uint256[](3); + } + } else { + vars.amountsWithWeth = new uint256[](3); + } + + uint256 bestAmountOut; + try UNISWAP_ROUTER.getAmountsOut(vars.finalAmountIn, vars.simplePath) returns ( + uint256[] memory resultAmounts + ) { + vars.amountsWithoutWeth = resultAmounts; + + bestAmountOut = (vars.amountsWithWeth[2] > vars.amountsWithoutWeth[1]) + ? vars.amountsWithWeth[2] + : vars.amountsWithoutWeth[1]; + } catch { + vars.amountsWithoutWeth = new uint256[](2); + bestAmountOut = vars.amountsWithWeth[2]; + } + + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 outPerInPrice = + vars.finalAmountIn.mul(10**18).mul(10**reserveOutDecimals).div( + bestAmountOut.mul(10**reserveInDecimals) + ); + + return + AmountCalc( + bestAmountOut, + outPerInPrice, + _calcUsdValue(reserveIn, amountIn, reserveInDecimals), + _calcUsdValue(reserveOut, bestAmountOut, reserveOutDecimals), + (bestAmountOut == 0) ? new address[](2) : (bestAmountOut == vars.amountsWithoutWeth[1]) + ? vars.simplePath + : vars.pathWithWeth + ); + } + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return Struct containing the following information: + * uint256 Amount in of the reserveIn + * uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * uint256 In amount of reserveIn value denominated in USD (8 decimals) + * uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function _getAmountsInData( + address reserveIn, + address reserveOut, + uint256 amountOut, + bool withFlash + ) internal view returns (AmountCalc memory) { + (uint256[] memory amounts, address[] memory path) = + _getAmountsInAndPath(reserveIn, reserveOut, amountOut); + + // Add flash loan fee + uint256 finalAmountIn = + amounts[0].add(withFlash ? amounts[0].mul(FLASHLOAN_PREMIUM_TOTAL).div(10000) : 0); + + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 inPerOutPrice = + amountOut.mul(10**18).mul(10**reserveInDecimals).div( + finalAmountIn.mul(10**reserveOutDecimals) + ); + + return + AmountCalc( + finalAmountIn, + inPerOutPrice, + _calcUsdValue(reserveIn, finalAmountIn, reserveInDecimals), + _calcUsdValue(reserveOut, amountOut, reserveOutDecimals), + path + ); + } + + /** + * @dev Calculates the input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return uint256[] amounts Array containing the amountIn and amountOut for a swap + */ + function _getAmountsInAndPath( + address reserveIn, + address reserveOut, + uint256 amountOut + ) internal view returns (uint256[] memory, address[] memory) { + address[] memory simplePath = new address[](2); + simplePath[0] = reserveIn; + simplePath[1] = reserveOut; + + uint256[] memory amountsWithoutWeth; + uint256[] memory amountsWithWeth; + address[] memory pathWithWeth = new address[](3); + + if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) { + pathWithWeth[0] = reserveIn; + pathWithWeth[1] = WETH_ADDRESS; + pathWithWeth[2] = reserveOut; + + try UNISWAP_ROUTER.getAmountsIn(amountOut, pathWithWeth) returns ( + uint256[] memory resultsWithWeth + ) { + amountsWithWeth = resultsWithWeth; + } catch { + amountsWithWeth = new uint256[](3); + } + } else { + amountsWithWeth = new uint256[](3); + } + + try UNISWAP_ROUTER.getAmountsIn(amountOut, simplePath) returns ( + uint256[] memory resultAmounts + ) { + amountsWithoutWeth = resultAmounts; + + return + (amountsWithWeth[2] > amountsWithoutWeth[1]) + ? (amountsWithWeth, pathWithWeth) + : (amountsWithoutWeth, simplePath); + } catch { + return (amountsWithWeth, pathWithWeth); + } + } + + /** + * @dev Calculates the input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return uint256[] amounts Array containing the amountIn and amountOut for a swap + */ + function _getAmountsIn( + address reserveIn, + address reserveOut, + uint256 amountOut, + bool useEthPath + ) internal view returns (uint256[] memory) { + address[] memory path; + + if (useEthPath) { + path = new address[](3); + path[0] = reserveIn; + path[1] = WETH_ADDRESS; + path[2] = reserveOut; + } else { + path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + } + + return UNISWAP_ROUTER.getAmountsIn(amountOut, path); + } + + /** + * @dev Emergency rescue for token stucked on this contract, as failsafe mechanism + * - Funds should never remain in this contract more time than during transactions + * - Only callable by the owner + **/ + function rescueTokens(IERC20 token) external onlyOwner { + token.transfer(owner(), token.balanceOf(address(this))); + } +} diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol new file mode 100644 index 00000000..4b07de60 --- /dev/null +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -0,0 +1,254 @@ +// 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 debtAsset; + address user; + uint256 debtToCover; + bool useEthPath; + } + + struct LiquidationCallLocalVars { + uint256 userCollateralBalance; + uint256 userStableDebt; + uint256 userVariableDebt; + uint256 maxLiquidatableDebt; + uint256 actualDebtToLiquidate; + uint256 maxAmountCollateralToLiquidate; + uint256 maxCollateralToLiquidate; + uint256 debtAmountNeeded; + uint256 collateralPrice; + uint256 debtAssetPrice; + uint256 liquidationBonus; + uint256 collateralDecimals; + uint256 debtAssetDecimals; + IAToken collateralAtoken; + } + + 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 debtAsset The asset that must be covered + * address user The user address with a Health Factor below 1 + * uint256 debtToCover The amount of debt to cover + * bool useEthPath Use WETH as connector path between the collateralAsset and debtAsset at Uniswap + */ + 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.debtAsset, 'INCONSISTENT_PARAMS'); + + _liquidateAndSwap( + decodedParams.collateralAsset, + decodedParams.debtAsset, + 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 debtAsset The asset that must be covered + * @param user The user address with a Health Factor below 1 + * @param debtToCover The amount of debt to coverage, can be max(-1) to liquidate all possible debt + * @param useEthPath true if the swap needs to occur using ETH in the routing, false otherwise + * @param coverAmount Amount of asset requested at the flash loan to liquidate the user position + * @param premium Fee of the requested flash loan + * @param initiator Address of the caller + */ + function _liquidateAndSwap( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover, + bool useEthPath, + uint256 coverAmount, + uint256 premium, + address initiator + ) internal { + DataTypes.ReserveData memory collateralReserve = LENDING_POOL.getReserveData(collateralAsset); + DataTypes.ReserveData memory debtReserve = LENDING_POOL.getReserveData(debtAsset); + LiquidationCallLocalVars memory vars; + + (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebtMemory( + user, + debtReserve + ); + + vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul( + LIQUIDATION_CLOSE_FACTOR_PERCENT + ); + vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); + vars.actualDebtToLiquidate = debtToCover > vars.maxLiquidatableDebt + ? vars.maxLiquidatableDebt + : debtToCover; + + ( + vars.maxCollateralToLiquidate, + vars.debtAmountNeeded + ) = _calculateAvailableCollateralToLiquidate( + collateralReserve, + debtReserve, + collateralAsset, + debtAsset, + vars.actualDebtToLiquidate, + vars.userCollateralBalance + ); + + require(coverAmount >= vars.debtAmountNeeded, 'Not enought cover amount requested'); + + uint256 flashLoanDebt = coverAmount.add(premium); + + // Liquidate the user position and release the underlying collateral + LENDING_POOL.liquidationCall(collateralAsset, debtAsset, user, debtToCover, false); + + // Swap released collateral into the debt asset, to repay the flash loan + uint256 soldAmount = + _swapTokensForExactTokens( + collateralAsset, + debtAsset, + vars.maxCollateralToLiquidate, + flashLoanDebt, + useEthPath + ); + + // Repay flash loan + IERC20(debtAsset).approve(address(LENDING_POOL), flashLoanDebt); + + // Transfer remaining profit to initiator + if (vars.maxCollateralToLiquidate.sub(soldAmount) > 0) { + IERC20(collateralAsset).transfer(initiator, vars.maxCollateralToLiquidate.sub(soldAmount)); + } + } + + /** + * @dev Decodes the information encoded in the flash loan params + * @param params Additional variadic field to include extra params. Expected parameters: + * address collateralAsset The collateral asset to claim + * address debtAsset The asset that must be covered and will be exchanged to pay the flash loan premium + * address user The user address with a Health Factor below 1 + * uint256 debtToCover The amount of debt to cover + * bool useEthPath Use WETH as connector path between the collateralAsset and debtAsset at Uniswap + * @return LiquidationParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal pure returns (LiquidationParams memory) { + ( + address collateralAsset, + address debtAsset, + address user, + uint256 debtToCover, + bool useEthPath + ) = abi.decode(params, (address, address, address, uint256, bool)); + + return LiquidationParams(collateralAsset, debtAsset, user, debtToCover, useEthPath); + } + + /** + * @dev Calculates how much of a specific collateral can be liquidated, given + * a certain amount of debt asset. + * - This function needs to be called after all the checks to validate the liquidation have been performed, + * otherwise it might fail. + * @param collateralReserve The data of the collateral reserve + * @param debtReserve The data of the debt reserve + * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation + * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation + * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover + * @param userCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated + * @return collateralAmount: The maximum amount that is possible to liquidate given all the liquidation constraints + * (user balance, close factor) + * debtAmountNeeded: The amount to repay with the liquidation + **/ + function _calculateAvailableCollateralToLiquidate( + DataTypes.ReserveData memory collateralReserve, + DataTypes.ReserveData memory debtReserve, + address collateralAsset, + address debtAsset, + uint256 debtToCover, + uint256 userCollateralBalance + ) internal view returns (uint256, uint256) { + uint256 collateralAmount = 0; + uint256 debtAmountNeeded = 0; + + LiquidationCallLocalVars memory vars; + + vars.collateralPrice = ORACLE.getAssetPrice(collateralAsset); + vars.debtAssetPrice = ORACLE.getAssetPrice(debtAsset); + + (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve + .configuration + .getParamsMemory(); + (, , , , vars.debtAssetDecimals) = debtReserve.configuration.getParamsMemory(); + + // This is the maximum possible amount of the selected collateral that can be liquidated, given the + // max amount of liquidatable debt + vars.maxAmountCollateralToLiquidate = vars + .debtAssetPrice + .mul(debtToCover) + .mul(10**vars.collateralDecimals) + .percentMul(vars.liquidationBonus) + .div(vars.collateralPrice.mul(10**vars.debtAssetDecimals)); + + if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) { + collateralAmount = userCollateralBalance; + debtAmountNeeded = vars + .collateralPrice + .mul(collateralAmount) + .mul(10**vars.debtAssetDecimals) + .div(vars.debtAssetPrice.mul(10**vars.collateralDecimals)) + .percentDiv(vars.liquidationBonus); + } else { + collateralAmount = vars.maxAmountCollateralToLiquidate; + debtAmountNeeded = debtToCover; + } + return (collateralAmount, debtAmountNeeded); + } +} diff --git a/contracts/adapters/interfaces/IBaseUniswapAdapter.sol b/contracts/adapters/interfaces/IBaseUniswapAdapter.sol new file mode 100644 index 00000000..e94727a2 --- /dev/null +++ b/contracts/adapters/interfaces/IBaseUniswapAdapter.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; +import {IUniswapV2Router02} from '../../interfaces/IUniswapV2Router02.sol'; + +interface IBaseUniswapAdapter { + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + + struct PermitSignature { + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + struct AmountCalc { + uint256 calculatedAmount; + uint256 relativePrice; + uint256 amountInUsd; + uint256 amountOutUsd; + address[] path; + } + + function WETH_ADDRESS() external returns (address); + + function MAX_SLIPPAGE_PERCENT() external returns (uint256); + + function FLASHLOAN_PREMIUM_TOTAL() external returns (uint256); + + function USD_ADDRESS() external returns (address); + + function ORACLE() external returns (IPriceOracleGetter); + + function UNISWAP_ROUTER() external returns (IUniswapV2Router02); + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices + * @param amountIn Amount of reserveIn + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount out of the reserveOut + * @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + * @return address[] The exchange path + */ + function getAmountsOut( + uint256 amountIn, + address reserveIn, + address reserveOut, + bool withFlash + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ); + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount and the prices + * @param amountOut Amount of reserveOut + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount in of the reserveIn + * @return uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + * @return address[] The exchange path + */ + function getAmountsIn( + uint256 amountOut, + address reserveIn, + address reserveOut, + bool withFlash + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ); +} diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol new file mode 100644 index 00000000..46466b90 --- /dev/null +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IERC20WithPermit is IERC20 { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/interfaces/IUniswapV2Router02.sol b/contracts/interfaces/IUniswapV2Router02.sol new file mode 100644 index 00000000..1b1dc475 --- /dev/null +++ b/contracts/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface IUniswapV2Router02 { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint256 amountOut, + uint256 amountInMax, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + returns (uint256[] memory amounts); + + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + returns (uint256[] memory amounts); +} From 94dd996666e7184d66a2a7201f8c35f60f16d679 Mon Sep 17 00:00:00 2001 From: David Racero Date: Mon, 18 Jan 2021 15:40:02 +0100 Subject: [PATCH 2/7] Add test cases for FlashLiquidationAdapter --- .../adapters/FlashLiquidationAdapter.sol | 4 +- test/__setup.spec.ts | 1 + test/helpers/make-suite.ts | 1 - test/uniswapAdapters.flashLiquidation.spec.ts | 625 +++++++++++++++--- 4 files changed, 522 insertions(+), 109 deletions(-) diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol index 5ac66e4f..8c4703ce 100644 --- a/contracts/adapters/FlashLiquidationAdapter.sol +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -145,11 +145,11 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { vars.userCollateralBalance ); - require(coverAmount >= vars.debtAmountNeeded, 'Not enought cover amount requested'); + require(coverAmount >= vars.debtAmountNeeded, 'FLASH_COVER_NOT_ENOUGH'); uint256 flashLoanDebt = coverAmount.add(premium); - require(IERC20(debtAsset).approve(address(LENDING_POOL), debtToCover), 'Approval error'); + IERC20(debtAsset).approve(address(LENDING_POOL), debtToCover); // Liquidate the user position and release the underlying collateral LENDING_POOL.liquidationCall(collateralAsset, debtAsset, user, debtToCover, false); diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 53166680..37ff9cfc 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -233,6 +233,7 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { await waitForTx( await addressesProvider.setLendingPoolCollateralManager(collateralManager.address) ); + await deployMockFlashLoanReceiver(addressesProvider.address); const mockUniswapRouter = await deployMockUniswapRouter(); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index c1bb5b85..4a75e54d 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -39,7 +39,6 @@ import { solidity } from 'ethereum-waffle'; import { AaveConfig } from '../../markets/aave'; import { FlashLiquidationAdapter } from '../../types'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { hrtime } from 'process'; import { usingTenderly } from '../../helpers/tenderly-utils'; chai.use(bignumberChai()); diff --git a/test/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts index 2865810d..ff2284e3 100644 --- a/test/uniswapAdapters.flashLiquidation.spec.ts +++ b/test/uniswapAdapters.flashLiquidation.spec.ts @@ -1,34 +1,113 @@ import { makeSuite, TestEnv } from './helpers/make-suite'; import { convertToCurrencyDecimals, - getContract, buildFlashLiquidationAdapterParams, } from '../helpers/contracts-helpers'; import { getMockUniswapRouter } from '../helpers/contracts-getters'; import { deployFlashLiquidationAdapter } from '../helpers/contracts-deployments'; import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; -import { Zero } from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; import { DRE, evmRevert, evmSnapshot, increaseTime } from '../helpers/misc-utils'; import { ethers } from 'ethers'; -import { eContractid, ProtocolErrors, RateMode } from '../helpers/types'; -import { StableDebtToken } from '../types/StableDebtToken'; +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'; -import { HardhatRuntimeEnvironment } from 'hardhat/types'; -const { parseEther } = ethers.utils; - const { expect } = require('chai'); makeSuite('Uniswap adapters', (testEnv: TestEnv) => { let mockUniswapRouter: MockUniswapV2Router02; + let evmSnapshotId: string; before(async () => { mockUniswapRouter = await getMockUniswapRouter(); }); + const depositAndHFBelowOne = async () => { + const { INVALID_HF } = ProtocolErrors; + const { dai, weth, users, pool, oracle } = testEnv; + const depositor = users[0]; + const borrower = users[1]; + + //mints DAI to depositor + await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); + + //approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + //user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); + //user 2 deposits 1 ETH + const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); + + //mints WETH to borrower + await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); + + //approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); + + //user 2 borrows + + const userGlobalDataBefore = await pool.getUserAccountData(borrower.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const amountDAIToBorrow = await convertToCurrencyDecimals( + dai.address, + new BigNumber(userGlobalDataBefore.availableBorrowsETH.toString()) + .div(daiPrice.toString()) + .multipliedBy(0.95) + .toFixed(0) + ); + + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address); + + const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); + + expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.equal( + '8250', + INVALID_HF + ); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.18).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(borrower.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( + oneEther.toFixed(0), + INVALID_HF + ); + }; + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + describe('Flash Liquidation Adapter', () => { + before('Before LendingPool liquidation: set config', () => { + BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); + }); + + after('After LendingPool liquidation: reset config', () => { + BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); + }); + describe('constructor', () => { it('should deploy with correct parameters', async () => { const { addressesProvider, weth } = testEnv; @@ -51,96 +130,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe('executeOperation', () => { - const { INVALID_HF } = ProtocolErrors; + describe('executeOperation: succesfully liquidateCall and swap via Flash Loan with profits', () => { + it('Liquidates the borrow with profit', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); - 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 }); - }); - - it('Deposits WETH, borrows DAI', 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 userGlobalData = await pool.getUserAccountData(borrower.address); - const daiPrice = await oracle.getAssetPrice(dai.address); - - const amountDAIToBorrow = await convertToCurrencyDecimals( - dai.address, - new BigNumber(userGlobalData.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.bignumber.equal( - '8250', - INVALID_HF - ); - }); - - it('Drop the health factor below 1', async () => { - const { dai, weth, users, pool, oracle } = testEnv; - const borrower = users[1]; - - const daiPrice = await oracle.getAssetPrice(dai.address); - - 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 - ); - }); - - it('Liquidates the borrow', async () => { const { dai, weth, @@ -150,8 +144,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); @@ -162,6 +164,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { dai.address, borrower.address ); + const collateralDecimals = ( await helpersContract.getReserveConfigurationData(weth.address) ).decimals.toString(); @@ -181,17 +184,12 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div(100) .decimalPlaces(0, BigNumber.ROUND_DOWN); - await increaseTime(100); - const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) .multipliedBy(1.0009) .toFixed(0); - await mockUniswapRouter.setAmountOut( - expectedCollateralLiquidated.toString(), - weth.address, - dai.address, - flashLoanDebt + const expectedProfit = ethers.BigNumber.from(expectedCollateralLiquidated.toString()).sub( + expectedSwap ); const params = buildFlashLiquidationAdapterParams( @@ -212,13 +210,23 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { params, 0 ); - await expect(tx) + + // Expect Swapped event + await expect(Promise.resolve(tx)) .to.emit(flashLiquidationAdapter, 'Swapped') + .withArgs(weth.address, dai.address, expectedSwap.toString(), flashLoanDebt); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)) + .to.emit(pool, 'LiquidationCall') .withArgs( weth.address, dai.address, + borrower.address, + amountToLiquidate.toString(), expectedCollateralLiquidated.toString(), - flashLoanDebt + flashLiquidationAdapter.address, + false ); const userReserveDataAfter = await getUserData( @@ -227,6 +235,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); @@ -265,7 +274,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) - .plus(amountToLiquidate) + .plus(flashLoanDebt) .toFixed(0), 'Invalid principal available liquidity' ); @@ -276,6 +285,410 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .toFixed(0), 'Invalid collateral available liquidity' ); + + // Profit after flash loan liquidation + expect(liquidatorWethBalanceAfter).to.be.equal( + liquidatorWethBalanceBefore.add(expectedProfit), + 'Invalid expected WETH profit' + ); + }); + }); + + describe('executeOperation: succesfully liquidateCall and swap via Flash Loan without profits', () => { + it('Liquidates the borrow', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); + + const { + dai, + weth, + users, + pool, + oracle, + helpersContract, + flashLiquidationAdapter, + } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const collateralDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await helpersContract.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(collateralDecimals)) + .div( + new BigNumber(collateralPrice.toString()).times( + new BigNumber(10).pow(principalDecimals) + ) + ) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await ( + await mockUniswapRouter.setAmountToSwap( + weth.address, + expectedCollateralLiquidated.toString() + ) + ).wait(); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ); + + // Expect Swapped event + await expect(Promise.resolve(tx)) + .to.emit(flashLiquidationAdapter, 'Swapped') + .withArgs( + weth.address, + dai.address, + expectedCollateralLiquidated.toString(), + flashLoanDebt + ); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)) + .to.emit(pool, 'LiquidationCall') + .withArgs( + weth.address, + dai.address, + borrower.address, + amountToLiquidate.toString(), + expectedCollateralLiquidated.toString(), + flashLiquidationAdapter.address, + false + ); + + const userReserveDataAfter = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + const liquidatorWethBalanceAfter = await weth.balanceOf(liquidator.address); + + const daiReserveDataAfter = await helpersContract.getReserveData(dai.address); + const ethReserveDataAfter = await helpersContract.getReserveData(weth.address); + + if (!tx.blockNumber) { + expect(false, 'Invalid block number'); + return; + } + const txTimestamp = new BigNumber( + (await DRE.ethers.provider.getBlock(tx.blockNumber)).timestamp + ); + + const stableDebtBeforeTx = calcExpectedStableDebtTokenBalance( + userReserveDataBefore.principalStableDebt, + userReserveDataBefore.stableBorrowRate, + userReserveDataBefore.stableRateLastUpdated, + txTimestamp + ); + + expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( + stableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), + 'Invalid user debt after liquidation' + ); + + //the liquidity index of the principal reserve needs to be bigger than the index before + expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( + daiReserveDataBefore.liquidityIndex.toString(), + 'Invalid liquidity index' + ); + + //the principal APY after a liquidation needs to be lower than the APY before + expect(daiReserveDataAfter.liquidityRate.toString()).to.be.bignumber.lt( + daiReserveDataBefore.liquidityRate.toString(), + 'Invalid liquidity APY' + ); + + expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( + new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) + .plus(flashLoanDebt) + .toFixed(0), + 'Invalid principal available liquidity' + ); + + expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( + new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) + .minus(expectedCollateralLiquidated) + .toFixed(0), + 'Invalid collateral available liquidity' + ); + + // Net Profit == 0 after flash loan liquidation + expect(liquidatorWethBalanceAfter).to.be.equal( + liquidatorWethBalanceBefore, + 'Invalid expected WETH profit' + ); + }); + }); + + describe('executeOperation: succesfully liquidateCall all available debt and swap via Flash Loan ', () => { + it('Liquidates the borrow', async () => { + await depositAndHFBelowOne(); + await increaseTime(100); + + const { + dai, + weth, + users, + pool, + oracle, + helpersContract, + flashLiquidationAdapter, + } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + const daiReserveDataBefore = await helpersContract.getReserveData(dai.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const collateralDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await helpersContract.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + const extraAmount = new BigNumber(amountToLiquidate).times('1.15').toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(collateralDecimals)) + .div( + new BigNumber(collateralPrice.toString()).times( + new BigNumber(10).pow(principalDecimals) + ) + ) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(extraAmount.toString()).multipliedBy(1.0009).toFixed(0); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await ( + await mockUniswapRouter.setAmountToSwap( + weth.address, + expectedCollateralLiquidated.toString() + ) + ).wait(); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + MAX_UINT_AMOUNT, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [extraAmount], + [0], + borrower.address, + params, + 0 + ); + + // Expect Swapped event + await expect(Promise.resolve(tx)) + .to.emit(flashLiquidationAdapter, 'Swapped') + .withArgs( + weth.address, + dai.address, + expectedCollateralLiquidated.toString(), + flashLoanDebt + ); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); + }); + }); + + describe('executeOperation: invalid params', async () => { + it('Revert if debt asset is different than requested flash loan token', async () => { + await depositAndHFBelowOne(); + + const { dai, weth, users, pool, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + + // Wrong debt asset + const params = buildFlashLiquidationAdapterParams( + weth.address, + weth.address, // intentionally bad + borrower.address, + amountToLiquidate, + false + ); + await expect( + pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + + it('Revert if debt asset amount to liquidate is greater than requested flash loan', async () => { + await depositAndHFBelowOne(); + + const { dai, weth, users, pool, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + const expectedSwap = ethers.utils.parseEther('0.4'); + + // Set how much ETH will be sold and swapped for DAI at Uniswap mock + await (await mockUniswapRouter.setAmountToSwap(weth.address, expectedSwap)).wait(); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2); + + // Correct params + const params = buildFlashLiquidationAdapterParams( + weth.address, + dai.address, + borrower.address, + amountToLiquidate.toString(), + false + ); + // Bad flash loan params: requested DAI amount below amountToLiquidate + await expect( + pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [dai.address], + [amountToLiquidate.div(2).toString()], + [0], + borrower.address, + params, + 0 + ) + ).to.be.revertedWith('FLASH_COVER_NOT_ENOUGH'); + }); + + 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'); }); }); }); From c7a8f41d46620511392be89559e445dba3c030bf Mon Sep 17 00:00:00 2001 From: David Racero Date: Thu, 21 Jan 2021 00:42:39 +0100 Subject: [PATCH 3/7] Added test fixes to support latest stable fix --- package.json | 2 +- .../scenarios/borrow-repay-stable.json | 9 ++ test/helpers/scenarios/credit-delegation.json | 30 ++++- .../scenarios/rebalance-stable-rate.json | 111 +++++------------- test/mainnet/check-list.spec.ts | 92 ++++++++------- test/scenario.spec.ts | 16 +-- test/uniswapAdapters.repay.spec.ts | 16 +-- test/weth-gateway.spec.ts | 108 ++++++++++------- 8 files changed, 194 insertions(+), 190 deletions(-) diff --git a/package.json b/package.json index 4fefea1b..7ea71aea 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,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", diff --git a/test/helpers/scenarios/borrow-repay-stable.json b/test/helpers/scenarios/borrow-repay-stable.json index 87e7de0b..3f472387 100644 --- a/test/helpers/scenarios/borrow-repay-stable.json +++ b/test/helpers/scenarios/borrow-repay-stable.json @@ -208,6 +208,15 @@ }, "expected": "revert", "revertMessage": "The collateral balance is 0" + }, + { + "name": "withdraw", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "1" + }, + "expected": "success" } ] }, diff --git a/test/helpers/scenarios/credit-delegation.json b/test/helpers/scenarios/credit-delegation.json index a0aecf1b..0d15d7f1 100644 --- a/test/helpers/scenarios/credit-delegation.json +++ b/test/helpers/scenarios/credit-delegation.json @@ -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" diff --git a/test/helpers/scenarios/rebalance-stable-rate.json b/test/helpers/scenarios/rebalance-stable-rate.json index 70ea820a..8c7e6c19 100644 --- a/test/helpers/scenarios/rebalance-stable-rate.json +++ b/test/helpers/scenarios/rebalance-stable-rate.json @@ -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" }, diff --git a/test/mainnet/check-list.spec.ts b/test/mainnet/check-list.spec.ts index 08416eda..b6b30bc1 100644 --- a/test/mainnet/check-list.spec.ts +++ b/test/mainnet/check-list.spec.ts @@ -1,28 +1,28 @@ -import {MAX_UINT_AMOUNT} from '../../helpers/constants'; -import {convertToCurrencyDecimals} from '../../helpers/contracts-helpers'; -import {makeSuite, TestEnv} from '../helpers/make-suite'; -import {parseEther} from 'ethers/lib/utils'; -import {DRE, waitForTx} from '../../helpers/misc-utils'; -import {BigNumber} from 'ethers'; -import {getStableDebtToken, getVariableDebtToken} from '../../helpers/contracts-getters'; -import {deploySelfdestructTransferMock} from '../../helpers/contracts-deployments'; -import {IUniswapV2Router02Factory} from '../../types/IUniswapV2Router02Factory'; +import { MAX_UINT_AMOUNT } from '../../helpers/constants'; +import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers'; +import { makeSuite, TestEnv } from '../helpers/make-suite'; +import { parseEther } from 'ethers/lib/utils'; +import { DRE, waitForTx } from '../../helpers/misc-utils'; +import { BigNumber } from 'ethers'; +import { getStableDebtToken, getVariableDebtToken } from '../../helpers/contracts-getters'; +import { deploySelfdestructTransferMock } from '../../helpers/contracts-deployments'; +import { IUniswapV2Router02Factory } from '../../types/IUniswapV2Router02Factory'; -const {expect} = require('chai'); +const { expect } = require('chai'); 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; + const { users, wethGateway, aWETH, pool } = testEnv; const user = users[1]; // Deposit with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', {value: depositSize}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -31,7 +31,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Withdraw WETH - Partial', async () => { - const {users, wethGateway, aWETH, pool} = testEnv; + const { users, wethGateway, aWETH, pool } = testEnv; const user = users[1]; const priorEthersBalance = await user.signer.getBalance(); @@ -46,10 +46,10 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { const approveTx = await aWETH .connect(user.signer) .approve(wethGateway.address, MAX_UINT_AMOUNT); - const {gasUsed: approveGas} = await waitForTx(approveTx); + const { gasUsed: approveGas } = await waitForTx(approveTx); // Partial Withdraw and send native Ether to user - const {gasUsed: withdrawGas} = await waitForTx( + const { gasUsed: withdrawGas } = await waitForTx( await wethGateway.connect(user.signer).withdrawETH(partialWithdraw, user.address) ); @@ -68,7 +68,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Withdraw WETH - Full', async () => { - const {users, aWETH, wethGateway, pool} = testEnv; + const { users, aWETH, wethGateway, pool } = testEnv; const user = users[1]; const priorEthersBalance = await user.signer.getBalance(); @@ -80,10 +80,10 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { const approveTx = await aWETH .connect(user.signer) .approve(wethGateway.address, MAX_UINT_AMOUNT); - const {gasUsed: approveGas} = await waitForTx(approveTx); + const { gasUsed: approveGas } = await waitForTx(approveTx); // Full withdraw - const {gasUsed: withdrawGas} = await waitForTx( + const { gasUsed: withdrawGas } = await waitForTx( await wethGateway.connect(user.signer).withdrawETH(MAX_UINT_AMOUNT, user.address) ); @@ -99,22 +99,26 @@ 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, weth, pool, helpersContract } = testEnv; const borrowSize = parseEther('1'); const repaySize = borrowSize.add(borrowSize.mul(5).div(100)); const user = users[1]; - const {stableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses(weth.address); + const { stableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( + weth.address + ); 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( @@ -129,7 +133,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '1', user.address, {value: repaySize}) + .repayETH(MAX_UINT_AMOUNT, '1', user.address, { value: repaySize }) ); const debtBalanceAfterRepay = await stableDebtToken.balanceOf(user.address); @@ -137,19 +141,19 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Borrow variable WETH and Full Repay with ETH', async () => { - const {users, wethGateway, aWETH, weth, pool, helpersContract} = testEnv; + const { users, wethGateway, aWETH, weth, pool, helpersContract } = testEnv; const borrowSize = parseEther('1'); const repaySize = borrowSize.add(borrowSize.mul(5).div(100)); const user = users[1]; - const {variableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses( + const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const varDebtToken = await getVariableDebtToken(variableDebtTokenAddress); // Deposit with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', {value: depositSize}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -170,7 +174,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { await waitForTx( await wethGateway .connect(user.signer) - .repayETH(partialPayment, '2', user.address, {value: partialPayment}) + .repayETH(partialPayment, '2', user.address, { value: partialPayment }) ); const debtBalanceAfterPartialRepay = await varDebtToken.balanceOf(user.address); @@ -180,17 +184,17 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '2', user.address, {value: repaySize}) + .repayETH(MAX_UINT_AMOUNT, '2', user.address, { value: repaySize }) ); const debtBalanceAfterFullRepay = await varDebtToken.balanceOf(user.address); expect(debtBalanceAfterFullRepay).to.be.eq(zero); }); it('Borrow ETH via delegateApprove ETH and repays back', async () => { - const {users, wethGateway, aWETH, weth, helpersContract} = testEnv; + const { users, wethGateway, aWETH, weth, helpersContract } = testEnv; const borrowSize = parseEther('1'); const user = users[2]; - const {variableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses( + const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const varDebtToken = await getVariableDebtToken(variableDebtTokenAddress); @@ -199,7 +203,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { expect(priorDebtBalance).to.be.eq(zero); // Deposit WETH with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', {value: depositSize}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -222,14 +226,14 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '2', user.address, {value: borrowSize.mul(2)}) + .repayETH(MAX_UINT_AMOUNT, '2', user.address, { value: borrowSize.mul(2) }) ); const debtBalanceAfterFullRepay = await varDebtToken.balanceOf(user.address); expect(debtBalanceAfterFullRepay).to.be.eq(zero); }); it('Should revert if receiver function receives Ether if not WETH', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const amount = parseEther('1'); @@ -244,7 +248,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Should revert if fallback functions is called with Ether', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const amount = parseEther('1'); const fakeABI = ['function wantToCallFallback()']; @@ -263,7 +267,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Should revert if fallback functions is called', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const fakeABI = ['function wantToCallFallback()']; @@ -281,7 +285,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Getters should retrieve correct state', async () => { - const {aWETH, weth, pool, wethGateway} = testEnv; + const { aWETH, weth, pool, wethGateway } = testEnv; const WETHAddress = await wethGateway.getWETHAddress(); const aWETHAddress = await wethGateway.getAWETHAddress(); @@ -293,7 +297,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Owner can do emergency token recovery', async () => { - const {users, weth, dai, wethGateway, deployer} = testEnv; + const { users, weth, dai, wethGateway, deployer } = testEnv; const user = users[0]; const amount = parseEther('1'); @@ -328,7 +332,7 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { }); it('Owner can do emergency native ETH recovery', async () => { - const {users, wethGateway, deployer} = testEnv; + const { users, wethGateway, deployer } = testEnv; const user = users[0]; const amount = parseEther('1'); const userBalancePriorCall = await user.signer.getBalance(); @@ -339,13 +343,13 @@ makeSuite('Mainnet Check list', (testEnv: TestEnv) => { // Selfdestruct the mock, pointing to WETHGateway address const callTx = await selfdestructContract .connect(user.signer) - .destroyAndTransfer(wethGateway.address, {value: amount}); - const {gasUsed} = await waitForTx(callTx); + .destroyAndTransfer(wethGateway.address, { value: amount }); + const { gasUsed } = await waitForTx(callTx); const gasFees = gasUsed.mul(callTx.gasPrice); const userBalanceAfterCall = await user.signer.getBalance(); expect(userBalanceAfterCall).to.be.eq(userBalancePriorCall.sub(amount).sub(gasFees), ''); - 'User should have lost the funds'; + ('User should have lost the funds'); // Recover the funds from the contract and sends back to the user await wethGateway.connect(deployer.signer).emergencyEtherTransfer(user.address, amount); diff --git a/test/scenario.spec.ts b/test/scenario.spec.ts index 50793c0c..13037cfb 100644 --- a/test/scenario.spec.ts +++ b/test/scenario.spec.ts @@ -1,12 +1,12 @@ -import {configuration as actionsConfiguration} from './helpers/actions'; -import {configuration as calculationsConfiguration} from './helpers/utils/calculations'; +import { configuration as actionsConfiguration } from './helpers/actions'; +import { configuration as calculationsConfiguration } from './helpers/utils/calculations'; import fs from 'fs'; import BigNumber from 'bignumber.js'; -import {makeSuite} from './helpers/make-suite'; -import {getReservesConfigByPool} from '../helpers/configuration'; -import {AavePools, iAavePoolAssets, IReserveParams} from '../helpers/types'; -import {executeStory} from './helpers/scenario-engine'; +import { makeSuite } from './helpers/make-suite'; +import { getReservesConfigByPool } from '../helpers/configuration'; +import { AavePools, iAavePoolAssets, IReserveParams } from '../helpers/types'; +import { executeStory } from './helpers/scenario-engine'; const scenarioFolder = './test/helpers/scenarios/'; @@ -20,7 +20,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { makeSuite(scenario.title, async (testEnv) => { before('Initializing configuration', async () => { // Sets BigNumber for this suite, instead of globally - BigNumber.config({DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN}); + BigNumber.config({ DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN }); actionsConfiguration.skipIntegrityCheck = false; //set this to true to execute solidity-coverage @@ -30,7 +30,7 @@ fs.readdirSync(scenarioFolder).forEach((file) => { }); after('Reset', () => { // Reset BigNumber - BigNumber.config({DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP}); + BigNumber.config({ DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP }); }); for (const story of scenario.stories) { diff --git a/test/uniswapAdapters.repay.spec.ts b/test/uniswapAdapters.repay.spec.ts index b79409de..27f91e02 100644 --- a/test/uniswapAdapters.repay.spec.ts +++ b/test/uniswapAdapters.repay.spec.ts @@ -803,15 +803,15 @@ 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('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 = ( await helpersContract.getReserveTokensAddresses(dai.address) @@ -1376,16 +1376,16 @@ 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 = ( await helpersContract.getReserveTokensAddresses(dai.address) diff --git a/test/weth-gateway.spec.ts b/test/weth-gateway.spec.ts index 20393b85..1b3f93ea 100644 --- a/test/weth-gateway.spec.ts +++ b/test/weth-gateway.spec.ts @@ -1,25 +1,31 @@ -import {MAX_UINT_AMOUNT} from '../helpers/constants'; -import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; -import {makeSuite, TestEnv} from './helpers/make-suite'; -import {parseEther} from 'ethers/lib/utils'; -import {DRE, waitForTx} from '../helpers/misc-utils'; -import {BigNumber} from 'ethers'; -import {getStableDebtToken, getVariableDebtToken} from '../helpers/contracts-getters'; -import {deploySelfdestructTransferMock} from '../helpers/contracts-deployments'; +import { MAX_UINT_AMOUNT } from '../helpers/constants'; +import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { parseEther } from 'ethers/lib/utils'; +import { DRE, waitForTx } from '../helpers/misc-utils'; +import { BigNumber } from 'ethers'; +import { getStableDebtToken, getVariableDebtToken } from '../helpers/contracts-getters'; +import { deploySelfdestructTransferMock } from '../helpers/contracts-deployments'; -const {expect} = require('chai'); +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, dai, pool } = 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}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -28,7 +34,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Withdraw WETH - Partial', async () => { - const {users, wethGateway, aWETH, pool} = testEnv; + const { users, wethGateway, aWETH, pool } = testEnv; const user = users[1]; const priorEthersBalance = await user.signer.getBalance(); @@ -43,10 +49,10 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => const approveTx = await aWETH .connect(user.signer) .approve(wethGateway.address, MAX_UINT_AMOUNT); - const {gasUsed: approveGas} = await waitForTx(approveTx); + const { gasUsed: approveGas } = await waitForTx(approveTx); // Partial Withdraw and send native Ether to user - const {gasUsed: withdrawGas} = await waitForTx( + const { gasUsed: withdrawGas } = await waitForTx( await wethGateway.connect(user.signer).withdrawETH(partialWithdraw, user.address) ); @@ -65,7 +71,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Withdraw WETH - Full', async () => { - const {users, aWETH, wethGateway, pool} = testEnv; + const { users, aWETH, wethGateway, pool } = testEnv; const user = users[1]; const priorEthersBalance = await user.signer.getBalance(); @@ -77,10 +83,10 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => const approveTx = await aWETH .connect(user.signer) .approve(wethGateway.address, MAX_UINT_AMOUNT); - const {gasUsed: approveGas} = await waitForTx(approveTx); + const { gasUsed: approveGas } = await waitForTx(approveTx); // Full withdraw - const {gasUsed: withdrawGas} = await waitForTx( + const { gasUsed: withdrawGas } = await waitForTx( await wethGateway.connect(user.signer).withdrawETH(MAX_UINT_AMOUNT, user.address) ); @@ -96,22 +102,32 @@ 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]; - const {stableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses(weth.address); + // Deposit with native ETH + await wethGateway + .connect(depositor.signer) + .depositETH(depositor.address, '0', { value: depositSize }); + + const { stableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( + weth.address + ); 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( @@ -126,27 +142,31 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '1', user.address, {value: repaySize}) + .repayETH(MAX_UINT_AMOUNT, '1', user.address, { value: repaySize }) ); 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 () => { - const {users, wethGateway, aWETH, weth, pool, helpersContract} = testEnv; + const { users, wethGateway, aWETH, weth, pool, helpersContract } = testEnv; const borrowSize = parseEther('1'); const repaySize = borrowSize.add(borrowSize.mul(5).div(100)); const user = users[1]; - const {variableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses( + const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const varDebtToken = await getVariableDebtToken(variableDebtTokenAddress); // Deposit with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', {value: depositSize}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -167,7 +187,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => await waitForTx( await wethGateway .connect(user.signer) - .repayETH(partialPayment, '2', user.address, {value: partialPayment}) + .repayETH(partialPayment, '2', user.address, { value: partialPayment }) ); const debtBalanceAfterPartialRepay = await varDebtToken.balanceOf(user.address); @@ -177,17 +197,17 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '2', user.address, {value: repaySize}) + .repayETH(MAX_UINT_AMOUNT, '2', user.address, { value: repaySize }) ); const debtBalanceAfterFullRepay = await varDebtToken.balanceOf(user.address); expect(debtBalanceAfterFullRepay).to.be.eq(zero); }); it('Borrow ETH via delegateApprove ETH and repays back', async () => { - const {users, wethGateway, aWETH, weth, helpersContract} = testEnv; + const { users, wethGateway, aWETH, weth, helpersContract } = testEnv; const borrowSize = parseEther('1'); const user = users[2]; - const {variableDebtTokenAddress} = await helpersContract.getReserveTokensAddresses( + const { variableDebtTokenAddress } = await helpersContract.getReserveTokensAddresses( weth.address ); const varDebtToken = await getVariableDebtToken(variableDebtTokenAddress); @@ -196,7 +216,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => expect(priorDebtBalance).to.be.eq(zero); // Deposit WETH with native ETH - await wethGateway.connect(user.signer).depositETH(user.address, '0', {value: depositSize}); + await wethGateway.connect(user.signer).depositETH(user.address, '0', { value: depositSize }); const aTokensBalance = await aWETH.balanceOf(user.address); @@ -219,14 +239,14 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => await waitForTx( await wethGateway .connect(user.signer) - .repayETH(MAX_UINT_AMOUNT, '2', user.address, {value: borrowSize.mul(2)}) + .repayETH(MAX_UINT_AMOUNT, '2', user.address, { value: borrowSize.mul(2) }) ); const debtBalanceAfterFullRepay = await varDebtToken.balanceOf(user.address); expect(debtBalanceAfterFullRepay).to.be.eq(zero); }); it('Should revert if receiver function receives Ether if not WETH', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const amount = parseEther('1'); @@ -241,7 +261,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Should revert if fallback functions is called with Ether', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const amount = parseEther('1'); const fakeABI = ['function wantToCallFallback()']; @@ -260,7 +280,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Should revert if fallback functions is called', async () => { - const {users, wethGateway} = testEnv; + const { users, wethGateway } = testEnv; const user = users[0]; const fakeABI = ['function wantToCallFallback()']; @@ -278,7 +298,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Getters should retrieve correct state', async () => { - const {aWETH, weth, pool, wethGateway} = testEnv; + const { aWETH, weth, pool, wethGateway } = testEnv; const WETHAddress = await wethGateway.getWETHAddress(); const aWETHAddress = await wethGateway.getAWETHAddress(); @@ -290,7 +310,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Owner can do emergency token recovery', async () => { - const {users, dai, wethGateway, deployer} = testEnv; + const { users, dai, wethGateway, deployer } = testEnv; const user = users[0]; const amount = parseEther('1'); @@ -316,7 +336,7 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => }); it('Owner can do emergency native ETH recovery', async () => { - const {users, wethGateway, deployer} = testEnv; + const { users, wethGateway, deployer } = testEnv; const user = users[0]; const amount = parseEther('1'); const userBalancePriorCall = await user.signer.getBalance(); @@ -327,13 +347,13 @@ makeSuite('Use native ETH at LendingPool via WETHGateway', (testEnv: TestEnv) => // Selfdestruct the mock, pointing to WETHGateway address const callTx = await selfdestructContract .connect(user.signer) - .destroyAndTransfer(wethGateway.address, {value: amount}); - const {gasUsed} = await waitForTx(callTx); + .destroyAndTransfer(wethGateway.address, { value: amount }); + const { gasUsed } = await waitForTx(callTx); const gasFees = gasUsed.mul(callTx.gasPrice); const userBalanceAfterCall = await user.signer.getBalance(); expect(userBalanceAfterCall).to.be.eq(userBalancePriorCall.sub(amount).sub(gasFees), ''); - 'User should have lost the funds'; + ('User should have lost the funds'); // Recover the funds from the contract and sends back to the user await wethGateway.connect(deployer.signer).emergencyEtherTransfer(user.address, amount); From f05550fc04b17eeda8d4511b99ec1eb25f0d3e9f Mon Sep 17 00:00:00 2001 From: David Racero Date: Thu, 21 Jan 2021 20:21:56 +0100 Subject: [PATCH 4/7] Fixed master tests --- test/helpers/utils/calculations.ts | 37 ++++++++++-- test/uniswapAdapters.repay.spec.ts | 92 ++++++++++++++++++++---------- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/test/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 752207e6..a4db008f 100644 --- a/test/helpers/utils/calculations.ts +++ b/test/helpers/utils/calculations.ts @@ -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, @@ -1211,7 +1237,6 @@ export const calcExpectedInterestRates = ( ): BigNumber[] => { const { reservesParams } = configuration; - const reserveIndex = Object.keys(reservesParams).findIndex((value) => value === reserveSymbol); const [, reserveConfiguration] = (Object.entries(reservesParams) as [string, IReserveParams][])[ reserveIndex diff --git a/test/uniswapAdapters.repay.spec.ts b/test/uniswapAdapters.repay.spec.ts index 27f91e02..c271917e 100644 --- a/test/uniswapAdapters.repay.spec.ts +++ b/test/uniswapAdapters.repay.spec.ts @@ -17,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'); @@ -797,7 +798,7 @@ 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; @@ -813,16 +814,18 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt 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( - eContractid.StableDebtToken, - daiStableDebtTokenAddress + const daiVariableDebtContract = await getContract( + eContractid.VariableDebtToken, + daiVariableDebtTokenAddress ); - const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); const flashLoanDebt = new BigNumber(amountCollateralToSwap.toString()) .multipliedBy(1.0009) @@ -835,7 +838,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, amountCollateralToSwap, - 1, + 2, 0, 0, 0, @@ -857,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'); }); }); @@ -1387,16 +1402,18 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt 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( + const daiVariableDebtContract = await getContract( 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); @@ -1407,7 +1424,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { dai.address, amountCollateralToSwap, amountCollateralToSwap, - 1, + 2, { amount: 0, deadline: 0, @@ -1419,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' + ); }); }); }); From 55f14c1af932d31465d8e0561195ccf439a0f243 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 Jan 2021 09:59:47 +0000 Subject: [PATCH 5/7] Use diff balances instead of liquidation logic for flash liquidations --- .../adapters/FlashLiquidationAdapter.sol | 144 +++--------------- test/uniswapAdapters.flashLiquidation.spec.ts | 4 +- 2 files changed, 27 insertions(+), 121 deletions(-) diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol index 8c4703ce..459c038e 100644 --- a/contracts/adapters/FlashLiquidationAdapter.sol +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -30,20 +30,11 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { } struct LiquidationCallLocalVars { - uint256 userCollateralBalance; - uint256 userStableDebt; - uint256 userVariableDebt; - uint256 maxLiquidatableDebt; - uint256 actualDebtToLiquidate; - uint256 maxAmountCollateralToLiquidate; - uint256 maxCollateralToLiquidate; - uint256 debtAmountNeeded; - uint256 collateralPrice; - uint256 debtAssetPrice; - uint256 liquidationBonus; - uint256 collateralDecimals; - uint256 debtAssetDecimals; - IAToken collateralAtoken; + uint256 initCollateralBalance; + uint256 diffCollateralBalance; + uint256 flashLoanDebt; + uint256 soldAmount; + uint256 remainingTokens; } constructor( @@ -115,63 +106,38 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { uint256 premium, address initiator ) internal { - DataTypes.ReserveData memory collateralReserve = LENDING_POOL.getReserveData(collateralAsset); - DataTypes.ReserveData memory debtReserve = LENDING_POOL.getReserveData(debtAsset); LiquidationCallLocalVars memory vars; + vars.initCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)); + vars.flashLoanDebt = coverAmount.add(premium); - (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebtMemory( - user, - debtReserve - ); - vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); - vars.maxLiquidatableDebt = vars.userStableDebt.add(vars.userVariableDebt).percentMul( - LIQUIDATION_CLOSE_FACTOR_PERCENT - ); - - vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); - vars.actualDebtToLiquidate = debtToCover > vars.maxLiquidatableDebt - ? vars.maxLiquidatableDebt - : debtToCover; - - ( - vars.maxCollateralToLiquidate, - vars.debtAmountNeeded - ) = _calculateAvailableCollateralToLiquidate( - collateralReserve, - debtReserve, - collateralAsset, - debtAsset, - vars.actualDebtToLiquidate, - vars.userCollateralBalance - ); - - require(coverAmount >= vars.debtAmountNeeded, 'FLASH_COVER_NOT_ENOUGH'); - - uint256 flashLoanDebt = coverAmount.add(premium); - + // Approve LendingPool to use debt token for liquidation IERC20(debtAsset).approve(address(LENDING_POOL), debtToCover); // Liquidate the user position and release the underlying collateral LENDING_POOL.liquidationCall(collateralAsset, debtAsset, user, debtToCover, false); + // Discover the liquidated tokens + vars.diffCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)).sub( + vars.initCollateralBalance + ); + // Swap released collateral into the debt asset, to repay the flash loan - uint256 soldAmount = - _swapTokensForExactTokens( - collateralAsset, - debtAsset, - vars.maxCollateralToLiquidate, - flashLoanDebt, - useEthPath - ); + vars.soldAmount = _swapTokensForExactTokens( + collateralAsset, + debtAsset, + vars.diffCollateralBalance, + vars.flashLoanDebt, + useEthPath + ); - // Repay flash loan - IERC20(debtAsset).approve(address(LENDING_POOL), flashLoanDebt); + // Allow repay of flash loan + IERC20(debtAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); - uint256 remainingTokens = vars.maxCollateralToLiquidate.sub(soldAmount); + vars.remainingTokens = vars.diffCollateralBalance.sub(vars.soldAmount); // Transfer remaining tokens to initiator - if (remainingTokens > 0) { - IERC20(collateralAsset).transfer(initiator, remainingTokens); + if (vars.remainingTokens > 0) { + IERC20(collateralAsset).transfer(initiator, vars.remainingTokens); } } @@ -196,64 +162,4 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { return LiquidationParams(collateralAsset, debtAsset, user, debtToCover, useEthPath); } - - /** - * @dev Calculates how much of a specific collateral can be liquidated, given - * a certain amount of debt asset. - * - This function needs to be called after all the checks to validate the liquidation have been performed, - * otherwise it might fail. - * @param collateralReserve The data of the collateral reserve - * @param debtReserve The data of the debt reserve - * @param collateralAsset The address of the underlying asset used as collateral, to receive as result of the liquidation - * @param debtAsset The address of the underlying borrowed asset to be repaid with the liquidation - * @param debtToCover The debt amount of borrowed `asset` the liquidator wants to cover - * @param userCollateralBalance The collateral balance for the specific `collateralAsset` of the user being liquidated - * @return collateralAmount: The maximum amount that is possible to liquidate given all the liquidation constraints - * (user balance, close factor) - * debtAmountNeeded: The amount to repay with the liquidation - **/ - function _calculateAvailableCollateralToLiquidate( - DataTypes.ReserveData memory collateralReserve, - DataTypes.ReserveData memory debtReserve, - address collateralAsset, - address debtAsset, - uint256 debtToCover, - uint256 userCollateralBalance - ) internal view returns (uint256, uint256) { - uint256 collateralAmount = 0; - uint256 debtAmountNeeded = 0; - - LiquidationCallLocalVars memory vars; - - vars.collateralPrice = ORACLE.getAssetPrice(collateralAsset); - vars.debtAssetPrice = ORACLE.getAssetPrice(debtAsset); - - (, , vars.liquidationBonus, vars.collateralDecimals, ) = collateralReserve - .configuration - .getParamsMemory(); - (, , , vars.debtAssetDecimals, ) = debtReserve.configuration.getParamsMemory(); - - // This is the maximum possible amount of the selected collateral that can be liquidated, given the - // max amount of liquidatable debt - vars.maxAmountCollateralToLiquidate = vars - .debtAssetPrice - .mul(debtToCover) - .mul(10**vars.collateralDecimals) - .percentMul(vars.liquidationBonus) - .div(vars.collateralPrice.mul(10**vars.debtAssetDecimals)); - - if (vars.maxAmountCollateralToLiquidate > userCollateralBalance) { - collateralAmount = userCollateralBalance; - debtAmountNeeded = vars - .collateralPrice - .mul(collateralAmount) - .mul(10**vars.debtAssetDecimals) - .div(vars.debtAssetPrice.mul(10**vars.collateralDecimals)) - .percentDiv(vars.liquidationBonus); - } else { - collateralAmount = vars.maxAmountCollateralToLiquidate; - debtAmountNeeded = debtToCover; - } - return (collateralAmount, debtAmountNeeded); - } } diff --git a/test/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts index ff2284e3..ab5a0bd1 100644 --- a/test/uniswapAdapters.flashLiquidation.spec.ts +++ b/test/uniswapAdapters.flashLiquidation.spec.ts @@ -18,13 +18,13 @@ 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 { INVALID_HF } = ProtocolErrors; const { dai, weth, users, pool, oracle } = testEnv; const depositor = users[0]; const borrower = users[1]; @@ -643,7 +643,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { params, 0 ) - ).to.be.revertedWith('FLASH_COVER_NOT_ENOUGH'); + ).to.be.revertedWith(LP_LIQUIDATION_CALL_FAILED); }); it('Revert if requested multiple assets', async () => { From 37ac8b5297b89c485fdbffdf8703b246cfcaa9ad Mon Sep 17 00:00:00 2001 From: David Racero Date: Fri, 29 Jan 2021 18:09:06 +0100 Subject: [PATCH 6/7] Add new edge case when flash liquidation same asset. Add tests. --- .../adapters/FlashLiquidationAdapter.sol | 74 +++-- package-lock.json | 4 +- test/uniswapAdapters.flashLiquidation.spec.ts | 257 ++++++++++++++---- 3 files changed, 252 insertions(+), 83 deletions(-) diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol index 459c038e..1f329e0a 100644 --- a/contracts/adapters/FlashLiquidationAdapter.sol +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -23,13 +23,15 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { struct LiquidationParams { address collateralAsset; - address debtAsset; + address borrowedAsset; address user; uint256 debtToCover; bool useEthPath; } struct LiquidationCallLocalVars { + uint256 initFlashBorrowedBalance; + uint256 diffFlashBorrowedBalance; uint256 initCollateralBalance; uint256 diffCollateralBalance; uint256 flashLoanDebt; @@ -53,10 +55,10 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { * @param initiator Address of the caller * @param params Additional variadic field to include extra params. Expected parameters: * address collateralAsset The collateral asset to release and will be exchanged to pay the flash loan premium - * address debtAsset The asset that must be covered + * address borrowedAsset The asset that must be covered * address user The user address with a Health Factor below 1 * uint256 debtToCover The amount of debt to cover - * bool useEthPath Use WETH as connector path between the collateralAsset and debtAsset at Uniswap + * bool useEthPath Use WETH as connector path between the collateralAsset and borrowedAsset at Uniswap */ function executeOperation( address[] calldata assets, @@ -69,11 +71,11 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { LiquidationParams memory decodedParams = _decodeParams(params); - require(assets.length == 1 && assets[0] == decodedParams.debtAsset, 'INCONSISTENT_PARAMS'); + require(assets.length == 1 && assets[0] == decodedParams.borrowedAsset, 'INCONSISTENT_PARAMS'); _liquidateAndSwap( decodedParams.collateralAsset, - decodedParams.debtAsset, + decodedParams.borrowedAsset, decodedParams.user, decodedParams.debtToCover, decodedParams.useEthPath, @@ -88,52 +90,64 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { /** * @dev * @param collateralAsset The collateral asset to release and will be exchanged to pay the flash loan premium - * @param debtAsset The asset that must be covered + * @param borrowedAsset The asset that must be covered * @param user The user address with a Health Factor below 1 * @param debtToCover The amount of debt to coverage, can be max(-1) to liquidate all possible debt * @param useEthPath true if the swap needs to occur using ETH in the routing, false otherwise - * @param coverAmount Amount of asset requested at the flash loan to liquidate the user position + * @param flashBorrowedAmount Amount of asset requested at the flash loan to liquidate the user position * @param premium Fee of the requested flash loan * @param initiator Address of the caller */ function _liquidateAndSwap( address collateralAsset, - address debtAsset, + address borrowedAsset, address user, uint256 debtToCover, bool useEthPath, - uint256 coverAmount, + uint256 flashBorrowedAmount, // 1000 uint256 premium, address initiator ) internal { LiquidationCallLocalVars memory vars; vars.initCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)); - vars.flashLoanDebt = coverAmount.add(premium); + if (collateralAsset != borrowedAsset) { + vars.initFlashBorrowedBalance = IERC20(borrowedAsset).balanceOf(address(this)); + } + vars.flashLoanDebt = flashBorrowedAmount.add(premium); // 1010 // Approve LendingPool to use debt token for liquidation - IERC20(debtAsset).approve(address(LENDING_POOL), debtToCover); + IERC20(borrowedAsset).approve(address(LENDING_POOL), debtToCover); // Liquidate the user position and release the underlying collateral - LENDING_POOL.liquidationCall(collateralAsset, debtAsset, user, debtToCover, false); + LENDING_POOL.liquidationCall(collateralAsset, borrowedAsset, user, debtToCover, false); // Discover the liquidated tokens - vars.diffCollateralBalance = IERC20(collateralAsset).balanceOf(address(this)).sub( - vars.initCollateralBalance - ); + uint256 collateralBalanceAfter = IERC20(collateralAsset).balanceOf(address(this)); - // Swap released collateral into the debt asset, to repay the flash loan - vars.soldAmount = _swapTokensForExactTokens( - collateralAsset, - debtAsset, - vars.diffCollateralBalance, - vars.flashLoanDebt, - useEthPath - ); + vars.diffCollateralBalance = collateralBalanceAfter.sub(vars.initCollateralBalance); + + if (collateralAsset != borrowedAsset) { + // Discover flash loan balance + uint256 flashBorrowedAssetAfter = IERC20(borrowedAsset).balanceOf(address(this)); + + vars.diffFlashBorrowedBalance = flashBorrowedAssetAfter.sub( + vars.initFlashBorrowedBalance.sub(flashBorrowedAmount) + ); + // Swap released collateral into the debt asset, to repay the flash loan + vars.soldAmount = _swapTokensForExactTokens( + collateralAsset, + borrowedAsset, + vars.diffCollateralBalance, + vars.flashLoanDebt.sub(vars.diffFlashBorrowedBalance), + useEthPath + ); + vars.remainingTokens = vars.diffCollateralBalance.sub(vars.soldAmount); + } else { + vars.remainingTokens = vars.diffCollateralBalance.sub(premium); + } // Allow repay of flash loan - IERC20(debtAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); - - vars.remainingTokens = vars.diffCollateralBalance.sub(vars.soldAmount); + IERC20(borrowedAsset).approve(address(LENDING_POOL), vars.flashLoanDebt); // Transfer remaining tokens to initiator if (vars.remainingTokens > 0) { @@ -145,21 +159,21 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { * @dev Decodes the information encoded in the flash loan params * @param params Additional variadic field to include extra params. Expected parameters: * address collateralAsset The collateral asset to claim - * address debtAsset The asset that must be covered and will be exchanged to pay the flash loan premium + * address borrowedAsset The asset that must be covered and will be exchanged to pay the flash loan premium * address user The user address with a Health Factor below 1 * uint256 debtToCover The amount of debt to cover - * bool useEthPath Use WETH as connector path between the collateralAsset and debtAsset at Uniswap + * bool useEthPath Use WETH as connector path between the collateralAsset and borrowedAsset at Uniswap * @return LiquidationParams struct containing decoded params */ function _decodeParams(bytes memory params) internal pure returns (LiquidationParams memory) { ( address collateralAsset, - address debtAsset, + address borrowedAsset, address user, uint256 debtToCover, bool useEthPath ) = abi.decode(params, (address, address, address, uint256, bool)); - return LiquidationParams(collateralAsset, debtAsset, user, debtToCover, useEthPath); + return LiquidationParams(collateralAsset, borrowedAsset, user, debtToCover, useEthPath); } } diff --git a/package-lock.json b/package-lock.json index dfdeb1a5..3ae83800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "protocol-v2", - "version": "1.0.0", + "name": "@aave/protocol-v2", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/test/uniswapAdapters.flashLiquidation.spec.ts b/test/uniswapAdapters.flashLiquidation.spec.ts index ab5a0bd1..063c6930 100644 --- a/test/uniswapAdapters.flashLiquidation.spec.ts +++ b/test/uniswapAdapters.flashLiquidation.spec.ts @@ -7,7 +7,7 @@ import { getMockUniswapRouter } from '../helpers/contracts-getters'; import { deployFlashLiquidationAdapter } from '../helpers/contracts-deployments'; import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; import BigNumber from 'bignumber.js'; -import { DRE, evmRevert, evmSnapshot, increaseTime } from '../helpers/misc-utils'; +import { DRE, evmRevert, evmSnapshot, increaseTime, waitForTx } from '../helpers/misc-utils'; import { ethers } from 'ethers'; import { ProtocolErrors, RateMode } from '../helpers/types'; import { APPROVAL_AMOUNT_LENDING_POOL, MAX_UINT_AMOUNT, oneEther } from '../helpers/constants'; @@ -91,6 +91,84 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); }; + const depositSameAssetAndHFBelowOne = async () => { + const { dai, weth, users, pool, oracle } = testEnv; + const depositor = users[0]; + const borrower = users[1]; + + //mints DAI to depositor + await dai.connect(depositor.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); + + //approve protocol to access depositor wallet + await dai.connect(depositor.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + //user 1 deposits 1000 DAI + const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); + + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); + //user 2 deposits 1 ETH + const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); + + //mints WETH to borrower + await weth.connect(borrower.signer).mint(await convertToCurrencyDecimals(weth.address, '1000')); + + //approve protocol to access the borrower wallet + await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); + + //user 2 borrows + + const userGlobalDataBefore = await pool.getUserAccountData(borrower.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + const amountDAIToBorrow = await convertToCurrencyDecimals( + dai.address, + new BigNumber(userGlobalDataBefore.availableBorrowsETH.toString()) + .div(daiPrice.toString()) + .multipliedBy(0.8) + .toFixed(0) + ); + await waitForTx( + await pool + .connect(borrower.signer) + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address) + ); + + const userGlobalDataBefore2 = await pool.getUserAccountData(borrower.address); + + const amountWETHToBorrow = new BigNumber(userGlobalDataBefore2.availableBorrowsETH.toString()) + .multipliedBy(0.8) + .toFixed(0); + + await pool + .connect(borrower.signer) + .borrow(weth.address, amountWETHToBorrow, RateMode.Variable, '0', borrower.address); + + const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); + + expect(userGlobalDataAfter.currentLiquidationThreshold.toString()).to.be.equal( + '8250', + INVALID_HF + ); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.18).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(borrower.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt( + oneEther.toFixed(0), + INVALID_HF + ); + }; + beforeEach(async () => { evmSnapshotId = await evmSnapshot(); }); @@ -212,22 +290,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs(weth.address, dai.address, expectedSwap.toString(), flashLoanDebt); + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); // Expect LiquidationCall event - await expect(Promise.resolve(tx)) - .to.emit(pool, 'LiquidationCall') - .withArgs( - weth.address, - dai.address, - borrower.address, - amountToLiquidate.toString(), - expectedCollateralLiquidated.toString(), - flashLiquidationAdapter.address, - false - ); + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); const userReserveDataAfter = await getUserData( pool, @@ -255,6 +321,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { txTimestamp ); + const collateralAssetContractBalance = await weth.balanceOf( + flashLiquidationAdapter.address + ); + const borrowAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( stableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), 'Invalid user debt after liquidation' @@ -294,6 +374,87 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); + describe('executeOperation: succesfully liquidateCall with same asset via Flash Loan, but no swap needed', () => { + it('Liquidates the borrow with profit', async () => { + await depositSameAssetAndHFBelowOne(); + await increaseTime(100); + + const { weth, users, pool, oracle, helpersContract, flashLiquidationAdapter } = testEnv; + + const liquidator = users[3]; + const borrower = users[1]; + + const liquidatorWethBalanceBefore = await weth.balanceOf(liquidator.address); + + const assetPrice = await oracle.getAssetPrice(weth.address); + const ethReserveDataBefore = await helpersContract.getReserveData(weth.address); + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + weth.address, + borrower.address + ); + + const assetDecimals = ( + await helpersContract.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const amountToLiquidate = userReserveDataBefore.currentVariableDebt.div(2).toFixed(0); + + const expectedCollateralLiquidated = new BigNumber(assetPrice.toString()) + .times(new BigNumber(amountToLiquidate).times(105)) + .times(new BigNumber(10).pow(assetDecimals)) + .div(new BigNumber(assetPrice.toString()).times(new BigNumber(10).pow(assetDecimals))) + .div(100) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + const params = buildFlashLiquidationAdapterParams( + weth.address, + weth.address, + borrower.address, + amountToLiquidate, + false + ); + const tx = await pool + .connect(liquidator.signer) + .flashLoan( + flashLiquidationAdapter.address, + [weth.address], + [amountToLiquidate], + [0], + borrower.address, + params, + 0 + ); + + // Dont expect Swapped event due is same asset + await expect(Promise.resolve(tx)).to.not.emit(flashLiquidationAdapter, 'Swapped'); + + // Expect LiquidationCall event + await expect(Promise.resolve(tx)) + .to.emit(pool, 'LiquidationCall') + .withArgs( + weth.address, + weth.address, + borrower.address, + amountToLiquidate.toString(), + expectedCollateralLiquidated.toString(), + flashLiquidationAdapter.address, + false + ); + + const borrowAssetContractBalance = await weth.balanceOf(flashLiquidationAdapter.address); + + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + }); + }); + describe('executeOperation: succesfully liquidateCall and swap via Flash Loan without profits', () => { it('Liquidates the borrow', async () => { await depositAndHFBelowOne(); @@ -367,7 +528,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .flashLoan( flashLiquidationAdapter.address, [dai.address], - [amountToLiquidate], + [flashLoanDebt], [0], borrower.address, params, @@ -375,27 +536,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs( - weth.address, - dai.address, - expectedCollateralLiquidated.toString(), - flashLoanDebt - ); + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); // Expect LiquidationCall event - await expect(Promise.resolve(tx)) - .to.emit(pool, 'LiquidationCall') - .withArgs( - weth.address, - dai.address, - borrower.address, - amountToLiquidate.toString(), - expectedCollateralLiquidated.toString(), - flashLiquidationAdapter.address, - false - ); + await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); const userReserveDataAfter = await getUserData( pool, @@ -423,6 +567,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { txTimestamp ); + const collateralAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + const borrowAssetContractBalance = await weth.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); expect(userReserveDataAfter.currentStableDebt.toString()).to.be.bignumber.almostEqual( stableDebtBeforeTx.minus(amountToLiquidate).toFixed(0), 'Invalid user debt after liquidation' @@ -440,13 +595,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 'Invalid liquidity APY' ); - expect(daiReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( - new BigNumber(daiReserveDataBefore.availableLiquidity.toString()) - .plus(flashLoanDebt) - .toFixed(0), - 'Invalid principal available liquidity' - ); - expect(ethReserveDataAfter.availableLiquidity.toString()).to.be.bignumber.almostEqual( new BigNumber(ethReserveDataBefore.availableLiquidity.toString()) .minus(expectedCollateralLiquidated) @@ -512,7 +660,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div(100) .decimalPlaces(0, BigNumber.ROUND_DOWN); - const flashLoanDebt = new BigNumber(extraAmount.toString()).multipliedBy(1.0009).toFixed(0); + const flashLoanDebt = new BigNumber(amountToLiquidate.toString()) + .multipliedBy(1.0009) + .toFixed(0); // Set how much ETH will be sold and swapped for DAI at Uniswap mock await ( @@ -542,17 +692,22 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); // Expect Swapped event - await expect(Promise.resolve(tx)) - .to.emit(flashLiquidationAdapter, 'Swapped') - .withArgs( - weth.address, - dai.address, - expectedCollateralLiquidated.toString(), - flashLoanDebt - ); + await expect(Promise.resolve(tx)).to.emit(flashLiquidationAdapter, 'Swapped'); // Expect LiquidationCall event await expect(Promise.resolve(tx)).to.emit(pool, 'LiquidationCall'); + + const collateralAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + const borrowAssetContractBalance = await dai.balanceOf(flashLiquidationAdapter.address); + + expect(collateralAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); + expect(borrowAssetContractBalance).to.be.equal( + '0', + 'Contract address should not keep any balance.' + ); }); }); From 211dca7a07c7959067074c7c68b58734742a8c2b Mon Sep 17 00:00:00 2001 From: David Racero Date: Mon, 1 Feb 2021 13:24:07 +0100 Subject: [PATCH 7/7] Add new variable borrowedAssetLeftovers to improve readability --- contracts/adapters/FlashLiquidationAdapter.sol | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/adapters/FlashLiquidationAdapter.sol b/contracts/adapters/FlashLiquidationAdapter.sol index 1f329e0a..d488ee7b 100644 --- a/contracts/adapters/FlashLiquidationAdapter.sol +++ b/contracts/adapters/FlashLiquidationAdapter.sol @@ -37,6 +37,7 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { uint256 flashLoanDebt; uint256 soldAmount; uint256 remainingTokens; + uint256 borrowedAssetLeftovers; } constructor( @@ -104,7 +105,7 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { address user, uint256 debtToCover, bool useEthPath, - uint256 flashBorrowedAmount, // 1000 + uint256 flashBorrowedAmount, uint256 premium, address initiator ) internal { @@ -112,8 +113,11 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { 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); // 1010 + vars.flashLoanDebt = flashBorrowedAmount.add(premium); // Approve LendingPool to use debt token for liquidation IERC20(borrowedAsset).approve(address(LENDING_POOL), debtToCover); @@ -124,15 +128,16 @@ contract FlashLiquidationAdapter is BaseUniswapAdapter { // 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 + // Discover flash loan balance after the liquidation uint256 flashBorrowedAssetAfter = IERC20(borrowedAsset).balanceOf(address(this)); - vars.diffFlashBorrowedBalance = flashBorrowedAssetAfter.sub( - vars.initFlashBorrowedBalance.sub(flashBorrowedAmount) - ); + // 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,