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/contracts/flashloan/base/FlashLoanReceiverBase.sol b/contracts/flashloan/base/FlashLoanReceiverBase.sol index f96609d2..a648508c 100644 --- a/contracts/flashloan/base/FlashLoanReceiverBase.sol +++ b/contracts/flashloan/base/FlashLoanReceiverBase.sol @@ -19,5 +19,4 @@ abstract contract FlashLoanReceiverBase is IFlashLoanReceiver { } receive() external payable {} - } diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 239787ed..b14f5cb8 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -220,6 +220,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/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/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index a4f62ce8..517dc997 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -51,6 +51,8 @@ contract LendingPool is VersionedInitializable, ILendingPool { address[] internal _reservesList; + bool internal _flashLiquidationLocked; + /** * @dev only lending pools configurator can use functions affected by this modifier **/ @@ -199,6 +201,16 @@ contract LendingPool is VersionedInitializable, ILendingPool { uint256 rateMode, address onBehalfOf ) external override { + _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); @@ -239,9 +251,9 @@ contract LendingPool is 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); } /** @@ -411,7 +423,55 @@ contract LendingPool is VersionedInitializable, ILendingPool { } /** - * @dev allows smart contracts to access the liquidity of the pool within one transaction, + * @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 { + require(!_flashLiquidationLocked, Errors.REENTRANCY_NOT_ALLOWED); + _flashLiquidationLocked = true; + + 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, Errors.FAILED_REPAY_WITH_COLLATERAL); + + (uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); + + if (returnCode != 0) { + revert(string(abi.encodePacked(returnMessage))); + } + + _flashLiquidationLocked = false; + } + + /** + * @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 * that must be kept into consideration. For further details please visit https://developers.aave.com * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the IFlashLoanReceiver interface. diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 12ecf777..6c7b340b 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -19,6 +19,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 {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; /** @@ -35,6 +36,9 @@ contract LendingPoolLiquidationManager is VersionedInitializable { 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; @@ -42,6 +46,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { address[] internal reservesList; + bool internal _flashLiquidationLocked; + uint256 internal constant LIQUIDATION_CLOSE_FACTOR_PERCENT = 5000; /** @@ -64,6 +70,24 @@ contract LendingPoolLiquidationManager is VersionedInitializable { 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, @@ -270,6 +294,157 @@ contract LendingPoolLiquidationManager is VersionedInitializable { return (uint256(LiquidationErrors.NO_ERROR), Errors.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), + Errors.HEALTH_FACTOR_NOT_BELOW_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), + Errors.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), + Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER + ); + } + + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); + + 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); + + 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 + ISwapAdapter(receiver).executeOperation( + collateral, + principal, + vars.maxCollateralToLiquidate, + address(this), + params + ); + + //updating debt reserve + debtReserve.updateCumulativeIndexesAndTimestamp(); + debtReserve.updateInterestRates(principal, principalAToken, vars.actualAmountToLiquidate, 0); + IERC20(principal).transferFrom(receiver, principalAToken, 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), Errors.NO_ERRORS); + } + struct AvailableCollateralToLiquidateLocalVars { uint256 userCompoundedBorrowBalance; uint256 liquidationBonus; diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 974aa5e1..b7598eec 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -38,6 +38,8 @@ library Errors { string public constant INCONSISTENT_PROTOCOL_ACTUAL_BALANCE = '26'; // 'The actual balance of the protocol is inconsistent' string public constant CALLER_NOT_LENDING_POOL_CONFIGURATOR = '27'; // 'The actual balance of the protocol is inconsistent' string public constant INVALID_FLASHLOAN_MODE = '43'; //Invalid flashloan mode selected + string public constant REENTRANCY_NOT_ALLOWED = '52'; + string public constant FAILED_REPAY_WITH_COLLATERAL = '53'; // require error messages - aToken string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool' @@ -70,7 +72,7 @@ library Errors { string public constant NO_ERRORS = '42'; // 'No errors' //require error messages - Math libraries - string public constant MULTIPLICATION_OVERFLOW = '44'; - string public constant ADDITION_OVERFLOW = '45'; + string public constant MULTIPLICATION_OVERFLOW = '44'; + string public constant ADDITION_OVERFLOW = '45'; string public constant DIVISION_BY_ZERO = '46'; } 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; diff --git a/contracts/libraries/logic/ReserveLogic.sol b/contracts/libraries/logic/ReserveLogic.sol index 607c8c63..06d08499 100644 --- a/contracts/libraries/logic/ReserveLogic.sol +++ b/contracts/libraries/logic/ReserveLogic.sol @@ -68,7 +68,6 @@ library ReserveLogic { uint40 lastUpdateTimestamp; //the index of the reserve in the list of the active reserves uint8 index; - } /** @@ -122,7 +121,7 @@ library ReserveLogic { * a formal specification. * @param reserve the reserve object **/ - function updateCumulativeIndexesAndTimestamp(ReserveData storage reserve) internal { + function updateCumulativeIndexesAndTimestamp(ReserveData storage reserve) internal { uint256 currentLiquidityRate = reserve.currentLiquidityRate; //only cumulating if there is any income being produced @@ -144,10 +143,8 @@ library ReserveLogic { reserve.currentVariableBorrowRate, lastUpdateTimestamp ); - index = cumulatedVariableBorrowInterest.rayMul( - reserve.lastVariableBorrowIndex - ); - require(index < (1 << 128), Errors.VARIABLE_BORROW_INDEX_OVERFLOW); + index = cumulatedVariableBorrowInterest.rayMul(reserve.lastVariableBorrowIndex); + require(index < (1 << 128), Errors.VARIABLE_BORROW_INDEX_OVERFLOW); reserve.lastVariableBorrowIndex = uint128(index); } } @@ -172,9 +169,7 @@ library ReserveLogic { uint256 result = amountToLiquidityRatio.add(WadRayMath.ray()); - result = result.rayMul( - reserve.lastLiquidityIndex - ); + result = result.rayMul(reserve.lastLiquidityIndex); require(result < (1 << 128), Errors.LIQUIDITY_INDEX_OVERFLOW); reserve.lastLiquidityIndex = uint128(result); @@ -217,6 +212,7 @@ library ReserveLogic { uint256 newStableRate; uint256 newVariableRate; } + /** * @dev Updates the reserve current stable borrow rate Rf, the current variable borrow rate Rv and the current liquidity rate Rl. * Also updates the lastUpdateTimestamp value. Please refer to the whitepaper for further information. @@ -234,7 +230,8 @@ library ReserveLogic { UpdateInterestRatesLocalVars memory vars; vars.stableDebtTokenAddress = reserve.stableDebtTokenAddress; - vars.currentAvgStableRate = IStableDebtToken(vars.stableDebtTokenAddress).getAverageStableRate(); + vars.currentAvgStableRate = IStableDebtToken(vars.stableDebtTokenAddress) + .getAverageStableRate(); vars.availableLiquidity = IERC20(reserveAddress).balanceOf(aTokenAddress); ( @@ -248,9 +245,9 @@ library ReserveLogic { IERC20(reserve.variableDebtTokenAddress).totalSupply(), vars.currentAvgStableRate ); - require(vars.newLiquidityRate < (1 << 128), "ReserveLogic: Liquidity rate overflow"); - require(vars.newStableRate < (1 << 128), "ReserveLogic: Stable borrow rate overflow"); - require(vars.newVariableRate < (1 << 128), "ReserveLogic: Variable borrow rate overflow"); + require(vars.newLiquidityRate < (1 << 128), 'ReserveLogic: Liquidity rate overflow'); + require(vars.newStableRate < (1 << 128), 'ReserveLogic: Stable borrow rate overflow'); + require(vars.newVariableRate < (1 << 128), 'ReserveLogic: Variable borrow rate overflow'); reserve.currentLiquidityRate = uint128(vars.newLiquidityRate); reserve.currentStableBorrowRate = uint128(vars.newStableRate); diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 7a640458..9be3f828 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -321,10 +321,10 @@ library ValidationLogic { } /** - * @dev validates a flashloan action - * @param mode the flashloan mode (0 = classic flashloan, 1 = open a stable rate loan, 2 = open a variable rate loan) - * @param premium the premium paid on the flashloan - **/ + * @dev validates a flashloan action + * @param mode the flashloan mode (0 = classic flashloan, 1 = open a stable rate loan, 2 = open a variable rate loan) + * @param premium the premium paid on the flashloan + **/ function validateFlashloan(uint256 mode, uint256 premium) internal pure { require(premium > 0, Errors.REQUESTED_AMOUNT_TOO_SMALL); require(mode <= uint256(ReserveLogic.InterestRateMode.VARIABLE), Errors.INVALID_FLASHLOAN_MODE); diff --git a/contracts/libraries/math/MathUtils.sol b/contracts/libraries/math/MathUtils.sol index 2d9c76a4..13cec3fb 100644 --- a/contracts/libraries/math/MathUtils.sol +++ b/contracts/libraries/math/MathUtils.sol @@ -55,17 +55,17 @@ library MathUtils { return WadRayMath.ray(); } - uint256 expMinusOne = exp-1; + uint256 expMinusOne = exp - 1; - uint256 expMinusTwo = exp > 2 ? exp-2 : 0; + uint256 expMinusTwo = exp > 2 ? exp - 2 : 0; - uint256 ratePerSecond = rate/SECONDS_PER_YEAR; + uint256 ratePerSecond = rate / SECONDS_PER_YEAR; uint256 basePowerTwo = ratePerSecond.rayMul(ratePerSecond); uint256 basePowerThree = basePowerTwo.rayMul(ratePerSecond); - uint256 secondTerm = exp.mul(expMinusOne).mul(basePowerTwo)/2; - uint256 thirdTerm = exp.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree)/6; + uint256 secondTerm = exp.mul(expMinusOne).mul(basePowerTwo) / 2; + uint256 thirdTerm = exp.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree) / 6; return WadRayMath.ray().add(ratePerSecond.mul(exp)).add(secondTerm).add(thirdTerm); } diff --git a/contracts/libraries/math/PercentageMath.sol b/contracts/libraries/math/PercentageMath.sol index 4d14107e..b853f1db 100644 --- a/contracts/libraries/math/PercentageMath.sol +++ b/contracts/libraries/math/PercentageMath.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.6.8; - import {Errors} from '../helpers/Errors.sol'; /** @@ -13,7 +12,6 @@ import {Errors} from '../helpers/Errors.sol'; **/ library PercentageMath { - uint256 constant PERCENTAGE_FACTOR = 1e4; //percentage plus two decimals uint256 constant HALF_PERCENT = PERCENTAGE_FACTOR / 2; @@ -24,19 +22,19 @@ library PercentageMath { * @return the percentage of value **/ function percentMul(uint256 value, uint256 percentage) internal pure returns (uint256) { - if(value == 0){ + if (value == 0) { return 0; } - - uint256 result = value*percentage; - - require(result/value == percentage, Errors.MULTIPLICATION_OVERFLOW); - - result+=HALF_PERCENT; - + + uint256 result = value * percentage; + + require(result / value == percentage, Errors.MULTIPLICATION_OVERFLOW); + + result += HALF_PERCENT; + require(result >= HALF_PERCENT, Errors.ADDITION_OVERFLOW); - return result/PERCENTAGE_FACTOR; + return result / PERCENTAGE_FACTOR; } /** @@ -48,15 +46,15 @@ library PercentageMath { function percentDiv(uint256 value, uint256 percentage) internal pure returns (uint256) { require(percentage != 0, Errors.DIVISION_BY_ZERO); uint256 halfPercentage = percentage / 2; - - uint256 result = value*PERCENTAGE_FACTOR; - require(result/PERCENTAGE_FACTOR == value, Errors.MULTIPLICATION_OVERFLOW); + uint256 result = value * PERCENTAGE_FACTOR; + + require(result / PERCENTAGE_FACTOR == value, Errors.MULTIPLICATION_OVERFLOW); result += halfPercentage; require(result >= halfPercentage, Errors.ADDITION_OVERFLOW); - return result/percentage; + return result / percentage; } } diff --git a/contracts/libraries/math/SafeMath.sol b/contracts/libraries/math/SafeMath.sol index 8ce54b91..0b251214 100644 --- a/contracts/libraries/math/SafeMath.sol +++ b/contracts/libraries/math/SafeMath.sol @@ -15,149 +15,149 @@ pragma solidity 0.6.8; * class of bugs, so it's recommended to use it always. */ library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a, 'SafeMath: addition overflow'); + /** + * @dev Returns the addition of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `+` operator. + * + * Requirements: + * - Addition cannot overflow. + */ + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, 'SafeMath: addition overflow'); - return c; + return c; + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + return sub(a, b, 'SafeMath: subtraction overflow'); + } + + /** + * @dev Returns the subtraction of two unsigned integers, reverting with custom message on + * overflow (when the result is negative). + * + * Counterpart to Solidity's `-` operator. + * + * Requirements: + * - Subtraction cannot overflow. + */ + function sub( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b <= a, errorMessage); + uint256 c = a - b; + + return c; + } + + /** + * @dev Returns the multiplication of two unsigned integers, reverting on + * overflow. + * + * Counterpart to Solidity's `*` operator. + * + * Requirements: + * - Multiplication cannot overflow. + */ + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) { + return 0; } - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return sub(a, b, 'SafeMath: subtraction overflow'); - } + uint256 c = a * b; + require(c / a == b, 'SafeMath: multiplication overflow'); - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - Subtraction cannot overflow. - */ - function sub( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b <= a, errorMessage); - uint256 c = a - b; + return c; + } - return c; - } + /** + * @dev Returns the integer division of two unsigned integers. Reverts on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div(uint256 a, uint256 b) internal pure returns (uint256) { + return div(a, b, 'SafeMath: division by zero'); + } - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) { - return 0; - } + /** + * @dev Returns the integer division of two unsigned integers. Reverts with custom message on + * division by zero. The result is rounded towards zero. + * + * Counterpart to Solidity's `/` operator. Note: this function uses a + * `revert` opcode (which leaves remaining gas untouched) while Solidity + * uses an invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function div( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + // Solidity only automatically asserts when dividing by 0 + require(b > 0, errorMessage); + uint256 c = a / b; + // assert(a == b * c + a % b); // There is no case in which this doesn't hold - uint256 c = a * b; - require(c / a == b, 'SafeMath: multiplication overflow'); + return c; + } - return c; - } + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod(uint256 a, uint256 b) internal pure returns (uint256) { + return mod(a, b, 'SafeMath: modulo by zero'); + } - /** - * @dev Returns the integer division of two unsigned integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return div(a, b, 'SafeMath: division by zero'); - } - - /** - * @dev Returns the integer division of two unsigned integers. Reverts with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function div( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0, errorMessage); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return mod(a, b, 'SafeMath: modulo by zero'); - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * Reverts with custom message when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - The divisor cannot be zero. - */ - function mod( - uint256 a, - uint256 b, - string memory errorMessage - ) internal pure returns (uint256) { - require(b != 0, errorMessage); - return a % b; - } + /** + * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), + * Reverts with custom message when dividing by zero. + * + * Counterpart to Solidity's `%` operator. This function uses a `revert` + * opcode (which leaves remaining gas untouched) while Solidity uses an + * invalid opcode to revert (consuming all remaining gas). + * + * Requirements: + * - The divisor cannot be zero. + */ + function mod( + uint256 a, + uint256 b, + string memory errorMessage + ) internal pure returns (uint256) { + require(b != 0, errorMessage); + return a % b; + } } diff --git a/contracts/libraries/math/WadRayMath.sol b/contracts/libraries/math/WadRayMath.sol index 1a1bdabd..7da5fc81 100644 --- a/contracts/libraries/math/WadRayMath.sol +++ b/contracts/libraries/math/WadRayMath.sol @@ -10,7 +10,6 @@ import {Errors} from '../helpers/Errors.sol'; **/ library WadRayMath { - uint256 internal constant WAD = 1e18; uint256 internal constant halfWAD = WAD / 2; @@ -55,20 +54,19 @@ library WadRayMath { * @return the result of a*b, in wad **/ function wadMul(uint256 a, uint256 b) internal pure returns (uint256) { - - if(a == 0){ + if (a == 0) { return 0; } - - uint256 result = a*b; - - require(result/a == b, Errors.MULTIPLICATION_OVERFLOW); - - result+=halfWAD; - + + uint256 result = a * b; + + require(result / a == b, Errors.MULTIPLICATION_OVERFLOW); + + result += halfWAD; + require(result >= halfWAD, Errors.ADDITION_OVERFLOW); - return result/WAD; + return result / WAD; } /** @@ -82,15 +80,15 @@ library WadRayMath { uint256 halfB = b / 2; - uint256 result = a*WAD; + uint256 result = a * WAD; - require(result/WAD == a, Errors.MULTIPLICATION_OVERFLOW); + require(result / WAD == a, Errors.MULTIPLICATION_OVERFLOW); result += halfB; require(result >= halfB, Errors.ADDITION_OVERFLOW); - return result/b; + return result / b; } /** @@ -100,19 +98,19 @@ library WadRayMath { * @return the result of a*b, in ray **/ function rayMul(uint256 a, uint256 b) internal pure returns (uint256) { - if(a == 0){ + if (a == 0) { return 0; } - - uint256 result = a*b; - - require(result/a == b, Errors.MULTIPLICATION_OVERFLOW); - - result+=halfRAY; - + + uint256 result = a * b; + + require(result / a == b, Errors.MULTIPLICATION_OVERFLOW); + + result += halfRAY; + require(result >= halfRAY, Errors.ADDITION_OVERFLOW); - return result/RAY; + return result / RAY; } /** @@ -126,16 +124,15 @@ library WadRayMath { uint256 halfB = b / 2; - uint256 result = a*RAY; + uint256 result = a * RAY; - require(result/RAY == a, Errors.MULTIPLICATION_OVERFLOW); + require(result / RAY == a, Errors.MULTIPLICATION_OVERFLOW); result += halfB; require(result >= halfB, Errors.ADDITION_OVERFLOW); - return result/b; - + return result / b; } /** @@ -145,10 +142,10 @@ library WadRayMath { **/ function rayToWad(uint256 a) internal pure returns (uint256) { uint256 halfRatio = WAD_RAY_RATIO / 2; - uint256 result = halfRatio+a; + uint256 result = halfRatio + a; require(result >= halfRatio, Errors.ADDITION_OVERFLOW); - return result/WAD_RAY_RATIO; + return result / WAD_RAY_RATIO; } /** @@ -157,8 +154,8 @@ library WadRayMath { * @return a converted in ray **/ function wadToRay(uint256 a) internal pure returns (uint256) { - uint256 result = a*WAD_RAY_RATIO; - require(result/WAD_RAY_RATIO == a, Errors.MULTIPLICATION_OVERFLOW); + uint256 result = a * WAD_RAY_RATIO; + require(result / WAD_RAY_RATIO == a, Errors.MULTIPLICATION_OVERFLOW); return result; } } diff --git a/contracts/misc/Address.sol b/contracts/misc/Address.sol index 2233df49..be5026db 100644 --- a/contracts/misc/Address.sol +++ b/contracts/misc/Address.sol @@ -5,57 +5,57 @@ pragma solidity 0.6.8; * @dev Collection of functions related to the address type */ library Address { - /** - * @dev Returns true if `account` is a contract. - * - * [IMPORTANT] - * ==== - * It is unsafe to assume that an address for which this function returns - * false is an externally-owned account (EOA) and not a contract. - * - * Among others, `isContract` will return false for the following - * types of addresses: - * - * - an externally-owned account - * - a contract in construction - * - an address where a contract will be created - * - an address where a contract lived, but was destroyed - * ==== - */ - function isContract(address account) internal view returns (bool) { - // According to EIP-1052, 0x0 is the value returned for not-yet created accounts - // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned - // for accounts without code, i.e. `keccak256('')` - bytes32 codehash; - bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; - // solhint-disable-next-line no-inline-assembly - assembly { - codehash := extcodehash(account) - } - return (codehash != accountHash && codehash != 0x0); + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // According to EIP-1052, 0x0 is the value returned for not-yet created accounts + // and 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 is returned + // for accounts without code, i.e. `keccak256('')` + bytes32 codehash; + bytes32 accountHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + // solhint-disable-next-line no-inline-assembly + assembly { + codehash := extcodehash(account) } + return (codehash != accountHash && codehash != 0x0); + } - /** - * @dev Replacement for Solidity's `transfer`: sends `amount` wei to - * `recipient`, forwarding all available gas and reverting on errors. - * - * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost - * of certain opcodes, possibly making contracts go over the 2300 gas limit - * imposed by `transfer`, making them unable to receive funds via - * `transfer`. {sendValue} removes this limitation. - * - * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. - * - * IMPORTANT: because control is transferred to `recipient`, care must be - * taken to not create reentrancy vulnerabilities. Consider using - * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. - */ - function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, 'Address: insufficient balance'); + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, 'Address: insufficient balance'); - // solhint-disable-next-line avoid-low-level-calls, avoid-call-value - (bool success, ) = recipient.call{value: amount}(''); - require(success, 'Address: unable to send value, recipient may have reverted'); - } + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{value: amount}(''); + require(success, 'Address: unable to send value, recipient may have reverted'); + } } diff --git a/contracts/misc/Context.sol b/contracts/misc/Context.sol index 7208e0c4..fb4afa42 100644 --- a/contracts/misc/Context.sol +++ b/contracts/misc/Context.sol @@ -12,12 +12,12 @@ pragma solidity 0.6.8; * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { - function _msgSender() internal virtual view returns (address payable) { - return msg.sender; - } + function _msgSender() internal virtual view returns (address payable) { + return msg.sender; + } - function _msgData() internal virtual view returns (bytes memory) { - this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 - return msg.data; - } + function _msgData() internal virtual view returns (bytes memory) { + this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691 + return msg.data; + } } diff --git a/contracts/misc/SafeERC20.sol b/contracts/misc/SafeERC20.sol index d1fa0c94..11416ac9 100644 --- a/contracts/misc/SafeERC20.sol +++ b/contracts/misc/SafeERC20.sol @@ -2,9 +2,9 @@ pragma solidity 0.6.8; -import {IERC20} from "../interfaces/IERC20.sol"; -import {SafeMath} from "../libraries/math/SafeMath.sol"; -import {Address} from "./Address.sol"; +import {IERC20} from '../interfaces/IERC20.sol'; +import {SafeMath} from '../libraries/math/SafeMath.sol'; +import {Address} from './Address.sol'; /** * @title SafeERC20 @@ -16,34 +16,49 @@ import {Address} from "./Address.sol"; * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. */ library SafeERC20 { - using SafeMath for uint256; - using Address for address; + using SafeMath for uint256; + using Address for address; - function safeTransfer(IERC20 token, address to, uint256 value) internal { - callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } - - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); - } - - function safeApprove(IERC20 token, address spender, uint256 value) internal { - require((value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); - } - - function callOptionalReturn(IERC20 token, bytes memory data) private { - require(address(token).isContract(), "SafeERC20: call to non-contract"); - - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory returndata) = address(token).call(data); - require(success, "SafeERC20: low-level call failed"); - - if (returndata.length > 0) { // Return data is optional - // solhint-disable-next-line max-line-length - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); - } + function safeTransfer( + IERC20 token, + address to, + uint256 value + ) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom( + IERC20 token, + address from, + address to, + uint256 value + ) internal { + callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeApprove( + IERC20 token, + address spender, + uint256 value + ) internal { + require( + (value == 0) || (token.allowance(address(this), spender) == 0), + 'SafeERC20: approve from non-zero to non-zero allowance' + ); + callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function callOptionalReturn(IERC20 token, bytes memory data) private { + require(address(token).isContract(), 'SafeERC20: call to non-contract'); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = address(token).call(data); + require(success, 'SafeERC20: low-level call failed'); + + if (returndata.length > 0) { + // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), 'SafeERC20: ERC20 operation did not succeed'); } + } } diff --git a/contracts/mocks/flashloan/MockSwapAdapter.sol b/contracts/mocks/flashloan/MockSwapAdapter.sol new file mode 100644 index 00000000..85c7d84b --- /dev/null +++ b/contracts/mocks/flashloan/MockSwapAdapter.sol @@ -0,0 +1,60 @@ +// 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 {ILendingPool} from "../../interfaces/ILendingPool.sol"; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +contract MockSwapAdapter is ISwapAdapter { + + uint256 internal _amountToReturn; + bool internal _tryReentrancy; + 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 setTryReentrancy(bool tryReentrancy) public { + _tryReentrancy = tryReentrancy; + } + + 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); + + 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 { + 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/contracts/tokenization/AToken.sol b/contracts/tokenization/AToken.sol index de178819..53870c24 100644 --- a/contracts/tokenization/AToken.sol +++ b/contracts/tokenization/AToken.sol @@ -10,7 +10,7 @@ import { } from '../libraries/openzeppelin-upgradeability/VersionedInitializable.sol'; import {IAToken} from './interfaces/IAToken.sol'; import {IERC20} from '../interfaces/IERC20.sol'; -import {SafeERC20} from "../misc/SafeERC20.sol"; +import {SafeERC20} from '../misc/SafeERC20.sol'; /** * @title Aave ERC20 AToken diff --git a/contracts/tokenization/StableDebtToken.sol b/contracts/tokenization/StableDebtToken.sol index 4fdcad09..8e5da21b 100644 --- a/contracts/tokenization/StableDebtToken.sol +++ b/contracts/tokenization/StableDebtToken.sol @@ -118,7 +118,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase { .add(vars.amountInRay.rayMul(rate)) .rayDiv(currentBalance.add(amount).wadToRay()); - require(vars.newStableRate < (1 << 128), "Debt token: stable rate overflow"); + require(vars.newStableRate < (1 << 128), 'Debt token: stable rate overflow'); _usersData[user] = vars.newStableRate; //solium-disable-next-line diff --git a/contracts/tokenization/VariableDebtToken.sol b/contracts/tokenization/VariableDebtToken.sol index 580151eb..e52f7c61 100644 --- a/contracts/tokenization/VariableDebtToken.sol +++ b/contracts/tokenization/VariableDebtToken.sol @@ -76,7 +76,7 @@ contract VariableDebtToken is DebtTokenBase, IVariableDebtToken { _mint(user, amount.add(balanceIncrease)); uint256 newUserIndex = POOL.getReserveNormalizedVariableDebt(UNDERLYING_ASSET); - require(newUserIndex < (1 << 128), "Debt token: Index overflow"); + require(newUserIndex < (1 << 128), 'Debt token: Index overflow'); _usersData[user] = newUserIndex; emit MintDebt(user, amount, previousBalance, currentBalance, balanceIncrease, newUserIndex); @@ -104,7 +104,7 @@ contract VariableDebtToken is DebtTokenBase, IVariableDebtToken { //if user not repaid everything if (currentBalance != amount) { newUserIndex = POOL.getReserveNormalizedVariableDebt(UNDERLYING_ASSET); - require(newUserIndex < (1 << 128), "Debt token: Index overflow"); + require(newUserIndex < (1 << 128), 'Debt token: Index overflow'); } _usersData[user] = newUserIndex; diff --git a/contracts/tokenization/base/DebtTokenBase.sol b/contracts/tokenization/base/DebtTokenBase.sol index 04009023..a6bd4005 100644 --- a/contracts/tokenization/base/DebtTokenBase.sol +++ b/contracts/tokenization/base/DebtTokenBase.sol @@ -5,7 +5,9 @@ import {Context} from '@openzeppelin/contracts/GSN/Context.sol'; import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol'; import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; import {ILendingPool} from '../../interfaces/ILendingPool.sol'; -import {VersionedInitializable} from '../../libraries/openzeppelin-upgradeability/VersionedInitializable.sol'; +import { + VersionedInitializable +} from '../../libraries/openzeppelin-upgradeability/VersionedInitializable.sol'; import {ERC20} from '../ERC20.sol'; import {Errors} from '../../libraries/helpers/Errors.sol'; @@ -16,7 +18,6 @@ import {Errors} from '../../libraries/helpers/Errors.sol'; */ abstract contract DebtTokenBase is ERC20, VersionedInitializable { - address internal immutable UNDERLYING_ASSET; ILendingPool internal immutable POOL; mapping(address => uint256) internal _usersData; @@ -29,7 +30,7 @@ abstract contract DebtTokenBase is ERC20, VersionedInitializable { _; } - /** + /** * @dev The metadata of the token will be set on the proxy, that the reason of * passing "NULL" and 0 as metadata */ @@ -37,7 +38,7 @@ abstract contract DebtTokenBase is ERC20, VersionedInitializable { address pool, address underlyingAssetAddress, string memory name, - string memory symbol + string memory symbol ) public ERC20(name, symbol, 18) { POOL = ILendingPool(pool); UNDERLYING_ASSET = underlyingAssetAddress; @@ -76,32 +77,59 @@ abstract contract DebtTokenBase is ERC20, VersionedInitializable { * standard ERC20 functions for transfer and allowance. **/ function transfer(address recipient, uint256 amount) public virtual override returns (bool) { - recipient; amount; + recipient; + amount; revert('TRANSFER_NOT_SUPPORTED'); } - function allowance(address owner, address spender) public virtual override view returns (uint256) { - owner; spender; + function allowance(address owner, address spender) + public + virtual + override + view + returns (uint256) + { + owner; + spender; revert('ALLOWANCE_NOT_SUPPORTED'); } function approve(address spender, uint256 amount) public virtual override returns (bool) { - spender; amount; + spender; + amount; revert('APPROVAL_NOT_SUPPORTED'); } - function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { - sender; recipient; amount; + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override returns (bool) { + sender; + recipient; + amount; revert('TRANSFER_NOT_SUPPORTED'); } - function increaseAllowance(address spender, uint256 addedValue) public virtual override returns (bool) { - spender; addedValue; + function increaseAllowance(address spender, uint256 addedValue) + public + virtual + override + returns (bool) + { + spender; + addedValue; revert('ALLOWANCE_NOT_SUPPORTED'); } - function decreaseAllowance(address spender, uint256 subtractedValue) public virtual override returns (bool) { - spender; subtractedValue; + function decreaseAllowance(address spender, uint256 subtractedValue) + public + virtual + override + returns (bool) + { + spender; + subtractedValue; revert('ALLOWANCE_NOT_SUPPORTED'); } @@ -111,7 +139,15 @@ abstract contract DebtTokenBase is ERC20, VersionedInitializable { * @return The previous principal balance, the new principal balance, the balance increase * and the new user index **/ - function _calculateBalanceIncrease(address user) internal view returns (uint256, uint256, uint256) { + function _calculateBalanceIncrease(address user) + internal + view + returns ( + uint256, + uint256, + uint256 + ) + { uint256 previousPrincipalBalance = principalBalanceOf(user); if (previousPrincipalBalance == 0) { diff --git a/deployed-contracts.json b/deployed-contracts.json index d04816b9..cb373f29 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": "0xDf73fC454FA018051D4a1509e63D11530A59DE10" @@ -489,5 +489,10 @@ "address": "0xA8083d78B6ABC328b4d3B714F76F384eCC7147e1", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } + }, + "MockSwapAdapter": { + "buidlerevm": { + "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2" + } } -} \ No newline at end of file +} diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index b5b483d3..4bebd442 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -31,10 +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'; 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}`); @@ -212,6 +213,9 @@ 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 +391,14 @@ 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 e18ca909..79a810c2 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', @@ -111,7 +112,7 @@ export enum ProtocolErrors { INVALID_REDIRECTION_ADDRESS = 'Invalid redirection address', INVALID_HF = 'Invalid health factor', TRANSFER_AMOUNT_EXCEEDS_BALANCE = 'ERC20: transfer amount exceeds balance', - SAFEERC20_LOWLEVEL_CALL = 'SafeERC20: low-level call failed' + SAFEERC20_LOWLEVEL_CALL = 'SafeERC20: low-level call failed', } export type tEthereumAddress = string; diff --git a/package.json b/package.json index 606aaaf9..3da86f23 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "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", + "test-liquidate-with-collateral": "buidler test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", "test-flash": "buidler test test/__setup.spec.ts test/flashloan.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/__setup.spec.ts b/test/__setup.spec.ts index 342b7b64..e2db2752 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/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts new file mode 100644 index 00000000..c48b5e99 --- /dev/null +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -0,0 +1,916 @@ +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, BRE, increaseTime} from '../helpers/misc-utils'; +import {ProtocolErrors} from '../helpers/types'; +import {convertToCurrencyDecimals} from '../helpers/contracts-helpers'; +import {expectRepayWithCollateralEvent} from './repay-with-collateral.spec'; + +const {expect} = require('chai'); +const {parseUnits, parseEther} = ethers.utils; + +makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEnv) => { + const {INVALID_HF, COLLATERAL_CANNOT_BE_LIQUIDATED} = ProtocolErrors; + + it('User 1 provides some liquidity for others to borrow', async () => { + const {pool, weth, dai, usdc, deployer} = testEnv; + + await weth.mint(parseEther('200')); + await weth.approve(pool.address, parseEther('200')); + await pool.deposit(weth.address, parseEther('200'), deployer.address, 0); + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + await usdc.mint(parseEther('20000')); + await usdc.approve(pool.address, parseEther('20000')); + await pool.deposit(usdc.address, parseEther('20000'), deployer.address, 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, user.address, '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, user.address, '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' + ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; + + // 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('40'); + + 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]; + const amountToDeposit = ethers.utils.parseEther('2'); + + 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, user.address, '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.9) + .toFixed(0) + ); + + 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('53'); + + // 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]; + 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('38'); + }); + 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.multipliedBy(0.6).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() + ) + ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; + + // 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 + ); + + await increaseTime(1000); + // Repay the remaining DAI + const amountToRepay = daiReserveDataBefore.totalBorrowsVariable.toString(); + + await mockSwapAdapter.setAmountToReturn(amountToRepay); + const receipt = await waitForTx( + await pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + amountToRepay, + mockSwapAdapter.address, + '0x' + ) + ); + const repayWithCollateralTimestamp = (await BRE.ethers.provider.getBlock(receipt.blockNumber)) + .timestamp; + + 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( + 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 () => { + 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 = parseUnits('80', '6'); + + 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, user.address, '0'); + await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, user.address, '0'); + + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0); + + const amountToRepay = amountToBorrowVariable; + + 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 + const daiPrice = await oracle.getAssetPrice(dai.address); + await oracle.setAssetPrice( + 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, + 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 collateralConfig = await pool.getReserveConfigurationData(weth.address); + + const collateralDecimals = collateralConfig.decimals.toString(); + const principalDecimals = ( + await pool.getReserveConfigurationData(usdc.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( + usdcReserveDataBefore, + usdcUserDataBefore, + new BigNumber(repayWithCollateralTimestamp) + ).minus(usdcUserDataBefore.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 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, user.address, '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, user.address, '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/flashloan.spec.ts b/test/flashloan.spec.ts index 724be7ef..952bdab4 100644 --- a/test/flashloan.spec.ts +++ b/test/flashloan.spec.ts @@ -21,7 +21,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { REQUESTED_AMOUNT_TOO_SMALL, TRANSFER_AMOUNT_EXCEEDS_BALANCE, INVALID_FLASHLOAN_MODE, - SAFEERC20_LOWLEVEL_CALL + SAFEERC20_LOWLEVEL_CALL, } = ProtocolErrors; before(async () => { @@ -341,8 +341,8 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await pool - .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, weth.address, flashAmount, 1, '0x10', '0'); + .connect(caller.signer) + .flashLoan(_mockFlashLoanReceiver.address, weth.address, flashAmount, 1, '0x10', '0'); const {stableDebtTokenAddress} = await pool.getReserveTokensAddresses(weth.address); @@ -354,6 +354,5 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const callerDebt = await wethDebtToken.balanceOf(caller.address); expect(callerDebt.toString()).to.be.equal('800720000000000000', 'Invalid user debt'); - }); }); diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 76d4d907..3942fdf3 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -849,7 +849,7 @@ const getTxCostAndTimestamp = async (tx: ContractReceipt) => { return {txCost, txTimestamp}; }; -const getContractsData = async ( +export const getContractsData = async ( reserve: string, user: string, testEnv: TestEnv, 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/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 4ff03b19..13c8da78 100644 --- a/test/helpers/utils/calculations.ts +++ b/test/helpers/utils/calculations.ts @@ -7,9 +7,9 @@ import { EXCESS_UTILIZATION_RATE, ZERO_ADDRESS, } from '../../../helpers/constants'; -import { IReserveParams, iAavePoolAssets, RateMode } from '../../../helpers/types'; +import {IReserveParams, iAavePoolAssets, RateMode} from '../../../helpers/types'; import './math'; -import { ReserveData, UserReserveData } from './interfaces'; +import {ReserveData, UserReserveData} from './interfaces'; export const strToBN = (amount: string): BigNumber => new BigNumber(amount); @@ -687,7 +687,7 @@ export const calcExpectedUserDataAfterSetUseAsCollateral = ( userDataBeforeAction: UserReserveData, txCost: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; expectedUserData.usageAsCollateralEnabled = useAsCollateral; @@ -793,7 +793,7 @@ export const calcExpectedUserDataAfterSwapRateMode = ( txCost: BigNumber, txTimestamp: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; const variableBorrowBalance = calcExpectedVariableDebtTokenBalance( reserveDataBeforeAction, @@ -937,7 +937,7 @@ export const calcExpectedUserDataAfterStableRateRebalance = ( txCost: BigNumber, txTimestamp: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; expectedUserData.principalVariableDebt = calcExpectedVariableDebtTokenBalance( reserveDataBeforeAction, @@ -992,8 +992,8 @@ export const calcExpectedUsersDataAfterRedirectInterest = ( txCost: BigNumber, txTimestamp: BigNumber ): UserReserveData[] => { - const expectedFromData = { ...fromDataBeforeAction }; - const expectedToData = { ...toDataBeforeAction }; + const expectedFromData = {...fromDataBeforeAction}; + const expectedToData = {...toDataBeforeAction}; expectedFromData.currentStableDebt = calcExpectedStableDebtTokenBalance( fromDataBeforeAction, @@ -1164,7 +1164,7 @@ export const calcExpectedVariableDebtTokenBalance = ( ) => { const debt = calcExpectedReserveNormalizedDebt(reserveDataBeforeAction, currentTimestamp); - const { principalVariableDebt, variableBorrowIndex } = userDataBeforeAction; + const {principalVariableDebt, variableBorrowIndex} = userDataBeforeAction; if (variableBorrowIndex.eq(0)) { return principalVariableDebt; @@ -1177,7 +1177,7 @@ export const calcExpectedStableDebtTokenBalance = ( userDataBeforeAction: UserReserveData, currentTimestamp: BigNumber ) => { - const { principalStableDebt, stableBorrowRate, stableRateLastUpdated } = userDataBeforeAction; + const {principalStableDebt, stableBorrowRate, stableRateLastUpdated} = userDataBeforeAction; if ( stableBorrowRate.eq(0) || @@ -1250,7 +1250,7 @@ const calcExpectedInterestRates = ( totalBorrowsVariable: BigNumber, averageStableBorrowRate: BigNumber ): BigNumber[] => { - const { reservesParams } = configuration; + const {reservesParams} = configuration; const reserveIndex = Object.keys(reservesParams).findIndex((value) => value === reserveSymbol); const [, reserveConfiguration] = (Object.entries(reservesParams) as [string, IReserveParams][])[ @@ -1340,7 +1340,7 @@ const calcExpectedReserveNormalizedIncome = ( reserveData: ReserveData, currentTimestamp: BigNumber ) => { - const { liquidityRate, liquidityIndex, lastUpdateTimestamp } = reserveData; + const {liquidityRate, liquidityIndex, lastUpdateTimestamp} = reserveData; //if utilization rate is 0, nothing to compound if (liquidityRate.eq('0')) { @@ -1362,7 +1362,7 @@ const calcExpectedReserveNormalizedDebt = ( reserveData: ReserveData, currentTimestamp: BigNumber ) => { - const { variableBorrowRate, variableBorrowIndex, lastUpdateTimestamp } = reserveData; + const {variableBorrowRate, variableBorrowIndex, lastUpdateTimestamp} = reserveData; //if utilization rate is 0, nothing to compound if (variableBorrowRate.eq('0')) { diff --git a/test/liquidation-atoken.spec.ts b/test/liquidation-atoken.spec.ts index 90293397..342c2a63 100644 --- a/test/liquidation-atoken.spec.ts +++ b/test/liquidation-atoken.spec.ts @@ -196,7 +196,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' ); @@ -217,6 +217,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) @@ -254,7 +255,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) ); @@ -282,7 +283,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( @@ -336,7 +337,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 4be6097f..3f2aecbc 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'; @@ -15,6 +15,14 @@ const {expect} = chai; makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', (testEnv) => { const {INVALID_HF} = 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; const depositor = users[0]; @@ -107,6 +115,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); @@ -154,7 +164,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' ); @@ -224,7 +234,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) ); @@ -252,10 +262,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) @@ -300,7 +311,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' ); diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts new file mode 100644 index 00000000..faefa403 --- /dev/null +++ b/test/repay-with-collateral.spec.ts @@ -0,0 +1,640 @@ +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'; +import {parse} from 'path'; + +const {expect} = require('chai'); +const {parseUnits, parseEther} = ethers.utils; + +export const expectRepayWithCollateralEvent = ( + events: ethers.Event[], + pool: tEthereumAddress, + collateral: tEthereumAddress, + borrowing: tEthereumAddress, + user: tEthereumAddress +) => { + if (!events || events.length < 16) { + expect(false, 'INVALID_EVENTS_LENGTH_ON_REPAY_COLLATERAL'); + } + + const repayWithCollateralEvent = events[15]; + + 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, deployer} = testEnv; + + await weth.mint(parseEther('200')); + await weth.approve(pool.address, parseEther('200')); + await pool.deposit(weth.address, parseEther('200'), deployer.address, 0); + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + await usdc.mint(parseEther('20000')); + await usdc.approve(pool.address, parseEther('20000')); + await pool.deposit(usdc.address, parseEther('20000'), deployer.address, 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, user.address, '0'); + + 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('53'); + }); + + 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, + 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() + ) + ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; + }); + + 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, user.address, '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' + ); + + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.true; + }); + + 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]; + + 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('40'); + }); + + 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]; + + 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, user.address, '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 + ); + + 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 () => { + 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, user.address, '0'); + await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, user.address, '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 + ); + + 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 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); + + 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, user.address, '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, user.address, '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; + }); +}); 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 () => {