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 a7a5e1ca..b14f5cb8 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -15,7 +15,8 @@ interface ILendingPool { **/ event Deposit( address indexed reserve, - address indexed user, + address user, + address indexed onBehalfOf, uint256 amount, uint16 indexed referral ); @@ -139,6 +140,7 @@ interface ILendingPool { function deposit( address reserve, uint256 amount, + address onBehalfOf, uint16 referralCode ) external; @@ -218,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 9e4e6707..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 **/ @@ -89,6 +91,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { function deposit( address asset, uint256 amount, + address onBehalfOf, uint16 referralCode ) external override { ReserveLogic.ReserveData storage reserve = _reserves[asset]; @@ -100,18 +103,18 @@ contract LendingPool is VersionedInitializable, ILendingPool { reserve.updateCumulativeIndexesAndTimestamp(); reserve.updateInterestRates(asset, aToken, amount, 0); - bool isFirstDeposit = IAToken(aToken).balanceOf(msg.sender) == 0; + bool isFirstDeposit = IAToken(aToken).balanceOf(onBehalfOf) == 0; if (isFirstDeposit) { - _usersConfig[msg.sender].setUsingAsCollateral(reserve.index, true); + _usersConfig[onBehalfOf].setUsingAsCollateral(reserve.index, true); } //minting AToken to user 1:1 with the specific exchange rate - IAToken(aToken).mint(msg.sender, amount); + IAToken(aToken).mint(onBehalfOf, amount); //transfer to the aToken contract IERC20(asset).safeTransferFrom(msg.sender, aToken, amount); - emit Deposit(asset, msg.sender, amount, referralCode); + emit Deposit(asset, msg.sender, onBehalfOf, amount, referralCode); } /** @@ -198,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); @@ -238,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); } /** @@ -410,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. @@ -450,15 +511,13 @@ contract LendingPool is VersionedInitializable, ILendingPool { vars.amountPlusPremium = amount.add(vars.premium); if (debtMode == ReserveLogic.InterestRateMode.NONE) { - IERC20(asset).transferFrom(receiverAddress, vars.aTokenAddress, vars.amountPlusPremium); - + reserve.updateCumulativeIndexesAndTimestamp(); reserve.cumulateToLiquidityIndex(IERC20(vars.aTokenAddress).totalSupply(), vars.premium); reserve.updateInterestRates(asset, vars.aTokenAddress, vars.premium, 0); - - emit FlashLoan(receiverAddress, asset, amount, vars.premium, referralCode); + emit FlashLoan(receiverAddress, asset, amount, vars.premium, referralCode); } else { // If the transfer didn't succeed, the receiver either didn't return the funds, or didn't approve the transfer. _executeBorrow( @@ -728,13 +787,11 @@ contract LendingPool is VersionedInitializable, ILendingPool { oracle ); - uint256 reserveIndex = reserve.index; if (!userConfig.isBorrowing(reserveIndex)) { userConfig.setBorrowing(reserveIndex, true); } - reserve.updateCumulativeIndexesAndTimestamp(); //caching the current stable borrow rate @@ -754,13 +811,17 @@ contract LendingPool is VersionedInitializable, ILendingPool { IVariableDebtToken(reserve.variableDebtTokenAddress).mint(vars.user, vars.amount); } - reserve.updateInterestRates(vars.asset, vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0); - - if(vars.releaseUnderlying){ - IAToken(vars.aTokenAddress).transferUnderlyingTo(msg.sender, vars.amount); + reserve.updateInterestRates( + vars.asset, + vars.aTokenAddress, + 0, + vars.releaseUnderlying ? vars.amount : 0 + ); + + if (vars.releaseUnderlying) { + IAToken(vars.aTokenAddress).transferUnderlyingTo(msg.sender, vars.amount); } - - + emit Borrow( vars.asset, msg.sender, 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 0f0f4789..60fd2da3 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' @@ -71,7 +73,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 e969e5f3..2a21e629 100644 --- a/contracts/tokenization/AToken.sol +++ b/contracts/tokenization/AToken.sol @@ -10,9 +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 "@nomiclabs/buidler/console.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 07078f5d..c2aadefe 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" @@ -452,7 +452,7 @@ }, "MockAToken": { "buidlerevm": { - "address": "0x3bDA11B584dDff7F66E0cFe1da1562c92B45db60", + "address": "0x392E5355a0e88Bd394F717227c752670fb3a8020", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -472,7 +472,7 @@ }, "MockStableDebtToken": { "buidlerevm": { - "address": "0x392E5355a0e88Bd394F717227c752670fb3a8020", + "address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -482,12 +482,17 @@ }, "MockVariableDebtToken": { "buidlerevm": { - "address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460", + "address": "0xEBAB67ee3ef604D5c250A53b4b8fcbBC6ec3007C", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { "address": "0x1203D1b97BF6E546c00C45Cda035D3010ACe1180", "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/atoken-transfer.spec.ts b/test/atoken-transfer.spec.ts index f4b25065..b75db650 100644 --- a/test/atoken-transfer.spec.ts +++ b/test/atoken-transfer.spec.ts @@ -33,7 +33,9 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { //user 1 deposits 1000 DAI const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); - await pool.connect(users[0].signer).deposit(dai.address, amountDAItoDeposit, '0'); + await pool + .connect(users[0].signer) + .deposit(dai.address, amountDAItoDeposit, users[0].address, '0'); await aDai.connect(users[0].signer).transfer(users[1].address, amountDAItoDeposit); @@ -50,12 +52,15 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { it('User 0 deposits 1 WETH and user 1 tries to borrow, but the aTokens received as a transfer are not available as collateral (revert expected)', async () => { const {users, pool, weth} = testEnv; + const userAddress = await pool.signer.getAddress(); await weth.connect(users[0].signer).mint(await convertToCurrencyDecimals(weth.address, '1')); await weth.connect(users[0].signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - await pool.connect(users[0].signer).deposit(weth.address, ethers.utils.parseEther('1.0'), '0'); + await pool + .connect(users[0].signer) + .deposit(weth.address, ethers.utils.parseEther('1.0'), userAddress, '0'); await expect( pool .connect(users[1].signer) diff --git a/test/configurator.spec.ts b/test/configurator.spec.ts index a6513e54..6a85791c 100644 --- a/test/configurator.spec.ts +++ b/test/configurator.spec.ts @@ -234,7 +234,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { it('Reverts when trying to disable the DAI reserve with liquidity on it', async () => { const {dai, pool, configurator} = testEnv; - + const userAddress = await pool.signer.getAddress(); await dai.mint(await convertToCurrencyDecimals(dai.address, '1000')); //approve protocol to access depositor wallet @@ -242,7 +242,7 @@ makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); //user 1 deposits 1000 DAI - await pool.deposit(dai.address, amountDAItoDeposit, '0'); + await pool.deposit(dai.address, amountDAItoDeposit, userAddress, '0'); await expect( configurator.deactivateReserve(dai.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 79db4bcb..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 () => { @@ -30,13 +30,14 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { it('Deposits ETH into the reserve', async () => { const {pool, weth} = testEnv; + const userAddress = await pool.signer.getAddress(); const amountToDeposit = ethers.utils.parseEther('1'); await weth.mint(amountToDeposit); await weth.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - await pool.deposit(weth.address, amountToDeposit, '0'); + await pool.deposit(weth.address, amountToDeposit, userAddress, '0'); }); it('Takes WETH flashloan with mode = 0, returns the funds correctly', async () => { @@ -143,7 +144,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); - await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, '0'); + await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, caller.address, '0'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); @@ -210,6 +211,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { it('Deposits USDC into the reserve', async () => { const {usdc, pool} = testEnv; + const userAddress = await pool.signer.getAddress(); await usdc.mint(await convertToCurrencyDecimals(usdc.address, '1000')); @@ -217,7 +219,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const amountToDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); - await pool.deposit(usdc.address, amountToDeposit, '0'); + await pool.deposit(usdc.address, amountToDeposit, userAddress, '0'); }); it('Takes out a 500 USDC flashloan, returns the funds correctly', async () => { @@ -284,7 +286,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const amountToDeposit = await convertToCurrencyDecimals(weth.address, '5'); - await pool.connect(caller.signer).deposit(weth.address, amountToDeposit, '0'); + await pool.connect(caller.signer).deposit(weth.address, amountToDeposit, caller.address, '0'); await _mockFlashLoanReceiver.setFailExecutionTransfer(true); @@ -307,7 +309,6 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { it('Caller deposits 1000 DAI as collateral, Takes a WETH flashloan with mode = 0, does not approve the transfer of the funds', async () => { const {dai, pool, weth, users} = testEnv; - const caller = users[3]; await dai.connect(caller.signer).mint(await convertToCurrencyDecimals(dai.address, '1000')); @@ -316,7 +317,7 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const amountToDeposit = await convertToCurrencyDecimals(dai.address, '1000'); - await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, '0'); + await pool.connect(caller.signer).deposit(dai.address, amountToDeposit, caller.address, '0'); const flashAmount = ethers.utils.parseEther('0.8'); @@ -340,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); @@ -353,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 6cf5364f..1e8dd6db 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -134,7 +134,8 @@ export const approve = async (reserveSymbol: string, user: SignerWithAddress, te export const deposit = async ( reserveSymbol: string, amount: string, - user: SignerWithAddress, + sender: SignerWithAddress, + onBehalfOf: tEthereumAddress, sendValue: string, expectedResult: string, testEnv: TestEnv, @@ -150,8 +151,9 @@ export const deposit = async ( const {reserveData: reserveDataBefore, userData: userDataBefore} = await getContractsData( reserve, - user.address, - testEnv + onBehalfOf, + testEnv, + sender.address ); if (sendValue) { @@ -159,14 +161,16 @@ export const deposit = async ( } if (expectedResult === 'success') { const txResult = await waitForTx( - await await pool.connect(user.signer).deposit(reserve, amountToDeposit, '0', txOptions) + await pool + .connect(sender.signer) + .deposit(reserve, amountToDeposit, onBehalfOf, '0', txOptions) ); const { reserveData: reserveDataAfter, userData: userDataAfter, timestamp, - } = await getContractsData(reserve, user.address, testEnv); + } = await getContractsData(reserve, onBehalfOf, testEnv, sender.address); const {txCost, txTimestamp} = await getTxCostAndTimestamp(txResult); @@ -199,7 +203,7 @@ export const deposit = async ( // }); } else if (expectedResult === 'revert') { await expect( - pool.connect(user.signer).deposit(reserve, amountToDeposit, '0', txOptions), + pool.connect(sender.signer).deposit(reserve, amountToDeposit, onBehalfOf, '0', txOptions), revertMessage ).to.be.reverted; } @@ -699,10 +703,15 @@ const getTxCostAndTimestamp = async (tx: ContractReceipt) => { return {txCost, txTimestamp}; }; -const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { +export const getContractsData = async ( + reserve: string, + user: string, + testEnv: TestEnv, + sender?: string +) => { const {pool} = testEnv; const reserveData = await getReserveData(pool, reserve); - const userData = await getUserData(pool, reserve, user); + const userData = await getUserData(pool, reserve, user, sender || user); const timestamp = await timeLatest(); return { 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/scenario-engine.ts b/test/helpers/scenario-engine.ts index 11fe8f10..8618de3d 100644 --- a/test/helpers/scenario-engine.ts +++ b/test/helpers/scenario-engine.ts @@ -89,13 +89,25 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv case 'deposit': { - const {amount, sendValue} = action.args; + const {amount, sendValue, onBehalfOf: onBehalfOfIndex} = action.args; + const onBehalfOf = onBehalfOfIndex + ? users[parseInt(onBehalfOfIndex)].address + : user.address; if (!amount || amount === '') { throw `Invalid amount to deposit into the ${reserve} reserve`; } - await deposit(reserve, amount, user, sendValue, expected, testEnv, revertMessage); + await deposit( + reserve, + amount, + user, + onBehalfOf, + sendValue, + expected, + testEnv, + revertMessage + ); } break; diff --git a/test/helpers/scenarios/deposit.json b/test/helpers/scenarios/deposit.json index 34f9c9e9..2456d931 100644 --- a/test/helpers/scenarios/deposit.json +++ b/test/helpers/scenarios/deposit.json @@ -206,7 +206,6 @@ "name": "deposit", "args": { "reserve": "WETH", - "amount": "0", "user": "1" }, @@ -229,6 +228,40 @@ "revertMessage": "Amount must be greater than 0" } ] + }, + { + "description": "User 1 deposits 100 DAI on behalf of user 2, user 2 tries to borrow 0.1 WETH", + "actions": [ + { + "name": "mint", + "args": { + "reserve": "DAI", + "amount": "100", + "user": "1" + }, + "expected": "success" + }, + { + "name": "deposit", + "args": { + "reserve": "DAI", + "amount": "100", + "user": "1", + "onBehalfOf": "2" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "0.1", + "borrowRateMode": "variable", + "user": "2" + }, + "expected": "success" + } + ] } ] } diff --git a/test/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 502d93be..ff4c64be 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); @@ -640,7 +640,7 @@ export const calcExpectedUserDataAfterSetUseAsCollateral = ( userDataBeforeAction: UserReserveData, txCost: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; expectedUserData.usageAsCollateralEnabled = useAsCollateral; @@ -746,7 +746,7 @@ export const calcExpectedUserDataAfterSwapRateMode = ( txCost: BigNumber, txTimestamp: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; const variableBorrowBalance = calcExpectedVariableDebtTokenBalance( reserveDataBeforeAction, @@ -879,7 +879,7 @@ export const calcExpectedUserDataAfterStableRateRebalance = ( txCost: BigNumber, txTimestamp: BigNumber ): UserReserveData => { - const expectedUserData = { ...userDataBeforeAction }; + const expectedUserData = {...userDataBeforeAction}; expectedUserData.principalVariableDebt = calcExpectedVariableDebtTokenBalance( reserveDataBeforeAction, @@ -980,7 +980,7 @@ export const calcExpectedVariableDebtTokenBalance = ( ) => { const debt = calcExpectedReserveNormalizedDebt(reserveDataBeforeAction, currentTimestamp); - const { principalVariableDebt, variableBorrowIndex } = userDataBeforeAction; + const {principalVariableDebt, variableBorrowIndex} = userDataBeforeAction; if (variableBorrowIndex.eq(0)) { return principalVariableDebt; @@ -993,7 +993,7 @@ export const calcExpectedStableDebtTokenBalance = ( userDataBeforeAction: UserReserveData, currentTimestamp: BigNumber ) => { - const { principalStableDebt, stableBorrowRate, stableRateLastUpdated } = userDataBeforeAction; + const {principalStableDebt, stableBorrowRate, stableRateLastUpdated} = userDataBeforeAction; if ( stableBorrowRate.eq(0) || @@ -1066,7 +1066,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][])[ @@ -1156,7 +1156,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')) { @@ -1178,7 +1178,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/helpers/utils/helpers.ts b/test/helpers/utils/helpers.ts index f1a1d206..7d3ccee5 100644 --- a/test/helpers/utils/helpers.ts +++ b/test/helpers/utils/helpers.ts @@ -61,7 +61,8 @@ export const getReserveData = async ( export const getUserData = async ( pool: LendingPool, reserve: string, - user: string + user: tEthereumAddress, + sender?: tEthereumAddress ): Promise => { const [userData, scaledATokenBalance] = await Promise.all([ pool.getUserReserveData(reserve, user), @@ -70,7 +71,7 @@ export const getUserData = async ( const token = await getMintableErc20(reserve); - const walletBalance = new BigNumber((await token.balanceOf(user)).toString()); + const walletBalance = new BigNumber((await token.balanceOf(sender || user)).toString()); return { scaledATokenBalance: new BigNumber(scaledATokenBalance), diff --git a/test/liquidation-atoken.spec.ts b/test/liquidation-atoken.spec.ts index 921114f0..342c2a63 100644 --- a/test/liquidation-atoken.spec.ts +++ b/test/liquidation-atoken.spec.ts @@ -32,7 +32,9 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => //user 1 deposits 1000 DAI const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); - await pool.connect(depositor.signer).deposit(dai.address, amountDAItoDeposit, '0'); + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); @@ -43,7 +45,9 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); //user 2 deposits 1 WETH - await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '0'); + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //user 2 borrows const userGlobalData = await pool.getUserAccountData(borrower.address); @@ -192,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' ); @@ -213,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) @@ -224,7 +229,9 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => //user 3 deposits 1000 USDC const amountUSDCtoDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); - await pool.connect(depositor.signer).deposit(usdc.address, amountUSDCtoDeposit, '0'); + await pool + .connect(depositor.signer) + .deposit(usdc.address, amountUSDCtoDeposit, depositor.address, '0'); //user 4 deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); @@ -235,7 +242,9 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => //approve protocol to access borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '0'); + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //user 4 borrows const userGlobalData = await pool.getUserAccountData(borrower.address); @@ -246,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) ); @@ -274,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( @@ -328,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 064e3856..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]; @@ -29,7 +37,9 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', //user 1 deposits 1000 DAI const amountDAItoDeposit = await convertToCurrencyDecimals(dai.address, '1000'); - await pool.connect(depositor.signer).deposit(dai.address, amountDAItoDeposit, '0'); + await pool + .connect(depositor.signer) + .deposit(dai.address, amountDAItoDeposit, depositor.address, '0'); //user 2 deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); @@ -39,7 +49,9 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', //approve protocol to access the borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '0'); + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //user 2 borrows @@ -103,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); @@ -150,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' ); @@ -194,7 +208,9 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', //depositor deposits 1000 USDC const amountUSDCtoDeposit = await convertToCurrencyDecimals(usdc.address, '1000'); - await pool.connect(depositor.signer).deposit(usdc.address, amountUSDCtoDeposit, '0'); + await pool + .connect(depositor.signer) + .deposit(usdc.address, amountUSDCtoDeposit, depositor.address, '0'); //borrower deposits 1 ETH const amountETHtoDeposit = await convertToCurrencyDecimals(weth.address, '1'); @@ -205,7 +221,9 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', //approve protocol to access the borrower wallet await weth.connect(borrower.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); - await pool.connect(borrower.signer).deposit(weth.address, amountETHtoDeposit, '0'); + await pool + .connect(borrower.signer) + .deposit(weth.address, amountETHtoDeposit, borrower.address, '0'); //borrower borrows const userGlobalData = await pool.getUserAccountData(borrower.address); @@ -216,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) ); @@ -244,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) @@ -292,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' ); @@ -334,7 +353,9 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', //borrower deposits 1000 LEND const amountLENDtoDeposit = await convertToCurrencyDecimals(lend.address, '1000'); - await pool.connect(borrower.signer).deposit(lend.address, amountLENDtoDeposit, '0'); + await pool + .connect(borrower.signer) + .deposit(lend.address, amountLENDtoDeposit, borrower.address, '0'); const usdcPrice = await oracle.getAssetPrice(usdc.address); //drops HF below 1 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 () => {