From e4485f12fe5df1a3d271fdd18fa50056903465a6 Mon Sep 17 00:00:00 2001 From: eboado Date: Wed, 26 Aug 2020 16:02:22 +0200 Subject: [PATCH 01/14] - Refactored logic of repay() to an internal _executeRepay(). - Initial implementation of flashCollateral() for flash liquidations, repayment with collateral and movement of position. --- contracts/lendingpool/LendingPool.sol | 123 +++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index ec587920..b1b20a5a 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -238,6 +238,16 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { uint256 rateMode, address onBehalfOf ) external override nonReentrant { + _executeRepay(asset, msg.sender, amount, rateMode, onBehalfOf); + } + + function _executeRepay( + address asset, + address user, + uint256 amount, + uint256 rateMode, + address onBehalfOf + ) internal { ReserveLogic.ReserveData storage reserve = _reserves[asset]; (uint256 stableDebt, uint256 variableDebt) = Helpers.getUserCurrentDebt(onBehalfOf, reserve); @@ -278,9 +288,9 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { _usersConfig[onBehalfOf].setBorrowing(reserve.index, false); } - IERC20(asset).safeTransferFrom(msg.sender, aToken, paybackAmount); + IERC20(asset).safeTransferFrom(user, aToken, paybackAmount); - emit Repay(asset, onBehalfOf, msg.sender, paybackAmount); + emit Repay(asset, onBehalfOf, user, paybackAmount); } /** @@ -509,6 +519,115 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { emit FlashLoan(receiverAddress, asset, amount, amountFee); } + /** + * @dev flashes collateral, by both a flash liquidator or the user owning it. + * @param collateralAsset The address of the collateral asset. + * @param debtAsset The address of the debt asset. + * @param collateralAmount Collateral amount to flash. + * @param user Address of the user owning the collateral. + * @param receiverAddress Address of the contract receiving the collateral. + * @param debtMode Numeric variable, managing how to operate with the debt side. + * 1 -> With final repayment, to do it on the stable debt. + * 2 -> With final repayment, to do it on the variable debt. + * 3 -> On movement of the debt to the liquidator, to move the stable debt + * 4 -> On movement of the debt to the liquidator, to move the variable debt + * @param receiveAToken "true" to send aToken to the receiver contract, "false" to send underlying tokens. + * @param referralCode Integrators are assigned a referral code and can potentially receive rewards. + **/ + function flashCollateral( + address collateralAsset, + address debtAsset, + uint256 collateralAmount, + address user, + address receiverAddress, + uint256 debtMode, + bool receiveAToken, + uint16 referralCode + ) external override { + require(debtMode > 0, 'INVALID_DEBT_FLAG'); + + ReserveLogic.ReserveData storage collateralReserve = _reserves[collateralAsset]; + ReserveLogic.ReserveData storage debtReserve = _reserves[debtAsset]; + + address collateralAToken = collateralReserve.aTokenAddress; + uint256 availableCollateral = IERC20(collateralAToken).balanceOf(user); + + require(collateralAmount <= availableCollateral, 'NOT_ENOUGH_BALANCE'); + + address oracle = addressesProvider.getPriceOracle(); + (, , , , healthFactor) = GenericLogic.calculateUserAccountData( + user, + _reserves, + _usersConfig[user], + _reservesList, + oracle + ); + + if (healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && msg.sender != user) { + revert('INVALID_FLASH_COLLATERAL_BY_NON_OWNER'); + } + + uint256 amountToFlash = (msg.sender == user || healthFactor < 0.98 ether) // TODO: better constant + ? collateralAmount + : collateralAmount.div(2); // TODO: better constant + + // If liquidator reclaims the aToken, he receives the equivalent atoken amount, + // otherwise receives the underlying asset + if (receiveAToken) { + IAToken(collateralAToken).transferOnLiquidation(user, receiverAddress, amountToFlash); + } else { + collateralReserve.updateCumulativeIndexesAndTimestamp(); + collateralReserve.updateInterestRates(collateral, aTokenAddress, 0, collateralAmount); + + // Burn of aToken and send the underlying to the receiver + IAToken(aTokenAddress).burn(user, receiver, collateralAmount); + } + + // Notifies the receiver to proceed, sending the underlying or the aToken amount already transferred + IFlashLoanReceiver(receiverAddress).executeOperation( + collateralAsset, + aTokenAddress, + (!receiveAToken) ? collateralAmount : 0, + receiverAToken ? aTokenAmount : 0, + params + ); + + // Calculation of the minimum amount of the debt asset to be received + uint256 debtAmountNeeded = oracle + .getAssetPrice(collateralAsset) + .mul(collateralAmount) + .mul(10**debtReserve.configuration.getDecimals()) + .div(oracle.getAssetPrice(debtAsset).mul(10**collateralReserve.configuration.getDecimals())) + .percentDiv(collateralReserve.configuration.getLiquidationBonus()); + + // Or the debt is transferred to the msg.sender, or funds are transferred from the receiver to repay the debt + if (debtMode > 2) { + (uint256 userStableDebt, uint256 userVariableDebt) = Helpers.getUserCurrentDebt( + user, + debtReserve + ); + + uint256 debtToTransfer; + if (debtMode.div(3) == 1) { + // stable + debtToTransfer = (userStableDebt > debtAmountNeeded) ? debtAmountNeeded : userStableDebt; + IStableDebtToken(debtReserve.stableDebtTokenAddress).burn(user, debtToTransfer); + } else if (debtMode.div(3) == 2) { + // variable + debtToTransfer = (userVariableDebt > debtAmountNeeded) + ? debtAmountNeeded + : userVariableDebt; + IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn(user, debtToTransfer); + } + _executeBorrow( + BorrowLocalVars(debtAsset, msg.sender, debtToTransfer, debt, false, referralCode) + ); + } else { + IERC20(debtAsset).transferFrom(receiverAddress, address(this), debtAmountNeeded); + _executeRepay(asset, msg.sender, amount, debtMode, user); + } + } + /** * @dev accessory functions to fetch data from the core contract **/ From 2cbb1f57148004eedbd75345f621307b18e00832 Mon Sep 17 00:00:00 2001 From: eboado Date: Thu, 3 Sep 2020 15:46:45 +0200 Subject: [PATCH 02/14] - Implemented repayWithCollateral() on LendingPoolLiquidationManager. --- contracts/interfaces/ILendingPool.sol | 21 ++ contracts/lendingpool/LendingPool.sol | 152 +++++---------- .../LendingPoolLiquidationManager.sol | 179 ++++++++++++++++++ contracts/libraries/logic/GenericLogic.sol | 3 +- 4 files changed, 245 insertions(+), 110 deletions(-) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 43bfb554..b773a638 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -231,6 +231,27 @@ interface ILendingPool { bool receiveAToken ) external; + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it + * - The owner can repay with his collateral at any point, no matter the health factor + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below + * @param collateral The address of the collateral asset + * @param principal The address of the owed asset + * @param user Address of the borrower + * @param principalAmount Amount of the debt to repay. type(uint256).max to repay the maximum possible + * @param receiver Address of the contract receiving the collateral to swap + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external; + /** * @dev allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index b1b20a5a..1d266080 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -455,6 +455,49 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { } } + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it + * - The owner can repay with his collateral at any point, no matter the health factor + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below + * @param collateral The address of the collateral asset + * @param principal The address of the owed asset + * @param user Address of the borrower + * @param principalAmount Amount of the debt to repay. type(uint256).max to repay the maximum possible + * @param receiver Address of the contract receiving the collateral to swap + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external override nonReentrant { + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); + + //solium-disable-next-line + (bool success, bytes memory result) = liquidationManager.delegatecall( + abi.encodeWithSignature( + 'repayWithCollateral(address,address,address,uint256,address,bytes)', + collateral, + principal, + user, + principalAmount, + receiver, + params + ) + ); + require(success, 'FAILED_REPAY_WITH_COLLATERAL'); + + (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); + + if (returnCode != 0) { + revert(string(abi.encodePacked(returnMessage))); + } + } + /** * @dev allows smartcontracts to access the liquidity of the pool within one transaction, * as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts @@ -519,115 +562,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { emit FlashLoan(receiverAddress, asset, amount, amountFee); } - /** - * @dev flashes collateral, by both a flash liquidator or the user owning it. - * @param collateralAsset The address of the collateral asset. - * @param debtAsset The address of the debt asset. - * @param collateralAmount Collateral amount to flash. - * @param user Address of the user owning the collateral. - * @param receiverAddress Address of the contract receiving the collateral. - * @param debtMode Numeric variable, managing how to operate with the debt side. - * 1 -> With final repayment, to do it on the stable debt. - * 2 -> With final repayment, to do it on the variable debt. - * 3 -> On movement of the debt to the liquidator, to move the stable debt - * 4 -> On movement of the debt to the liquidator, to move the variable debt - * @param receiveAToken "true" to send aToken to the receiver contract, "false" to send underlying tokens. - * @param referralCode Integrators are assigned a referral code and can potentially receive rewards. - **/ - function flashCollateral( - address collateralAsset, - address debtAsset, - uint256 collateralAmount, - address user, - address receiverAddress, - uint256 debtMode, - bool receiveAToken, - uint16 referralCode - ) external override { - require(debtMode > 0, 'INVALID_DEBT_FLAG'); - - ReserveLogic.ReserveData storage collateralReserve = _reserves[collateralAsset]; - ReserveLogic.ReserveData storage debtReserve = _reserves[debtAsset]; - - address collateralAToken = collateralReserve.aTokenAddress; - uint256 availableCollateral = IERC20(collateralAToken).balanceOf(user); - - require(collateralAmount <= availableCollateral, 'NOT_ENOUGH_BALANCE'); - - address oracle = addressesProvider.getPriceOracle(); - (, , , , healthFactor) = GenericLogic.calculateUserAccountData( - user, - _reserves, - _usersConfig[user], - _reservesList, - oracle - ); - - if (healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD && msg.sender != user) { - revert('INVALID_FLASH_COLLATERAL_BY_NON_OWNER'); - } - - uint256 amountToFlash = (msg.sender == user || healthFactor < 0.98 ether) // TODO: better constant - ? collateralAmount - : collateralAmount.div(2); // TODO: better constant - - // If liquidator reclaims the aToken, he receives the equivalent atoken amount, - // otherwise receives the underlying asset - if (receiveAToken) { - IAToken(collateralAToken).transferOnLiquidation(user, receiverAddress, amountToFlash); - } else { - collateralReserve.updateCumulativeIndexesAndTimestamp(); - collateralReserve.updateInterestRates(collateral, aTokenAddress, 0, collateralAmount); - - // Burn of aToken and send the underlying to the receiver - IAToken(aTokenAddress).burn(user, receiver, collateralAmount); - } - - // Notifies the receiver to proceed, sending the underlying or the aToken amount already transferred - IFlashLoanReceiver(receiverAddress).executeOperation( - collateralAsset, - aTokenAddress, - (!receiveAToken) ? collateralAmount : 0, - receiverAToken ? aTokenAmount : 0, - params - ); - - // Calculation of the minimum amount of the debt asset to be received - uint256 debtAmountNeeded = oracle - .getAssetPrice(collateralAsset) - .mul(collateralAmount) - .mul(10**debtReserve.configuration.getDecimals()) - .div(oracle.getAssetPrice(debtAsset).mul(10**collateralReserve.configuration.getDecimals())) - .percentDiv(collateralReserve.configuration.getLiquidationBonus()); - - // Or the debt is transferred to the msg.sender, or funds are transferred from the receiver to repay the debt - if (debtMode > 2) { - (uint256 userStableDebt, uint256 userVariableDebt) = Helpers.getUserCurrentDebt( - user, - debtReserve - ); - - uint256 debtToTransfer; - if (debtMode.div(3) == 1) { - // stable - debtToTransfer = (userStableDebt > debtAmountNeeded) ? debtAmountNeeded : userStableDebt; - IStableDebtToken(debtReserve.stableDebtTokenAddress).burn(user, debtToTransfer); - } else if (debtMode.div(3) == 2) { - // variable - debtToTransfer = (userVariableDebt > debtAmountNeeded) - ? debtAmountNeeded - : userVariableDebt; - IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn(user, debtToTransfer); - } - _executeBorrow( - BorrowLocalVars(debtAsset, msg.sender, debtToTransfer, debt, false, referralCode) - ); - } else { - IERC20(debtAsset).transferFrom(receiverAddress, address(this), debtAmountNeeded); - _executeRepay(asset, msg.sender, amount, debtMode, user); - } - } - /** * @dev accessory functions to fetch data from the core contract **/ diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index a36ed3f3..7bbf149b 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -21,6 +21,7 @@ import {Helpers} from '../libraries/helpers/Helpers.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; +import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; /** * @title LendingPoolLiquidationManager contract @@ -65,6 +66,24 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl bool receiveAToken ); + /** + @dev emitted when a borrower/liquidator repays with the borrower's collateral + @param collateral the address of the collateral being swapped to repay + @param principal the address of the reserve of the debt + @param user the borrower's address + @param liquidator the address of the liquidator, same as the one of the borrower on self-repayment + @param principalAmount the amount of the debt finally covered + @param swappedCollateralAmount the amount of collateral finally swapped + */ + event RepaidWithCollateral( + address indexed collateral, + address indexed principal, + address indexed user, + address liquidator, + uint256 principalAmount, + uint256 swappedCollateralAmount + ); + enum LiquidationErrors { NO_ERROR, NO_COLLATERAL_AVAILABLE, @@ -271,6 +290,166 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl return (uint256(LiquidationErrors.NO_ERROR), 'No errors'); } + /** + * @dev flashes the underlying collateral on an user to swap for the owed asset and repay + * - Both the owner of the position and other liquidators can execute it. + * - The owner can repay with his collateral at any point, no matter the health factor. + * - Other liquidators can only use this function below 1 HF. To liquidate 50% of the debt > HF 0.98 or the whole below. + * @param collateral The address of the collateral asset. + * @param principal The address of the owed asset. + * @param user Address of the borrower. + * @param principalAmount Amount of the debt to repay. + * @param receiver Address of the contract receiving the collateral to swap. + * @param params Variadic bytes param to pass with extra information to the receiver + **/ + function repayWithCollateral( + address collateral, + address principal, + address user, + uint256 principalAmount, + address receiver, + bytes calldata params + ) external returns (uint256, string memory) { + ReserveLogic.ReserveData storage debtReserve = reserves[principal]; + ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; + + UserConfiguration.Map storage userConfig = usersConfig[user]; + + LiquidationCallLocalVars memory vars; + + (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + user, + reserves, + usersConfig[user], + reservesList, + addressesProvider.getPriceOracle() + ); + + if (msg.sender != user && vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + 'HEALTH_FACTOR_ABOVE_THRESHOLD' + ); + } + + if (msg.sender != user) { + vars.isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.index); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!vars.isCollateralEnabled) { + return ( + uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + 'COLLATERAL_CANNOT_BE_LIQUIDATED' + ); + } + } + + (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt( + user, + debtReserve + ); + + if (vars.userStableDebt == 0 && vars.userVariableDebt == 0) { + return ( + uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), + 'CURRRENCY_NOT_BORROWED' + ); + } + + if (msg.sender == user || vars.healthFactor < GenericLogic.HEALTH_FACTOR_CRITICAL_THRESHOLD) { + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); + } else { + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt).percentMul( + LIQUIDATION_CLOSE_FACTOR_PERCENT + ); + } + + vars.actualAmountToLiquidate = principalAmount > vars.maxPrincipalAmountToLiquidate + ? vars.maxPrincipalAmountToLiquidate + : principalAmount; + + vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); + vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); + + ( + vars.maxCollateralToLiquidate, + vars.principalAmountNeeded + ) = calculateAvailableCollateralToLiquidate( + collateralReserve, + debtReserve, + collateral, + principal, + vars.actualAmountToLiquidate, + vars.userCollateralBalance + ); + + //if principalAmountNeeded < vars.ActualAmountToLiquidate, there isn't enough + //of collateral to cover the actual amount that is being liquidated, hence we liquidate + //a smaller amount + if (vars.principalAmountNeeded < vars.actualAmountToLiquidate) { + vars.actualAmountToLiquidate = vars.principalAmountNeeded; + } + + vars.collateralAtoken.burn(user, receiver, vars.maxCollateralToLiquidate); + + // Notifies the receiver to proceed, sending as param the underlying already transferred + IFlashLoanReceiver(receiver).executeOperation( + collateral, + address(vars.collateralAtoken), + vars.maxCollateralToLiquidate, + 0, + params + ); + + //updating debt reserve + debtReserve.updateCumulativeIndexesAndTimestamp(); + debtReserve.updateInterestRates( + principal, + debtReserve.aTokenAddress, + vars.actualAmountToLiquidate, + 0 + ); + IERC20(principal).transferFrom(receiver, debtReserve.aTokenAddress, vars.actualAmountToLiquidate); + + if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { + IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + user, + vars.actualAmountToLiquidate + ); + } else { + IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( + user, + vars.userVariableDebt + ); + IStableDebtToken(debtReserve.stableDebtTokenAddress).burn( + user, + vars.actualAmountToLiquidate.sub(vars.userVariableDebt) + ); + } + + + //updating collateral reserve + collateralReserve.updateCumulativeIndexesAndTimestamp(); + collateralReserve.updateInterestRates( + collateral, + address(vars.collateralAtoken), + 0, + vars.maxCollateralToLiquidate + ); + + emit RepaidWithCollateral( + collateral, + principal, + user, + msg.sender, + vars.actualAmountToLiquidate, + vars.maxCollateralToLiquidate + ); + + return (uint256(LiquidationErrors.NO_ERROR), 'SUCCESS'); + } + struct AvailableCollateralToLiquidateLocalVars { uint256 userCompoundedBorrowBalance; uint256 liquidationBonus; diff --git a/contracts/libraries/logic/GenericLogic.sol b/contracts/libraries/logic/GenericLogic.sol index ead09d38..efe053a0 100644 --- a/contracts/libraries/logic/GenericLogic.sol +++ b/contracts/libraries/logic/GenericLogic.sol @@ -24,7 +24,8 @@ library GenericLogic { using ReserveConfiguration for ReserveConfiguration.Map; using UserConfiguration for UserConfiguration.Map; - uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1e18; + uint256 public constant HEALTH_FACTOR_LIQUIDATION_THRESHOLD = 1 ether; + uint256 public constant HEALTH_FACTOR_CRITICAL_THRESHOLD = 0.98 ether; struct balanceDecreaseAllowedLocalVars { uint256 decimals; From 3aa0dbc570be1b76b8a13e97951f0d9099051908 Mon Sep 17 00:00:00 2001 From: eboado Date: Tue, 8 Sep 2020 15:05:53 +0200 Subject: [PATCH 03/14] - Added tests of repayWithCollateral(), only for self-liquidation. --- contracts/interfaces/ISwapAdapter.sol | 21 + .../LendingPoolLiquidationManager.sol | 14 +- contracts/mocks/flashloan/MockSwapAdapter.sol | 43 ++ helpers/contracts-helpers.ts | 15 + helpers/types.ts | 1 + package.json | 1 + test/__setup.spec.ts | 4 + test/helpers/actions.ts | 2 +- test/helpers/make-suite.ts | 6 + test/repay-with-collateral.spec.ts | 471 ++++++++++++++++++ 10 files changed, 571 insertions(+), 7 deletions(-) create mode 100644 contracts/interfaces/ISwapAdapter.sol create mode 100644 contracts/mocks/flashloan/MockSwapAdapter.sol create mode 100644 test/repay-with-collateral.spec.ts diff --git a/contracts/interfaces/ISwapAdapter.sol b/contracts/interfaces/ISwapAdapter.sol new file mode 100644 index 00000000..c51ee0fd --- /dev/null +++ b/contracts/interfaces/ISwapAdapter.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +interface ISwapAdapter { + + /** + * @dev Swaps an `amountToSwap` of an asset to another, approving a `fundsDestination` to pull the funds + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap How much `assetToSwapFrom` needs to be swapped + * @param fundsDestination Address that will be pulling the swapped funds + * @param params Additional variadic field to include extra params + */ + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external; +} \ No newline at end of file diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 7bbf149b..b79bfed7 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -21,7 +21,7 @@ import {Helpers} from '../libraries/helpers/Helpers.sol'; import {WadRayMath} from '../libraries/math/WadRayMath.sol'; import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; -import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; +import {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; /** * @title LendingPoolLiquidationManager contract @@ -393,12 +393,14 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl vars.collateralAtoken.burn(user, receiver, vars.maxCollateralToLiquidate); + address principalAToken = debtReserve.aTokenAddress; + // Notifies the receiver to proceed, sending as param the underlying already transferred - IFlashLoanReceiver(receiver).executeOperation( + ISwapAdapter(receiver).executeOperation( collateral, - address(vars.collateralAtoken), + principal, vars.maxCollateralToLiquidate, - 0, + address(this), params ); @@ -406,11 +408,11 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl debtReserve.updateCumulativeIndexesAndTimestamp(); debtReserve.updateInterestRates( principal, - debtReserve.aTokenAddress, + principalAToken, vars.actualAmountToLiquidate, 0 ); - IERC20(principal).transferFrom(receiver, debtReserve.aTokenAddress, vars.actualAmountToLiquidate); + IERC20(principal).transferFrom(receiver, principalAToken, vars.actualAmountToLiquidate); if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( diff --git a/contracts/mocks/flashloan/MockSwapAdapter.sol b/contracts/mocks/flashloan/MockSwapAdapter.sol new file mode 100644 index 00000000..6b2f2330 --- /dev/null +++ b/contracts/mocks/flashloan/MockSwapAdapter.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +import {MintableERC20} from '../tokens/MintableERC20.sol'; +import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; +import {ISwapAdapter} from '../../interfaces/ISwapAdapter.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +contract MockSwapAdapter is ISwapAdapter { + + uint256 amountToReturn; + ILendingPoolAddressesProvider public addressesProvider; + + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + + constructor(ILendingPoolAddressesProvider provider) public { + addressesProvider = provider; + } + + function setAmountToReturn(uint256 amount) public { + amountToReturn = amount; + } + + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external override { + params; + IERC20(assetToSwapFrom).transfer(address(1), amountToSwap); // We don't want to keep funds here + MintableERC20(assetToSwapTo).mint(amountToReturn); + IERC20(assetToSwapTo).approve(fundsDestination, amountToReturn); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, amountToReturn); + } + + function burnAsset(IERC20 asset, uint256 amount) public { + uint256 amountToBurn = (amount == type(uint256).max) ? asset.balanceOf(address(this)) : amount; + asset.transfer(address(0), amountToBurn); + } +} \ No newline at end of file diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index b5b483d3..909676a1 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -31,6 +31,7 @@ import BigNumber from 'bignumber.js'; import {Ierc20Detailed} from '../types/Ierc20Detailed'; import {StableDebtToken} from '../types/StableDebtToken'; import {VariableDebtToken} from '../types/VariableDebtToken'; +import { MockSwapAdapter } from '../types/MockSwapAdapter'; export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => { const currentNetwork = BRE.network.name; @@ -212,6 +213,11 @@ export const deployMockFlashLoanReceiver = async (addressesProvider: tEthereumAd addressesProvider, ]); +export const deployMockSwapAdapter = async (addressesProvider: tEthereumAddress) => + await deployContract(eContractid.MockSwapAdapter, [ + addressesProvider, + ]); + export const deployWalletBalancerProvider = async (addressesProvider: tEthereumAddress) => await deployContract(eContractid.WalletBalanceProvider, [ addressesProvider, @@ -387,6 +393,15 @@ export const getMockFlashLoanReceiver = async (address?: tEthereumAddress) => { ); }; +export const getMockSwapAdapter = async (address?: tEthereumAddress) => { + return await getContract( + eContractid.MockSwapAdapter, + address || + (await getDb().get(`${eContractid.MockSwapAdapter}.${BRE.network.name}`).value()) + .address + ); +}; + export const getLendingRateOracle = async (address?: tEthereumAddress) => { return await getContract( eContractid.LendingRateOracle, diff --git a/helpers/types.ts b/helpers/types.ts index 106e5376..68ec503d 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -32,6 +32,7 @@ export enum eContractid { LendingPoolLiquidationManager = 'LendingPoolLiquidationManager', InitializableAdminUpgradeabilityProxy = 'InitializableAdminUpgradeabilityProxy', MockFlashLoanReceiver = 'MockFlashLoanReceiver', + MockSwapAdapter = 'MockSwapAdapter', WalletBalanceProvider = 'WalletBalanceProvider', AToken = 'AToken', MockAToken = 'MockAToken', diff --git a/package.json b/package.json index 34e56bfa..94ec647c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "types-gen": "typechain --target ethers-v5 --outDir ./types './artifacts/*.json'", "test": "buidler test", "test-scenarios": "buidler test test/__setup.spec.ts test/scenario.spec.ts", + "test-repay-with-collateral": "buidler test test/__setup.spec.ts test/repay-with-collateral.spec.ts", "dev:coverage": "buidler coverage", "dev:deployment": "buidler dev-deployment", "dev:deployExample": "buidler deploy-Example", diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 342b7b64..689ecf3a 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -23,6 +23,7 @@ import { deployStableDebtToken, deployVariableDebtToken, deployGenericAToken, + deployMockSwapAdapter, } from '../helpers/contracts-helpers'; import {LendingPoolAddressesProvider} from '../types/LendingPoolAddressesProvider'; import {ContractTransaction, Signer} from 'ethers'; @@ -503,6 +504,9 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); + const mockSwapAdapter = await deployMockSwapAdapter(addressesProvider.address) + await insertContractAddressInDb(eContractid.MockSwapAdapter, mockSwapAdapter.address) + await deployWalletBalancerProvider(addressesProvider.address); const testHelpers = await deployAaveProtocolTestHelpers(addressesProvider.address); diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index be0daac5..4442b052 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -845,7 +845,7 @@ const getTxCostAndTimestamp = async (tx: ContractReceipt) => { return {txCost, txTimestamp}; }; -const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { +export const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { const {pool} = testEnv; const reserveData = await getReserveData(pool, reserve); const userData = await getUserData(pool, reserve, user); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index e6e5df98..a0787a17 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -9,6 +9,7 @@ import { getMintableErc20, getLendingPoolConfiguratorProxy, getPriceOracle, + getMockSwapAdapter, } from '../../helpers/contracts-helpers'; import {tEthereumAddress} from '../../helpers/types'; import {LendingPool} from '../../types/LendingPool'; @@ -23,6 +24,7 @@ import bignumberChai from 'chai-bignumber'; import {almostEqual} from './almost-equal'; import {PriceOracle} from '../../types/PriceOracle'; import {LendingPoolAddressesProvider} from '../../types/LendingPoolAddressesProvider'; +import { MockSwapAdapter } from '../../types/MockSwapAdapter'; chai.use(bignumberChai()); chai.use(almostEqual()); @@ -44,6 +46,7 @@ export interface TestEnv { usdc: MintableErc20; lend: MintableErc20; addressesProvider: LendingPoolAddressesProvider; + mockSwapAdapter: MockSwapAdapter; } let buidlerevmSnapshotId: string = '0x1'; @@ -67,6 +70,7 @@ const testEnv: TestEnv = { usdc: {} as MintableErc20, lend: {} as MintableErc20, addressesProvider: {} as LendingPoolAddressesProvider, + mockSwapAdapter: {} as MockSwapAdapter } as TestEnv; export async function initializeMakeSuite() { @@ -125,6 +129,8 @@ export async function initializeMakeSuite() { testEnv.usdc = await getMintableErc20(usdcAddress); testEnv.lend = await getMintableErc20(lendAddress); testEnv.weth = await getMintableErc20(wethAddress); + + testEnv.mockSwapAdapter = await getMockSwapAdapter() } export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts new file mode 100644 index 00000000..c22dd7a7 --- /dev/null +++ b/test/repay-with-collateral.spec.ts @@ -0,0 +1,471 @@ +import {TestEnv, makeSuite} from './helpers/make-suite'; +import {APPROVAL_AMOUNT_LENDING_POOL} from '../helpers/constants'; +import {ethers} from 'ethers'; +import BigNumber from 'bignumber.js'; +import { + calcExpectedVariableDebtTokenBalance, + calcExpectedStableDebtTokenBalance, +} from './helpers/utils/calculations'; +import {getContractsData} from './helpers/actions'; +import {waitForTx} from './__setup.spec'; +import {timeLatest} from '../helpers/misc-utils'; +import {tEthereumAddress} from '../helpers/types'; + +const {expect} = require('chai'); +const {parseUnits, parseEther} = ethers.utils; + +const expectRepayWithCollateralEvent = ( + events: ethers.Event[], + pool: tEthereumAddress, + collateral: tEthereumAddress, + borrowing: tEthereumAddress, + user: tEthereumAddress +) => { + if (!events || events.length < 14) { + expect(false, 'INVALID_EVENTS_LENGTH_ON_REPAY_COLLATERAL'); + } + + const repayWithCollateralEvent = events[13]; + + expect(repayWithCollateralEvent.address).to.be.equal(pool); + expect(`0x${repayWithCollateralEvent.topics[1].slice(26)}`.toLowerCase()).to.be.equal( + collateral.toLowerCase() + ); + expect(`0x${repayWithCollateralEvent.topics[2].slice(26)}`).to.be.equal(borrowing.toLowerCase()); + expect(`0x${repayWithCollateralEvent.topics[3].slice(26)}`.toLowerCase()).to.be.equal( + user.toLowerCase() + ); +}; + +makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { + it('User 1 provides some liquidity for others to borrow', async () => { + const {pool, weth, dai, usdc} = testEnv; + + await weth.mint(parseEther('200')); + await weth.approve(pool.address, parseEther('200')); + await pool.deposit(weth.address, parseEther('200'), 0); + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), 0); + await usdc.mint(parseEther('20000')); + await usdc.approve(pool.address, parseEther('20000')); + await pool.deposit(usdc.address, parseEther('20000'), 0); + }); + + it('User 2 deposit WETH and borrows DAI at Variable', async () => { + const {pool, weth, dai, users} = testEnv; + const user = users[1]; + const amountToDeposit = ethers.utils.parseEther('1'); + const amountToBorrow = ethers.utils.parseEther('20'); + + await weth.connect(user.signer).mint(amountToDeposit); + + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + await pool.connect(user.signer).borrow(dai.address, amountToBorrow, 2, 0); + }); + + it('User 2 tries to repay his DAI Variable loan using his WETH collateral. First half the amount, after that, the rest', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[1]; + + const amountToRepay = parseEther('10'); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(dai.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + daiReserveDataBefore, + daiUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(daiUserDataBefore.currentVariableDebt); + + expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(daiUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString() + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ) + ); + }); + + it('User 3 deposits WETH and borrows USDC at Variable', async () => { + const {pool, weth, usdc, users} = testEnv; + const user = users[2]; + const amountToDeposit = parseEther('10'); + const amountToBorrow = parseUnits('40', 6); + + await weth.connect(user.signer).mint(amountToDeposit); + + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + }); + + it('User 3 repays completely his USDC loan by swapping his WETH collateral', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[2]; + + const amountToRepay = parseUnits('10', 6); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString(), + 'INVALID_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + }); + + it('User tries to repay with his collateral a currency he havent borrow', async () => { + const {pool, weth, dai, users, mockSwapAdapter} = testEnv; + const user = users[2]; + + const amountToRepay = parseUnits('10', 6); + + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('revert CURRRENCY_NOT_BORROWED'); + }); + + it('User tries to repay with his collateral all his variable debt and part of the stable', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[2]; + + const amountToDeposit = parseEther('20'); + const amountToBorrowStable = parseUnits('40', 6); + const amountToBorrowVariable = parseUnits('40', 6); + + await weth.connect(user.signer).mint(amountToDeposit); + + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowStable, 1, 0); + + const amountToRepay = parseUnits('80', 6); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + const txReceipt = await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + const expectedStableDebtIncrease = calcExpectedStableDebtTokenBalance( + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentStableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.equal( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .gte(0) + ? new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString() + : '0', + 'INVALID_VARIABLE_DEBT_POSITION' + ); + + const stableDebtRepaid = new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .abs(); + + expect(usdcUserDataAfter.currentStableDebt).to.be.bignumber.equal( + new BigNumber(usdcUserDataBefore.currentStableDebt) + .minus(stableDebtRepaid) + .plus(expectedStableDebtIncrease) + .gte(0) + ? new BigNumber(usdcUserDataBefore.currentStableDebt) + .minus(stableDebtRepaid) + .plus(expectedStableDebtIncrease) + .toString() + : '0', + 'INVALID_STABLE_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + + const eventsEmitted = txReceipt.events || []; + + expectRepayWithCollateralEvent( + eventsEmitted, + pool.address, + weth.address, + usdc.address, + user.address + ); + }); + + // WIP + it('User tries to repay a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[3]; + + const amountToDepositWeth = parseEther('0.1'); + const amountToDepositDAI = parseEther('500'); + const amountToBorrowVariable = parseEther('80'); + + await weth.connect(user.signer).mint(amountToDepositWeth); + await dai.connect(user.signer).mint(amountToDepositDAI); + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDepositWeth, '0'); + await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, '0'); + + await pool.connect(user.signer).borrow(dai.address, amountToBorrowVariable, 2, 0); + + const amountToRepay = parseEther('80'); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + console.log('BEFORE'); + console.log(wethUserDataBefore.currentATokenBalance.toString()); + console.log(daiUserDataBefore.currentVariableDebt.toString()); + console.log(daiUserDataBefore.currentStableDebt.toString()); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + const txReceipt = await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + + console.log('AFTER'); + console.log(wethUserDataAfter.currentATokenBalance.toString()); + console.log(daiUserDataAfter.currentVariableDebt.toString()); + console.log(daiUserDataAfter.currentStableDebt.toString()); + }); +}); From 56ddeceb942dd6955aca52bf0ed7dbb817149135 Mon Sep 17 00:00:00 2001 From: eboado Date: Tue, 8 Sep 2020 16:25:16 +0200 Subject: [PATCH 04/14] - Added extra test of repayWithCollateral() on self-liquidation. --- test/repay-with-collateral.spec.ts | 52 ++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index c22dd7a7..0bd25680 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -247,7 +247,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { ); }); - it('User tries to repay with his collateral a currency he havent borrow', async () => { + it('Revert expected. User 3 tries to repay with his collateral a currency he havent borrow', async () => { const {pool, weth, dai, users, mockSwapAdapter} = testEnv; const user = users[2]; @@ -267,7 +267,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { ).to.be.revertedWith('revert CURRRENCY_NOT_BORROWED'); }); - it('User tries to repay with his collateral all his variable debt and part of the stable', async () => { + it('User 3 tries to repay with his collateral all his variable debt and part of the stable', async () => { const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; const user = users[2]; @@ -402,8 +402,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { ); }); - // WIP - it('User tries to repay a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { + it('User 4 tries to repay a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; const user = users[3]; @@ -435,13 +434,8 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { testEnv ); - console.log('BEFORE'); - console.log(wethUserDataBefore.currentATokenBalance.toString()); - console.log(daiUserDataBefore.currentVariableDebt.toString()); - console.log(daiUserDataBefore.currentStableDebt.toString()); - await mockSwapAdapter.setAmountToReturn(amountToRepay); - const txReceipt = await waitForTx( + await waitForTx( await pool .connect(user.signer) .repayWithCollateral( @@ -463,9 +457,39 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); - console.log('AFTER'); - console.log(wethUserDataAfter.currentATokenBalance.toString()); - console.log(daiUserDataAfter.currentVariableDebt.toString()); - console.log(daiUserDataAfter.currentStableDebt.toString()); + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralConfig = await pool.getReserveConfigurationData(weth.address); + + const collateralDecimals = collateralConfig.decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const collateralLiquidationBonus = collateralConfig.liquidationBonus.toString(); + + const expectedDebtCovered = new BigNumber(collateralPrice.toString()) + .times(new BigNumber(wethUserDataBefore.currentATokenBalance.toString())) + .times(new BigNumber(10).pow(principalDecimals)) + .div( + new BigNumber(principalPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .div(new BigNumber(collateralLiquidationBonus).div(10000).toString()) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + daiReserveDataBefore, + daiUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(daiUserDataBefore.currentVariableDebt); + + expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.equal( + new BigNumber(daiUserDataBefore.currentVariableDebt) + .minus(expectedDebtCovered.toString()) + .plus(expectedVariableDebtIncrease), + 'INVALID_VARIABLE_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal(0); }); }); From 863d888be600a0ce62d0b3e9defcd51e5cbe4445 Mon Sep 17 00:00:00 2001 From: David Racero Date: Tue, 8 Sep 2020 20:06:28 +0200 Subject: [PATCH 05/14] Added flash-liquidation tests, mimics self-liquidation tests --- .../flash-liquidation-with-collateral.spec.ts | 695 ++++++++++++++++++ 1 file changed, 695 insertions(+) create mode 100644 test/flash-liquidation-with-collateral.spec.ts diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts new file mode 100644 index 00000000..5dd2122f --- /dev/null +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -0,0 +1,695 @@ +import {TestEnv, makeSuite} from './helpers/make-suite'; +import {APPROVAL_AMOUNT_LENDING_POOL, oneEther} from '../helpers/constants'; +import {ethers} from 'ethers'; +import BigNumber from 'bignumber.js'; +import { + calcExpectedVariableDebtTokenBalance, + calcExpectedStableDebtTokenBalance, +} from './helpers/utils/calculations'; +import {getContractsData} from './helpers/actions'; +import {waitForTx} from './__setup.spec'; +import {timeLatest} from '../helpers/misc-utils'; +import {tEthereumAddress, ProtocolErrors} from '../helpers/types'; +import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; + +const {expect} = require('chai'); +const {parseUnits, parseEther} = ethers.utils; + +const expectRepayWithCollateralEvent = ( + events: ethers.Event[], + pool: tEthereumAddress, + collateral: tEthereumAddress, + borrowing: tEthereumAddress, + user: tEthereumAddress +) => { + if (!events || events.length < 14) { + expect(false, 'INVALID_EVENTS_LENGTH_ON_REPAY_COLLATERAL'); + } + + const repayWithCollateralEvent = events[13]; + + expect(repayWithCollateralEvent.address).to.be.equal(pool); + expect(`0x${repayWithCollateralEvent.topics[1].slice(26)}`.toLowerCase()).to.be.equal( + collateral.toLowerCase() + ); + expect(`0x${repayWithCollateralEvent.topics[2].slice(26)}`).to.be.equal(borrowing.toLowerCase()); + expect(`0x${repayWithCollateralEvent.topics[3].slice(26)}`.toLowerCase()).to.be.equal( + user.toLowerCase() + ); +}; + +makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEnv) => { + const {INVALID_HF} = ProtocolErrors; + + it('User 1 provides some liquidity for others to borrow', async () => { + const {pool, weth, dai, usdc} = testEnv; + + await weth.mint(parseEther('200')); + await weth.approve(pool.address, parseEther('200')); + await pool.deposit(weth.address, parseEther('200'), 0); + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), 0); + await usdc.mint(parseEther('20000')); + await usdc.approve(pool.address, parseEther('20000')); + await pool.deposit(usdc.address, parseEther('20000'), 0); + }); + + it('User 5 liquidate User 3 collateral, all his variable debt and part of the stable', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[2]; + const liquidator = users[4]; + const amountToDeposit = parseEther('20'); + const amountToBorrow = parseUnits('40', 6); + + await weth.connect(user.signer).mint(amountToDeposit); + + await weth.connect(user.signer).approve(pool.address, amountToDeposit); + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 1, 0); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + // Set HF below 1 + await oracle.setAssetPrice( + usdc.address, + new BigNumber(usdcPrice.toString()).multipliedBy(60).toFixed(0) + ); + const userGlobalDataPrior = await pool.getUserAccountData(user.address); + expect(userGlobalDataPrior.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + const amountToRepay = parseUnits('80', 6); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + const txReceipt = await waitForTx( + await pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + const expectedStableDebtIncrease = calcExpectedStableDebtTokenBalance( + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentStableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.equal( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .gte(0) + ? new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString() + : '0', + 'INVALID_VARIABLE_DEBT_POSITION' + ); + + const stableDebtRepaid = new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .abs(); + + expect(usdcUserDataAfter.currentStableDebt).to.be.bignumber.equal( + new BigNumber(usdcUserDataBefore.currentStableDebt) + .minus(stableDebtRepaid) + .plus(expectedStableDebtIncrease) + .gte(0) + ? new BigNumber(usdcUserDataBefore.currentStableDebt) + .minus(stableDebtRepaid) + .plus(expectedStableDebtIncrease) + .toString() + : '0', + 'INVALID_STABLE_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + + const eventsEmitted = txReceipt.events || []; + + expectRepayWithCollateralEvent( + eventsEmitted, + pool.address, + weth.address, + usdc.address, + user.address + ); + // Resets USDC Price + await oracle.setAssetPrice(usdc.address, usdcPrice); + }); + + it('User 3 deposits WETH and borrows USDC at Variable', async () => { + const {pool, weth, usdc, users, oracle} = testEnv; + const user = users[2]; + const amountToDeposit = parseEther('10'); + + await weth.connect(user.signer).mint(amountToDeposit); + + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + const userGlobalData = await pool.getUserAccountData(user.address); + + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const amountUSDCToBorrow = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(userGlobalData.availableBorrowsETH.toString()) + .div(usdcPrice.toString()) + .multipliedBy(0.95) + .toFixed(0) + ); + + await pool.connect(user.signer).borrow(usdc.address, amountUSDCToBorrow, 2, 0); + }); + + it('User 5 liquidates half the USDC loan of User 3 by swapping his WETH collateral', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[2]; + const liquidator = users[4]; + // Sets USDC Price higher to decrease health factor below 1 + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + await oracle.setAssetPrice( + usdc.address, + new BigNumber(usdcPrice.toString()).multipliedBy(1.15).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(user.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + const amountToRepay = usdcReserveDataBefore.totalBorrowsVariable.dividedBy(2).toFixed(0); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString(), + 'INVALID_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + + // Resets USDC Price + await oracle.setAssetPrice(usdc.address, usdcPrice); + }); + + it('Revert expected. User 5 tries to liquidate an User 3 collateral a currency he havent borrow', async () => { + const {pool, weth, dai, users, oracle, mockSwapAdapter, usdc} = testEnv; + const user = users[2]; + const liquidator = users[4]; + + const amountToRepay = parseUnits('10', 6); + + // Sets USDC Price higher to decrease health factor below 1 + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + await oracle.setAssetPrice( + usdc.address, + new BigNumber(usdcPrice.toString()).multipliedBy(6.4).toFixed(0) + ); + const userGlobalData = await pool.getUserAccountData(user.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + await expect( + pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('revert CURRRENCY_NOT_BORROWED'); + + await oracle.setAssetPrice(usdc.address, usdcPrice); + }); + + it('User 2 deposit WETH and borrows DAI at Variable', async () => { + const {pool, weth, dai, users, oracle} = testEnv; + const user = users[1]; + const amountToDeposit = ethers.utils.parseEther('1'); + + await weth.connect(user.signer).mint(amountToDeposit); + + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDeposit, '0'); + + const userGlobalData = await pool.getUserAccountData(user.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(user.signer).borrow(dai.address, amountDAIToBorrow, 2, 0); + }); + it('User 5 tries to liquidate User 2 DAI Variable loan using his WETH collateral, with good HF', async () => { + const {pool, weth, dai, users, mockSwapAdapter} = testEnv; + const user = users[1]; + const liquidator = users[4]; + + const {reserveData: daiReserveDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + // First half + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.dividedBy(2).toString(); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await expect( + pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('HEALTH_FACTOR_ABOVE_THRESHOLD'); + }); + it('User 5 liquidates User 2 DAI Variable loan using his WETH collateral, half the amount', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[1]; + const liquidator = users[4]; + + // Sets DAI Price higher to decrease health factor below 1 + const daiPrice = await oracle.getAssetPrice(dai.address); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.4).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(user.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + // First half + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.dividedBy(2).toString(); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(dai.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + daiReserveDataBefore, + daiUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(daiUserDataBefore.currentVariableDebt); + + expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(daiUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString() + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ) + ); + // Resets DAI price + await oracle.setAssetPrice(dai.address, daiPrice); + }); + + it('User 2 tries to repay remaining DAI Variable loan using his WETH collateral', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[1]; + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + // Repay the remaining DAI + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.toString(); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(dai.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + daiReserveDataBefore, + daiUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(daiUserDataBefore.currentVariableDebt); + + expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(daiUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString() + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ) + ); + }); + + it.skip('WIP Liquidator tries to repay 4 user a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[3]; + const liquidator = users[5]; + + const amountToDepositWeth = parseEther('0.1'); + const amountToDepositDAI = parseEther('500'); + const amountToBorrowVariable = parseEther('80'); + + await weth.connect(user.signer).mint(amountToDepositWeth); + await dai.connect(user.signer).mint(amountToDepositDAI); + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + + await pool.connect(user.signer).deposit(weth.address, amountToDepositWeth, '0'); + await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, '0'); + + await pool.connect(user.signer).borrow(dai.address, amountToBorrowVariable, 2, 0); + + const amountToRepay = parseEther('80'); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + const wethPrice = await oracle.getAssetPrice(weth.address); + // Set HF below 1 + await oracle.setAssetPrice( + weth.address, + new BigNumber(wethPrice.toString()).multipliedBy(0.1).toFixed(0) + ); + const userGlobalDataPrior = await pool.getUserAccountData(user.address); + expect(userGlobalDataPrior.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(dai.address); + + const collateralConfig = await pool.getReserveConfigurationData(weth.address); + + const collateralDecimals = collateralConfig.decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(dai.address) + ).decimals.toString(); + const collateralLiquidationBonus = collateralConfig.liquidationBonus.toString(); + + const expectedDebtCovered = new BigNumber(collateralPrice.toString()) + .times(new BigNumber(wethUserDataBefore.currentATokenBalance.toString())) + .times(new BigNumber(10).pow(principalDecimals)) + .div( + new BigNumber(principalPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .div(new BigNumber(collateralLiquidationBonus).div(10000).toString()) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + const expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + daiReserveDataBefore, + daiUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(daiUserDataBefore.currentVariableDebt); + + expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.equal( + new BigNumber(daiUserDataBefore.currentVariableDebt) + .minus(expectedDebtCovered.toString()) + .plus(expectedVariableDebtIncrease), + 'INVALID_VARIABLE_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal(0); + // Resets WETH Price + await oracle.setAssetPrice(weth.address, wethPrice); + }); +}); From 37a9c7ad885b95bcfe4de35fc5f33d1e43ad510e Mon Sep 17 00:00:00 2001 From: eboado Date: Wed, 9 Sep 2020 13:06:46 +0200 Subject: [PATCH 06/14] - Added reentrancy guard on repayWithCollateral() and test. --- contracts/lendingpool/LendingPool.sol | 9 ++++++- .../LendingPoolLiquidationManager.sol | 5 ++++ contracts/mocks/flashloan/MockSwapAdapter.sol | 27 +++++++++++++++---- test/repay-with-collateral.spec.ts | 26 ++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 1d266080..9b9372f2 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -51,6 +51,8 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { address[] internal _reservesList; + bool internal _flashLiquidationLocked; + /** * @dev only lending pools configurator can use functions affected by this modifier **/ @@ -474,7 +476,10 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { uint256 principalAmount, address receiver, bytes calldata params - ) external override nonReentrant { + ) external override { + require(!_flashLiquidationLocked, "REENTRANCY_NOT_ALLOWED"); + _flashLiquidationLocked = true; + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line @@ -496,6 +501,8 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable, ILendingPool { if (returnCode != 0) { revert(string(abi.encodePacked(returnMessage))); } + + _flashLiquidationLocked = false; } /** diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index b79bfed7..888edb93 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -37,6 +37,9 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl using ReserveConfiguration for ReserveConfiguration.Map; using UserConfiguration for UserConfiguration.Map; + // IMPORTANT The storage layout of the LendingPool is reproduced here because this contract + // is gonna be used through DELEGATECALL + LendingPoolAddressesProvider internal addressesProvider; mapping(address => ReserveLogic.ReserveData) internal reserves; @@ -44,6 +47,8 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl address[] internal reservesList; + bool internal _flashLiquidationLocked; + uint256 internal constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 5000; /** diff --git a/contracts/mocks/flashloan/MockSwapAdapter.sol b/contracts/mocks/flashloan/MockSwapAdapter.sol index 6b2f2330..85c7d84b 100644 --- a/contracts/mocks/flashloan/MockSwapAdapter.sol +++ b/contracts/mocks/flashloan/MockSwapAdapter.sol @@ -4,11 +4,13 @@ pragma solidity ^0.6.8; import {MintableERC20} from '../tokens/MintableERC20.sol'; import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; import {ISwapAdapter} from '../../interfaces/ISwapAdapter.sol'; +import {ILendingPool} from "../../interfaces/ILendingPool.sol"; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; contract MockSwapAdapter is ISwapAdapter { - uint256 amountToReturn; + uint256 internal _amountToReturn; + bool internal _tryReentrancy; ILendingPoolAddressesProvider public addressesProvider; event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); @@ -18,7 +20,11 @@ contract MockSwapAdapter is ISwapAdapter { } function setAmountToReturn(uint256 amount) public { - amountToReturn = amount; + _amountToReturn = amount; + } + + function setTryReentrancy(bool tryReentrancy) public { + _tryReentrancy = tryReentrancy; } function executeOperation( @@ -30,10 +36,21 @@ contract MockSwapAdapter is ISwapAdapter { ) external override { params; IERC20(assetToSwapFrom).transfer(address(1), amountToSwap); // We don't want to keep funds here - MintableERC20(assetToSwapTo).mint(amountToReturn); - IERC20(assetToSwapTo).approve(fundsDestination, amountToReturn); + MintableERC20(assetToSwapTo).mint(_amountToReturn); + IERC20(assetToSwapTo).approve(fundsDestination, _amountToReturn); - emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, amountToReturn); + if (_tryReentrancy) { + ILendingPool(fundsDestination).repayWithCollateral( + assetToSwapFrom, + assetToSwapTo, + address(1), // Doesn't matter, we just want to test the reentrancy + 1 ether, // Same + address(1), // Same + "0x" + ); + } + + emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, _amountToReturn); } function burnAsset(IERC20 asset, uint256 amount) public { diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index 0bd25680..d7f9d77f 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -67,12 +67,38 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await pool.connect(user.signer).borrow(dai.address, amountToBorrow, 2, 0); }); + it('It is not possible to do reentrancy on repayWithCollateral()', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[1]; + + const amountToRepay = parseEther('10'); + + await waitForTx(await mockSwapAdapter.setTryReentrancy(true)) + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith("FAILED_REPAY_WITH_COLLATERAL") + + }); + it('User 2 tries to repay his DAI Variable loan using his WETH collateral. First half the amount, after that, the rest', async () => { const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; const user = users[1]; const amountToRepay = parseEther('10'); + await waitForTx(await mockSwapAdapter.setTryReentrancy(false)) + const {userData: wethUserDataBefore} = await getContractsData( weth.address, user.address, From d828c63a83e86cf578bb0abfa2635f154e97140f Mon Sep 17 00:00:00 2001 From: eboado Date: Wed, 9 Sep 2020 13:21:19 +0200 Subject: [PATCH 07/14] - Added reset of user's usage as collateral on repayWithCollateral(). --- contracts/lendingpool/LendingPoolLiquidationManager.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 888edb93..247c01e9 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -398,6 +398,10 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl vars.collateralAtoken.burn(user, receiver, vars.maxCollateralToLiquidate); + if (vars.userCollateralBalance == vars.maxCollateralToLiquidate) { + usersConfig[user].setUsingAsCollateral(collateralReserve.index, false); + } + address principalAToken = debtReserve.aTokenAddress; // Notifies the receiver to proceed, sending as param the underlying already transferred From 75c5c7c615aa1700fff0933f6c31c9ec72ff0774 Mon Sep 17 00:00:00 2001 From: eboado Date: Wed, 9 Sep 2020 14:22:35 +0200 Subject: [PATCH 08/14] - Added test for user's usage as collateral on repayWithCollateral(). --- test/repay-with-collateral.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index d7f9d77f..d4bac478 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -171,6 +171,8 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { expectedCollateralLiquidated.toString() ) ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; }); it('User 3 deposits WETH and borrows USDC at Variable', async () => { @@ -271,6 +273,8 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { ), 'INVALID_COLLATERAL_POSITION' ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; }); it('Revert expected. User 3 tries to repay with his collateral a currency he havent borrow', async () => { @@ -426,6 +430,8 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { usdc.address, user.address ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; }); it('User 4 tries to repay a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { @@ -517,5 +523,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { ); expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal(0); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.false; }); }); From f1bd569346e234b8d443020110d0edf757d17425 Mon Sep 17 00:00:00 2001 From: David Racero Date: Wed, 9 Sep 2020 14:47:33 +0200 Subject: [PATCH 09/14] Added reentrancy test and full amount flash liquidation test --- .../flash-liquidation-with-collateral.spec.ts | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts index 5dd2122f..30ca5c19 100644 --- a/test/flash-liquidation-with-collateral.spec.ts +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -362,6 +362,106 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn await oracle.setAssetPrice(usdc.address, usdcPrice); }); + it('User 5 liquidates all the USDC loan of User 3 by swapping his WETH collateral', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[2]; + const liquidator = users[4]; + // Sets USDC Price higher to decrease health factor below 1 + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + await oracle.setAssetPrice( + usdc.address, + new BigNumber(usdcPrice.toString()).multipliedBy(1.35).toFixed(0) + ); + + const userGlobalData = await pool.getUserAccountData(user.address); + + expect(userGlobalData.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + const amountToRepay = usdcReserveDataBefore.totalBorrowsVariable.toFixed(0); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await waitForTx( + await pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString(), + 'INVALID_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + + // Resets USDC Price + await oracle.setAssetPrice(usdc.address, usdcPrice); + }); + it('User 2 deposit WETH and borrows DAI at Variable', async () => { const {pool, weth, dai, users, oracle} = testEnv; const user = users[1]; @@ -387,6 +487,50 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn await pool.connect(user.signer).borrow(dai.address, amountDAIToBorrow, 2, 0); }); + + it('It is not possible to do reentrancy on repayWithCollateral()', async () => { + const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + const user = users[1]; + const liquidator = users[4]; + + // Sets DAI Price higher to decrease health factor below 1 + const daiPrice = await oracle.getAssetPrice(dai.address); + + await oracle.setAssetPrice( + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(1.4).toFixed(0) + ); + + const {reserveData: daiReserveDataBefore} = await getContractsData( + dai.address, + user.address, + testEnv + ); + + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.toString(); + + await waitForTx(await mockSwapAdapter.setTryReentrancy(true)); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await expect( + pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('FAILED_REPAY_WITH_COLLATERAL'); + + // Resets DAI Price + await oracle.setAssetPrice(dai.address, daiPrice); + // Resets mock + await waitForTx(await mockSwapAdapter.setTryReentrancy(false)); + }); + it('User 5 tries to liquidate User 2 DAI Variable loan using his WETH collateral, with good HF', async () => { const {pool, weth, dai, users, mockSwapAdapter} = testEnv; const user = users[1]; From 398335124f1620384385a2a5e1099e9accfa47c5 Mon Sep 17 00:00:00 2001 From: David Racero Date: Wed, 9 Sep 2020 15:43:02 +0200 Subject: [PATCH 10/14] Added collateral test to flash liquidation --- deployed-contracts.json | 11 +++- package.json | 1 + .../flash-liquidation-with-collateral.spec.ts | 61 +++++++++++-------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/deployed-contracts.json b/deployed-contracts.json index 8286b8f5..31972f1b 100644 --- a/deployed-contracts.json +++ b/deployed-contracts.json @@ -174,7 +174,7 @@ }, "WalletBalanceProvider": { "buidlerevm": { - "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2", + "address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -414,7 +414,7 @@ }, "AaveProtocolTestHelpers": { "buidlerevm": { - "address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10" + "address": "0x2cfcA5785261fbC88EFFDd46fCFc04c22525F9e4" }, "localhost": { "address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460" @@ -489,5 +489,10 @@ "address": "0x8733AfE8174BA7c04c6CD694bD673294079b7E10", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } + }, + "MockSwapAdapter": { + "buidlerevm": { + "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2" + } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 94ec647c..30e7d371 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "buidler test", "test-scenarios": "buidler test test/__setup.spec.ts test/scenario.spec.ts", "test-repay-with-collateral": "buidler test test/__setup.spec.ts test/repay-with-collateral.spec.ts", + "test-liquidate-with-collateral": "buidler test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", "dev:coverage": "buidler coverage", "dev:deployment": "buidler dev-deployment", "dev:deployExample": "buidler deploy-Example", diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts index 30ca5c19..2ffaa936 100644 --- a/test/flash-liquidation-with-collateral.spec.ts +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -11,6 +11,7 @@ import {waitForTx} from './__setup.spec'; import {timeLatest} from '../helpers/misc-utils'; import {tEthereumAddress, ProtocolErrors} from '../helpers/types'; import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; +import {formatUnits, formatEther} from 'ethers/lib/utils'; const {expect} = require('chai'); const {parseUnits, parseEther} = ethers.utils; @@ -323,6 +324,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn ), 'INVALID_COLLATERAL_POSITION' ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; // Resets USDC Price await oracle.setAssetPrice(usdc.address, usdcPrice); @@ -651,6 +653,8 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn expectedCollateralLiquidated.toString() ) ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; + // Resets DAI price await oracle.setAssetPrice(dai.address, daiPrice); }); @@ -736,14 +740,14 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn ); }); - it.skip('WIP Liquidator tries to repay 4 user a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { - const {pool, weth, dai, users, mockSwapAdapter, oracle} = testEnv; + it('Liquidator tries to repay 4 user a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { + const {pool, weth, dai, usdc, users, mockSwapAdapter, oracle} = testEnv; const user = users[3]; const liquidator = users[5]; const amountToDepositWeth = parseEther('0.1'); const amountToDepositDAI = parseEther('500'); - const amountToBorrowVariable = parseEther('80'); + const amountToBorrowVariable = parseUnits('80', '6'); await weth.connect(user.signer).mint(amountToDepositWeth); await dai.connect(user.signer).mint(amountToDepositDAI); @@ -753,9 +757,9 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn await pool.connect(user.signer).deposit(weth.address, amountToDepositWeth, '0'); await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, '0'); - await pool.connect(user.signer).borrow(dai.address, amountToBorrowVariable, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0); - const amountToRepay = parseEther('80'); + const amountToRepay = amountToBorrowVariable; const {userData: wethUserDataBefore} = await getContractsData( weth.address, @@ -763,26 +767,28 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn testEnv ); - const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( - dai.address, - user.address, - testEnv - ); - const wethPrice = await oracle.getAssetPrice(weth.address); + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + // Set HF below 1 + const daiPrice = await oracle.getAssetPrice(dai.address); await oracle.setAssetPrice( - weth.address, - new BigNumber(wethPrice.toString()).multipliedBy(0.1).toFixed(0) + dai.address, + new BigNumber(daiPrice.toString()).multipliedBy(0.1).toFixed(0) ); const userGlobalDataPrior = await pool.getUserAccountData(user.address); expect(userGlobalDataPrior.healthFactor.toString()).to.be.bignumber.lt(oneEther, INVALID_HF); + + // Execute liquidation await mockSwapAdapter.setAmountToReturn(amountToRepay); await waitForTx( await pool .connect(liquidator.signer) .repayWithCollateral( weth.address, - dai.address, + usdc.address, user.address, amountToRepay, mockSwapAdapter.address, @@ -797,16 +803,20 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn testEnv ); - const {userData: daiUserDataAfter} = await getContractsData(dai.address, user.address, testEnv); + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); const collateralPrice = await oracle.getAssetPrice(weth.address); - const principalPrice = await oracle.getAssetPrice(dai.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); const collateralConfig = await pool.getReserveConfigurationData(weth.address); const collateralDecimals = collateralConfig.decimals.toString(); const principalDecimals = ( - await pool.getReserveConfigurationData(dai.address) + await pool.getReserveConfigurationData(usdc.address) ).decimals.toString(); const collateralLiquidationBonus = collateralConfig.liquidationBonus.toString(); @@ -820,20 +830,23 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn .decimalPlaces(0, BigNumber.ROUND_DOWN); const expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( - daiReserveDataBefore, - daiUserDataBefore, + usdcReserveDataBefore, + usdcUserDataBefore, new BigNumber(repayWithCollateralTimestamp) - ).minus(daiUserDataBefore.currentVariableDebt); + ).minus(usdcUserDataBefore.currentVariableDebt); - expect(daiUserDataAfter.currentVariableDebt).to.be.bignumber.equal( - new BigNumber(daiUserDataBefore.currentVariableDebt) + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.equal( + new BigNumber(usdcUserDataBefore.currentVariableDebt) .minus(expectedDebtCovered.toString()) .plus(expectedVariableDebtIncrease), 'INVALID_VARIABLE_DEBT_POSITION' ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.false; + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal(0); - // Resets WETH Price - await oracle.setAssetPrice(weth.address, wethPrice); + + // Resets DAI Price + await oracle.setAssetPrice(dai.address, daiPrice); }); }); From 23b7226a73865b055915624ec957d1cb8aed407b Mon Sep 17 00:00:00 2001 From: David Racero Date: Wed, 9 Sep 2020 16:35:49 +0200 Subject: [PATCH 11/14] Fix bignumber global test config --- test/__setup.spec.ts | 4 ++-- test/liquidation-underlying.spec.ts | 6 ++++++ test/scenario.spec.ts | 9 +++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 689ecf3a..e2db2752 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -504,8 +504,8 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); - const mockSwapAdapter = await deployMockSwapAdapter(addressesProvider.address) - await insertContractAddressInDb(eContractid.MockSwapAdapter, mockSwapAdapter.address) + const mockSwapAdapter = await deployMockSwapAdapter(addressesProvider.address); + await insertContractAddressInDb(eContractid.MockSwapAdapter, mockSwapAdapter.address); await deployWalletBalancerProvider(addressesProvider.address); diff --git a/test/liquidation-underlying.spec.ts b/test/liquidation-underlying.spec.ts index 676c9c26..9a58965e 100644 --- a/test/liquidation-underlying.spec.ts +++ b/test/liquidation-underlying.spec.ts @@ -19,6 +19,12 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', USER_DID_NOT_BORROW_SPECIFIED, THE_COLLATERAL_CHOSEN_CANNOT_BE_LIQUIDATED, } = ProtocolErrors; + 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('LIQUIDATION - Deposits WETH, borrows DAI', async () => { const {dai, weth, users, pool, oracle} = testEnv; diff --git a/test/scenario.spec.ts b/test/scenario.spec.ts index 5d449d76..54fe7433 100644 --- a/test/scenario.spec.ts +++ b/test/scenario.spec.ts @@ -8,8 +8,6 @@ import {getReservesConfigByPool} from '../helpers/constants'; import {AavePools, iAavePoolAssets, IReserveParams} from '../helpers/types'; import {executeStory} from './helpers/scenario-engine'; -BigNumber.config({DECIMAL_PLACES: 0, ROUNDING_MODE: BigNumber.ROUND_DOWN}); - const scenarioFolder = './test/helpers/scenarios/'; const selectedScenarios: string[] = []; @@ -21,12 +19,19 @@ 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}); + actionsConfiguration.skipIntegrityCheck = false; //set this to true to execute solidity-coverage calculationsConfiguration.reservesParams = >( getReservesConfigByPool(AavePools.proto) ); }); + after('Reset', () => { + // Reset BigNumber + BigNumber.config({DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP}); + }); for (const story of scenario.stories) { it(story.description, async () => { From 113c481512dca429c12ee50be3a1d25f542f5830 Mon Sep 17 00:00:00 2001 From: David Racero Date: Wed, 9 Sep 2020 21:24:20 +0200 Subject: [PATCH 12/14] Fixed coverage random failing tests. Added coverage network and minimal config. --- .solcover.js | 2 +- buidler.config.ts | 3 +++ helpers/contracts-helpers.ts | 11 ++++------- package.json | 2 +- .../flash-liquidation-with-collateral.spec.ts | 19 +++++++++++-------- test/liquidation-atoken.spec.ts | 9 +++++---- test/liquidation-underlying.spec.ts | 17 ++++++++++------- 7 files changed, 35 insertions(+), 28 deletions(-) diff --git a/.solcover.js b/.solcover.js index b63d9c64..9235302a 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,7 +2,7 @@ const accounts = require(`./test-wallets.js`).accounts; module.exports = { client: require('ganache-cli'), - skipFiles: [], + skipFiles: ['./mocks', './interfaces'], mocha: { enableTimeouts: false, }, diff --git a/buidler.config.ts b/buidler.config.ts index d21f5293..be2d98f4 100644 --- a/buidler.config.ts +++ b/buidler.config.ts @@ -57,6 +57,9 @@ const config: any = { timeout: 0, }, networks: { + coverage: { + url: 'http://localhost:8555', + }, kovan: getCommonNetworkConfig(eEthereumNetwork.kovan, 42), ropsten: getCommonNetworkConfig(eEthereumNetwork.ropsten, 3), main: getCommonNetworkConfig(eEthereumNetwork.main, 1), diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 909676a1..4bebd442 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -31,11 +31,11 @@ import BigNumber from 'bignumber.js'; import {Ierc20Detailed} from '../types/Ierc20Detailed'; import {StableDebtToken} from '../types/StableDebtToken'; import {VariableDebtToken} from '../types/VariableDebtToken'; -import { MockSwapAdapter } from '../types/MockSwapAdapter'; +import {MockSwapAdapter} from '../types/MockSwapAdapter'; export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => { const currentNetwork = BRE.network.name; - if (currentNetwork !== 'buidlerevm' && currentNetwork !== 'soliditycoverage') { + if (currentNetwork !== 'buidlerevm' && !currentNetwork.includes('coverage')) { console.log(`*** ${contractId} ***\n`); console.log(`Network: ${currentNetwork}`); console.log(`tx: ${contractInstance.deployTransaction.hash}`); @@ -214,9 +214,7 @@ export const deployMockFlashLoanReceiver = async (addressesProvider: tEthereumAd ]); export const deployMockSwapAdapter = async (addressesProvider: tEthereumAddress) => - await deployContract(eContractid.MockSwapAdapter, [ - addressesProvider, - ]); + await deployContract(eContractid.MockSwapAdapter, [addressesProvider]); export const deployWalletBalancerProvider = async (addressesProvider: tEthereumAddress) => await deployContract(eContractid.WalletBalanceProvider, [ @@ -397,8 +395,7 @@ export const getMockSwapAdapter = async (address?: tEthereumAddress) => { return await getContract( eContractid.MockSwapAdapter, address || - (await getDb().get(`${eContractid.MockSwapAdapter}.${BRE.network.name}`).value()) - .address + (await getDb().get(`${eContractid.MockSwapAdapter}.${BRE.network.name}`).value()).address ); }; diff --git a/package.json b/package.json index 30e7d371..fd71a369 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test-scenarios": "buidler test test/__setup.spec.ts test/scenario.spec.ts", "test-repay-with-collateral": "buidler test test/__setup.spec.ts test/repay-with-collateral.spec.ts", "test-liquidate-with-collateral": "buidler test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", - "dev:coverage": "buidler coverage", + "dev:coverage": "buidler coverage --network coverage", "dev:deployment": "buidler dev-deployment", "dev:deployExample": "buidler deploy-Example", "dev:prettier": "prettier --write .", diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts index 2ffaa936..f74e442f 100644 --- a/test/flash-liquidation-with-collateral.spec.ts +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -8,10 +8,11 @@ import { } from './helpers/utils/calculations'; import {getContractsData} from './helpers/actions'; import {waitForTx} from './__setup.spec'; -import {timeLatest} from '../helpers/misc-utils'; +import {timeLatest, BRE, increaseTime} from '../helpers/misc-utils'; import {tEthereumAddress, ProtocolErrors} from '../helpers/types'; import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; import {formatUnits, formatEther} from 'ethers/lib/utils'; +import {buidlerArguments} from '@nomiclabs/buidler'; const {expect} = require('chai'); const {parseUnits, parseEther} = ethers.utils; @@ -467,7 +468,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn it('User 2 deposit WETH and borrows DAI at Variable', async () => { const {pool, weth, dai, users, oracle} = testEnv; const user = users[1]; - const amountToDeposit = ethers.utils.parseEther('1'); + const amountToDeposit = ethers.utils.parseEther('2'); await weth.connect(user.signer).mint(amountToDeposit); @@ -483,7 +484,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn dai.address, new BigNumber(userGlobalData.availableBorrowsETH.toString()) .div(daiPrice.toString()) - .multipliedBy(0.95) + .multipliedBy(0.9) .toFixed(0) ); @@ -591,7 +592,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn ); // First half - const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.dividedBy(2).toString(); + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.multipliedBy(0.6).toString(); await mockSwapAdapter.setAmountToReturn(amountToRepay); await waitForTx( @@ -675,11 +676,12 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn testEnv ); + await increaseTime(1000); // Repay the remaining DAI const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.toString(); await mockSwapAdapter.setAmountToReturn(amountToRepay); - await waitForTx( + const receipt = await waitForTx( await pool .connect(user.signer) .repayWithCollateral( @@ -691,7 +693,8 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn '0x' ) ); - const repayWithCollateralTimestamp = await timeLatest(); + const repayWithCollateralTimestamp = (await BRE.ethers.provider.getBlock(receipt.blockNumber)) + .timestamp; const {userData: wethUserDataAfter} = await getContractsData( weth.address, @@ -733,11 +736,11 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn .toString() ); - expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + expect( new BigNumber(wethUserDataBefore.currentATokenBalance).minus( expectedCollateralLiquidated.toString() ) - ); + ).to.be.bignumber.equal(wethUserDataAfter.currentATokenBalance); }); it('Liquidator tries to repay 4 user a bigger amount that what can be swapped of a particular collateral, repaying only the maximum allowed by that collateral', async () => { diff --git a/test/liquidation-atoken.spec.ts b/test/liquidation-atoken.spec.ts index 6e4af1e1..b21e20d9 100644 --- a/test/liquidation-atoken.spec.ts +++ b/test/liquidation-atoken.spec.ts @@ -192,7 +192,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => ); //the liquidity index of the principal reserve needs to be bigger than the index before - expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gt( + expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( daiReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); @@ -213,6 +213,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => const {users, pool, usdc, oracle, weth} = testEnv; const depositor = users[3]; const borrower = users[4]; + //mints USDC to depositor await usdc .connect(depositor.signer) @@ -246,7 +247,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => usdc.address, new BigNumber(userGlobalData.availableBorrowsETH.toString()) .div(usdcPrice.toString()) - .multipliedBy(0.95) + .multipliedBy(0.9502) .toFixed(0) ); @@ -274,7 +275,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => const ethReserveDataBefore = await pool.getReserveData(weth.address); const amountToLiquidate = new BigNumber(userReserveDataBefore.currentStableDebt.toString()) - .div(2) + .multipliedBy(0.5) .toFixed(0); await pool.liquidationCall( @@ -328,7 +329,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => ); //the liquidity index of the principal reserve needs to be bigger than the index before - expect(usdcReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gt( + expect(usdcReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( usdcReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); diff --git a/test/liquidation-underlying.spec.ts b/test/liquidation-underlying.spec.ts index 9a58965e..19c821f1 100644 --- a/test/liquidation-underlying.spec.ts +++ b/test/liquidation-underlying.spec.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js'; -import {BRE} from '../helpers/misc-utils'; +import {BRE, increaseTime} from '../helpers/misc-utils'; import {APPROVAL_AMOUNT_LENDING_POOL, oneEther} from '../helpers/constants'; import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; import {makeSuite} from './helpers/make-suite'; @@ -114,6 +114,8 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', const amountToLiquidate = userReserveDataBefore.currentStableDebt.div(2).toFixed(0); + await increaseTime(100); + const tx = await pool .connect(liquidator.signer) .liquidationCall(weth.address, dai.address, borrower.address, amountToLiquidate, false); @@ -161,7 +163,7 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', ); //the liquidity index of the principal reserve needs to be bigger than the index before - expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gt( + expect(daiReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( daiReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); @@ -227,7 +229,7 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', usdc.address, new BigNumber(userGlobalData.availableBorrowsETH.toString()) .div(usdcPrice.toString()) - .multipliedBy(0.95) + .multipliedBy(0.9502) .toFixed(0) ); @@ -255,10 +257,11 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', const usdcReserveDataBefore = await pool.getReserveData(usdc.address); const ethReserveDataBefore = await pool.getReserveData(weth.address); - const amountToLiquidate = new BigNumber(userReserveDataBefore.currentStableDebt.toString()) + const amountToLiquidate = BRE.ethers.BigNumber.from( + userReserveDataBefore.currentStableDebt.toString() + ) .div(2) - .decimalPlaces(0, BigNumber.ROUND_DOWN) - .toFixed(0); + .toString(); await pool .connect(liquidator.signer) @@ -303,7 +306,7 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', ); //the liquidity index of the principal reserve needs to be bigger than the index before - expect(usdcReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gt( + expect(usdcReserveDataAfter.liquidityIndex.toString()).to.be.bignumber.gte( usdcReserveDataBefore.liquidityIndex.toString(), 'Invalid liquidity index' ); From 831bc3d0ebafecac25a6dd641a1e300d51702103 Mon Sep 17 00:00:00 2001 From: David Racero Date: Thu, 10 Sep 2020 13:05:26 +0200 Subject: [PATCH 13/14] Added tests to check repay with collateral when is disabled --- .../flash-liquidation-with-collateral.spec.ts | 87 +++++++++++++- test/repay-with-collateral.spec.ts | 112 ++++++++++++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts index 220921bf..87edf6d6 100644 --- a/test/flash-liquidation-with-collateral.spec.ts +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -17,7 +17,7 @@ const {expect} = require('chai'); const {parseUnits, parseEther} = ethers.utils; makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEnv) => { - const {INVALID_HF} = ProtocolErrors; + const {INVALID_HF, COLLATERAL_CANNOT_BE_LIQUIDATED} = ProtocolErrors; it('User 1 provides some liquidity for others to borrow', async () => { const {pool, weth, dai, usdc} = testEnv; @@ -828,4 +828,89 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn // Resets DAI Price await oracle.setAssetPrice(dai.address, daiPrice); }); + + it('User 5 deposits WETH and DAI, then borrows USDC at Variable, then disables WETH as collateral', async () => { + const {pool, weth, dai, usdc, users} = testEnv; + const user = users[4]; + const amountWETHToDeposit = parseEther('10'); + const amountDAIToDeposit = parseEther('60'); + const amountToBorrow = parseUnits('65', 6); + + await weth.connect(user.signer).mint(amountWETHToDeposit); + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool.connect(user.signer).deposit(weth.address, amountWETHToDeposit, '0'); + + await dai.connect(user.signer).mint(amountDAIToDeposit); + await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool.connect(user.signer).deposit(dai.address, amountDAIToDeposit, '0'); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + }); + + it('Liquidator tries to liquidates User 5 USDC loan by swapping his WETH collateral, should revert due WETH collateral disabled', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[4]; + const liquidator = users[5]; + + const amountToRepay = parseUnits('65', 6); + + // User 5 Disable WETH as collateral + await pool.connect(user.signer).setUserUseReserveAsCollateral(weth.address, false); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + expect(wethUserDataBefore.usageAsCollateralEnabled).to.be.false; + + // Liquidator should NOT be able to liquidate himself with WETH, even if is disabled + await mockSwapAdapter.setAmountToReturn(amountToRepay); + await expect( + pool + .connect(liquidator.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith(COLLATERAL_CANNOT_BE_LIQUIDATED); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .plus(expectedVariableDebtIncrease) + .toString(), + 'INVALID_DEBT_POSITION' + ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.false; + }); }); diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index 9896b81c..2fbe66fc 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -10,6 +10,7 @@ import {getContractsData} from './helpers/actions'; import {waitForTx} from './__setup.spec'; import {timeLatest} from '../helpers/misc-utils'; import {tEthereumAddress} from '../helpers/types'; +import {parse} from 'path'; const {expect} = require('chai'); const {parseUnits, parseEther} = ethers.utils; @@ -525,4 +526,115 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.false; }); + + it('User 5 deposits WETH and DAI, then borrows USDC at Variable, then disables WETH as collateral', async () => { + const {pool, weth, dai, usdc, users} = testEnv; + const user = users[4]; + const amountWETHToDeposit = parseEther('10'); + const amountDAIToDeposit = parseEther('120'); + const amountToBorrow = parseUnits('65', 6); + + await weth.connect(user.signer).mint(amountWETHToDeposit); + await weth.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool.connect(user.signer).deposit(weth.address, amountWETHToDeposit, '0'); + + await dai.connect(user.signer).mint(amountDAIToDeposit); + await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool.connect(user.signer).deposit(dai.address, amountDAIToDeposit, '0'); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + }); + + it('User 5 tries to repay his USDC loan by swapping his WETH collateral, should not revert even with WETH collateral disabled', async () => { + const {pool, weth, usdc, users, mockSwapAdapter, oracle} = testEnv; + const user = users[4]; + + const amountToRepay = parseUnits('65', 6); + + // Disable WETH as collateral + await pool.connect(user.signer).setUserUseReserveAsCollateral(weth.address, false); + + const {userData: wethUserDataBefore} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const { + reserveData: usdcReserveDataBefore, + userData: usdcUserDataBefore, + } = await getContractsData(usdc.address, user.address, testEnv); + + expect(wethUserDataBefore.usageAsCollateralEnabled).to.be.false; + + // User 5 should be able to liquidate himself with WETH, even if is disabled + await mockSwapAdapter.setAmountToReturn(amountToRepay); + expect( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + usdc.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = await timeLatest(); + + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + user.address, + testEnv + ); + + const {userData: usdcUserDataAfter} = await getContractsData( + usdc.address, + user.address, + testEnv + ); + + const collateralPrice = await oracle.getAssetPrice(weth.address); + const principalPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = ( + await pool.getReserveConfigurationData(weth.address) + ).decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.address) + ).decimals.toString(); + + const expectedCollateralLiquidated = new BigNumber(principalPrice.toString()) + .times(new BigNumber(amountToRepay.toString()).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 expectedVariableDebtIncrease = calcExpectedVariableDebtTokenBalance( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.currentVariableDebt); + + expect(usdcUserDataAfter.currentVariableDebt).to.be.bignumber.almostEqual( + new BigNumber(usdcUserDataBefore.currentVariableDebt) + .minus(amountToRepay.toString()) + .plus(expectedVariableDebtIncrease) + .toString(), + 'INVALID_DEBT_POSITION' + ); + + expect(wethUserDataAfter.currentATokenBalance).to.be.bignumber.equal( + new BigNumber(wethUserDataBefore.currentATokenBalance).minus( + expectedCollateralLiquidated.toString() + ), + 'INVALID_COLLATERAL_POSITION' + ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.false; + }); }); From 167f02533f858f74ea0f140839a21ec76ca9c1b6 Mon Sep 17 00:00:00 2001 From: David Racero <4266635-kartojal@users.noreply.gitlab.com> Date: Thu, 10 Sep 2020 13:30:57 +0000 Subject: [PATCH 14/14] Delete "if" condition due always will be true. Delete unreachable "else" code at repayWithCollateral. --- contracts/lendingpool/LendingPoolLiquidationManager.sol | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index f63e1c82..6c7b340b 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -361,14 +361,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { ); } - if (msg.sender == user || vars.healthFactor < GenericLogic.HEALTH_FACTOR_CRITICAL_THRESHOLD) { - vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); - } else { - vars.maxPrincipalAmountToLiquidate = vars - .userStableDebt - .add(vars.userVariableDebt) - .percentMul(LIQUIDATION_CLOSE_FACTOR_PERCENT); - } + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); vars.actualAmountToLiquidate = principalAmount > vars.maxPrincipalAmountToLiquidate ? vars.maxPrincipalAmountToLiquidate