diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bbe8b021..37bc4176 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,4 +40,3 @@ certora-test: - certoraRun specs/harness/StableDebtTokenHarness.sol:StableDebtTokenHarness --solc_args '--optimize' --verify StableDebtTokenHarness:specs/StableDebtToken.spec --settings -assumeUnwindCond,-b=4 --cache StableDebtToken --cloud - certoraRun specs/harness/UserConfigurationHarness.sol --verify UserConfigurationHarness:specs/UserConfiguration.spec --solc_args '--optimize' --settings -useBitVectorTheory --cache UserConfiguration --cloud - certoraRun contracts/protocol/tokenization/VariableDebtToken.sol:VariableDebtToken specs/harness/LendingPoolHarnessForVariableDebtToken.sol --solc_args '--optimize' --link VariableDebtToken:POOL=LendingPoolHarnessForVariableDebtToken --verify VariableDebtToken:specs/VariableDebtToken.spec --settings -assumeUnwindCond,-useNonLinearArithmetic,-b=4 --cache VariableDebtToken --cloud - diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol new file mode 100644 index 00000000..b04bc8a1 --- /dev/null +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {PercentageMath} from '../protocol/libraries/math/PercentageMath.sol'; +import {SafeMath} from '../dependencies/openzeppelin/contracts/SafeMath.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {IERC20Detailed} from '../dependencies/openzeppelin/contracts/IERC20Detailed.sol'; +import {SafeERC20} from '../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {Ownable} from '../dependencies/openzeppelin/contracts/Ownable.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; +import {FlashLoanReceiverBase} from '../flashloan/base/FlashLoanReceiverBase.sol'; +import {IBaseUniswapAdapter} from './interfaces/IBaseUniswapAdapter.sol'; + +/** + * @title BaseUniswapAdapter + * @notice Implements the logic for performing assets swaps in Uniswap V2 + * @author Aave + **/ +abstract contract BaseUniswapAdapter is FlashLoanReceiverBase, IBaseUniswapAdapter, Ownable { + using SafeMath for uint256; + using PercentageMath for uint256; + using SafeERC20 for IERC20; + + // Max slippage percent allowed + uint256 public constant override MAX_SLIPPAGE_PERCENT = 3000; // 30% + // FLash Loan fee set in lending pool + uint256 public constant override FLASHLOAN_PREMIUM_TOTAL = 9; + // USD oracle asset address + address public constant override USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; + + address public immutable override WETH_ADDRESS; + IPriceOracleGetter public immutable override ORACLE; + IUniswapV2Router02 public immutable override UNISWAP_ROUTER; + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter, + address wethAddress + ) public FlashLoanReceiverBase(addressesProvider) { + ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle()); + UNISWAP_ROUTER = uniswapRouter; + WETH_ADDRESS = wethAddress; + } + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices + * @param amountIn Amount of reserveIn + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount out of the reserveOut + * @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function getAmountsOut( + uint256 amountIn, + address reserveIn, + address reserveOut + ) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ) + { + AmountCalc memory results = _getAmountsOutData(reserveIn, reserveOut, amountIn); + + return ( + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd, + results.path + ); + } + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount and the prices + * @param amountOut Amount of reserveOut + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount in of the reserveIn + * @return uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function getAmountsIn( + uint256 amountOut, + address reserveIn, + address reserveOut + ) + external + view + override + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ) + { + AmountCalc memory results = _getAmountsInData(reserveIn, reserveOut, amountOut); + + return ( + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd, + results.path + ); + } + + /** + * @dev Swaps an exact `amountToSwap` of an asset to another + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap Exact amount of `assetToSwapFrom` to be swapped + * @param minAmountOut the min amount of `assetToSwapTo` to be received from the swap + * @return the amount received from the swap + */ + function _swapExactTokensForTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + uint256 minAmountOut, + bool useEthPath + ) internal returns (uint256) { + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); + + uint256 expectedMinAmountOut = + amountToSwap + .mul(fromAssetPrice.mul(10**toAssetDecimals)) + .div(toAssetPrice.mul(10**fromAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(MAX_SLIPPAGE_PERCENT)); + + require(expectedMinAmountOut < minAmountOut, 'minAmountOut exceed max slippage'); + + // Approves the transfer for the swap. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), 0); + IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), amountToSwap); + + address[] memory path; + if (useEthPath) { + path = new address[](3); + path[0] = assetToSwapFrom; + path[1] = WETH_ADDRESS; + path[2] = assetToSwapTo; + } else { + path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + } + uint256[] memory amounts = + UNISWAP_ROUTER.swapExactTokensForTokens( + amountToSwap, + minAmountOut, + path, + address(this), + block.timestamp + ); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]); + + return amounts[amounts.length - 1]; + } + + /** + * @dev Receive an exact amount `amountToReceive` of `assetToSwapTo` tokens for as few `assetToSwapFrom` tokens as + * possible. + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param maxAmountToSwap Max amount of `assetToSwapFrom` allowed to be swapped + * @param amountToReceive Exact amount of `assetToSwapTo` to receive + * @return the amount swapped + */ + function _swapTokensForExactTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 maxAmountToSwap, + uint256 amountToReceive, + bool useEthPath + ) internal returns (uint256) { + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); + + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); + + uint256 expectedMaxAmountToSwap = + amountToReceive + .mul(toAssetPrice.mul(10**fromAssetDecimals)) + .div(fromAssetPrice.mul(10**toAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.add(MAX_SLIPPAGE_PERCENT)); + + require(maxAmountToSwap < expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage'); + + // Approves the transfer for the swap. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), 0); + IERC20(assetToSwapFrom).safeApprove(address(UNISWAP_ROUTER), maxAmountToSwap); + + address[] memory path; + if (useEthPath) { + path = new address[](3); + path[0] = assetToSwapFrom; + path[1] = WETH_ADDRESS; + path[2] = assetToSwapTo; + } else { + path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + } + + uint256[] memory amounts = + UNISWAP_ROUTER.swapTokensForExactTokens( + amountToReceive, + maxAmountToSwap, + path, + address(this), + block.timestamp + ); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[amounts.length - 1]); + + return amounts[0]; + } + + /** + * @dev Get the price of the asset from the oracle denominated in eth + * @param asset address + * @return eth price for the asset + */ + function _getPrice(address asset) internal view returns (uint256) { + return ORACLE.getAssetPrice(asset); + } + + /** + * @dev Get the decimals of an asset + * @return number of decimals of the asset + */ + function _getDecimals(address asset) internal view returns (uint256) { + return IERC20Detailed(asset).decimals(); + } + + /** + * @dev Get the aToken associated to the asset + * @return address of the aToken + */ + function _getReserveData(address asset) internal view returns (DataTypes.ReserveData memory) { + return LENDING_POOL.getReserveData(asset); + } + + /** + * @dev Pull the ATokens from the user + * @param reserve address of the asset + * @param reserveAToken address of the aToken of the reserve + * @param user address + * @param amount of tokens to be transferred to the contract + * @param permitSignature struct containing the permit signature + */ + function _pullAToken( + address reserve, + address reserveAToken, + address user, + uint256 amount, + PermitSignature memory permitSignature + ) internal { + if (_usePermit(permitSignature)) { + IERC20WithPermit(reserveAToken).permit( + user, + address(this), + permitSignature.amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + + // transfer from user to adapter + IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); + + // withdraw reserve + LENDING_POOL.withdraw(reserve, amount, address(this)); + } + + /** + * @dev Tells if the permit method should be called by inspecting if there is a valid signature. + * If signature params are set to 0, then permit won't be called. + * @param signature struct containing the permit signature + * @return whether or not permit should be called + */ + function _usePermit(PermitSignature memory signature) internal pure returns (bool) { + return + !(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0); + } + + /** + * @dev Calculates the value denominated in USD + * @param reserve Address of the reserve + * @param amount Amount of the reserve + * @param decimals Decimals of the reserve + * @return whether or not permit should be called + */ + function _calcUsdValue( + address reserve, + uint256 amount, + uint256 decimals + ) internal view returns (uint256) { + uint256 ethUsdPrice = _getPrice(USD_ADDRESS); + uint256 reservePrice = _getPrice(reserve); + + return amount.mul(reservePrice).div(10**decimals).mul(ethUsdPrice).div(10**18); + } + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountIn Amount of reserveIn + * @return Struct containing the following information: + * uint256 Amount out of the reserveOut + * uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * uint256 In amount of reserveIn value denominated in USD (8 decimals) + * uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function _getAmountsOutData( + address reserveIn, + address reserveOut, + uint256 amountIn + ) internal view returns (AmountCalc memory) { + // Subtract flash loan fee + uint256 finalAmountIn = amountIn.sub(amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); + + address[] memory simplePath = new address[](2); + simplePath[0] = reserveIn; + simplePath[1] = reserveOut; + + uint256[] memory amountsWithoutWeth; + uint256[] memory amountsWithWeth; + + address[] memory pathWithWeth = new address[](3); + if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) { + pathWithWeth[0] = reserveIn; + pathWithWeth[1] = WETH_ADDRESS; + pathWithWeth[2] = reserveOut; + + try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, pathWithWeth) returns ( + uint256[] memory resultsWithWeth + ) { + amountsWithWeth = resultsWithWeth; + } catch { + amountsWithWeth = new uint256[](3); + } + } else { + amountsWithWeth = new uint256[](3); + } + + uint256 bestAmountOut; + try UNISWAP_ROUTER.getAmountsOut(finalAmountIn, simplePath) returns ( + uint256[] memory resultAmounts + ) { + amountsWithoutWeth = resultAmounts; + + bestAmountOut = (amountsWithWeth[2] > amountsWithoutWeth[1]) + ? amountsWithWeth[2] + : amountsWithoutWeth[1]; + } catch { + amountsWithoutWeth = new uint256[](2); + bestAmountOut = amountsWithWeth[2]; + } + + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 outPerInPrice = + finalAmountIn.mul(10**18).mul(10**reserveOutDecimals).div( + bestAmountOut.mul(10**reserveInDecimals) + ); + + return + AmountCalc( + bestAmountOut, + outPerInPrice, + _calcUsdValue(reserveIn, amountIn, reserveInDecimals), + _calcUsdValue(reserveOut, bestAmountOut, reserveOutDecimals), + (bestAmountOut == 0) ? new address[](2) : (bestAmountOut == amountsWithoutWeth[1]) + ? simplePath + : pathWithWeth + ); + } + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return Struct containing the following information: + * uint256 Amount in of the reserveIn + * uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * uint256 In amount of reserveIn value denominated in USD (8 decimals) + * uint256 Out amount of reserveOut value denominated in USD (8 decimals) + */ + function _getAmountsInData( + address reserveIn, + address reserveOut, + uint256 amountOut + ) internal view returns (AmountCalc memory) { + (uint256[] memory amounts, address[] memory path) = + _getAmountsInAndPath(reserveIn, reserveOut, amountOut); + + // Add flash loan fee + uint256 finalAmountIn = amounts[0].add(amounts[0].mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); + + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 inPerOutPrice = + amountOut.mul(10**18).mul(10**reserveInDecimals).div( + finalAmountIn.mul(10**reserveOutDecimals) + ); + + return + AmountCalc( + finalAmountIn, + inPerOutPrice, + _calcUsdValue(reserveIn, finalAmountIn, reserveInDecimals), + _calcUsdValue(reserveOut, amountOut, reserveOutDecimals), + path + ); + } + + /** + * @dev Calculates the input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return uint256[] amounts Array containing the amountIn and amountOut for a swap + */ + function _getAmountsInAndPath( + address reserveIn, + address reserveOut, + uint256 amountOut + ) internal view returns (uint256[] memory, address[] memory) { + address[] memory simplePath = new address[](2); + simplePath[0] = reserveIn; + simplePath[1] = reserveOut; + + uint256[] memory amountsWithoutWeth; + uint256[] memory amountsWithWeth; + address[] memory pathWithWeth = new address[](3); + + if (reserveIn != WETH_ADDRESS && reserveOut != WETH_ADDRESS) { + pathWithWeth[0] = reserveIn; + pathWithWeth[1] = WETH_ADDRESS; + pathWithWeth[2] = reserveOut; + + try UNISWAP_ROUTER.getAmountsIn(amountOut, pathWithWeth) returns ( + uint256[] memory resultsWithWeth + ) { + amountsWithWeth = resultsWithWeth; + } catch { + amountsWithWeth = new uint256[](3); + } + } else { + amountsWithWeth = new uint256[](3); + } + + try UNISWAP_ROUTER.getAmountsIn(amountOut, simplePath) returns ( + uint256[] memory resultAmounts + ) { + amountsWithoutWeth = resultAmounts; + + return + (amountsWithWeth[0] < amountsWithoutWeth[0] && amountsWithWeth[0] != 0) + ? (amountsWithWeth, pathWithWeth) + : (amountsWithoutWeth, simplePath); + } catch { + return (amountsWithWeth, pathWithWeth); + } + } + + /** + * @dev Calculates the input asset amount required to buy the given output asset amount + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @param amountOut Amount of reserveOut + * @return uint256[] amounts Array containing the amountIn and amountOut for a swap + */ + function _getAmountsIn( + address reserveIn, + address reserveOut, + uint256 amountOut, + bool useEthPath + ) internal view returns (uint256[] memory) { + address[] memory path; + + if (useEthPath) { + path = new address[](3); + path[0] = reserveIn; + path[1] = WETH_ADDRESS; + path[2] = reserveOut; + } else { + path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + } + + return UNISWAP_ROUTER.getAmountsIn(amountOut, path); + } + + /** + * @dev Emergency rescue for token stucked on this contract, as failsafe mechanism + * - Funds should never remain in this contract more time than during transactions + * - Only callable by the owner + **/ + function rescueTokens(IERC20 token) external onlyOwner { + token.transfer(owner(), token.balanceOf(address(this))); + } +} diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol new file mode 100644 index 00000000..44745ad5 --- /dev/null +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +/** + * @title UniswapLiquiditySwapAdapter + * @notice Uniswap V2 Adapter to swap liquidity. + * @author Aave + **/ +contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter { + struct PermitParams { + uint256[] amount; + uint256[] deadline; + uint8[] v; + bytes32[] r; + bytes32[] s; + } + + struct SwapParams { + address[] assetToSwapToList; + uint256[] minAmountsToReceive; + bool[] swapAllBalance; + PermitParams permitParams; + bool[] useEthPath; + } + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter, + address wethAddress + ) public BaseUniswapAdapter(addressesProvider, uniswapRouter, wethAddress) {} + + /** + * @dev Swaps the received reserve amount from the flash loan into the asset specified in the params. + * The received funds from the swap are then deposited into the protocol on behalf of the user. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset and + * repay the flash loan. + * @param assets Address of asset to be swapped + * @param amounts Amount of the asset to be swapped + * @param premiums Fee of the flash loan + * @param initiator Address of the user + * @param params Additional variadic field to include extra params. Expected parameters: + * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and deposited + * uint256[] minAmountsToReceive List of min amounts to be received from the swap + * bool[] swapAllBalance Flag indicating if all the user balance should be swapped + * uint256[] permitAmount List of amounts for the permit signature + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature + */ + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override returns (bool) { + require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); + + SwapParams memory decodedParams = _decodeParams(params); + + require( + assets.length == decodedParams.assetToSwapToList.length && + assets.length == decodedParams.minAmountsToReceive.length && + assets.length == decodedParams.swapAllBalance.length && + assets.length == decodedParams.permitParams.amount.length && + assets.length == decodedParams.permitParams.deadline.length && + assets.length == decodedParams.permitParams.v.length && + assets.length == decodedParams.permitParams.r.length && + assets.length == decodedParams.permitParams.s.length && + assets.length == decodedParams.useEthPath.length, + 'INCONSISTENT_PARAMS' + ); + + for (uint256 i = 0; i < assets.length; i++) { + _swapLiquidity( + assets[i], + decodedParams.assetToSwapToList[i], + amounts[i], + premiums[i], + initiator, + decodedParams.minAmountsToReceive[i], + decodedParams.swapAllBalance[i], + PermitSignature( + decodedParams.permitParams.amount[i], + decodedParams.permitParams.deadline[i], + decodedParams.permitParams.v[i], + decodedParams.permitParams.r[i], + decodedParams.permitParams.s[i] + ), + decodedParams.useEthPath[i] + ); + } + + return true; + } + + struct SwapAndDepositLocalVars { + uint256 i; + uint256 aTokenInitiatorBalance; + uint256 amountToSwap; + uint256 receivedAmount; + address aToken; + } + + /** + * @dev Swaps an amount of an asset to another and deposits the new asset amount on behalf of the user without using + * a flash loan. This method can be used when the temporary transfer of the collateral asset to this contract + * does not affect the user position. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset and + * perform the swap. + * @param assetToSwapFromList List of addresses of the underlying asset to be swap from + * @param assetToSwapToList List of addresses of the underlying asset to be swap to and deposited + * @param amountToSwapList List of amounts to be swapped. If the amount exceeds the balance, the total balance is used for the swap + * @param minAmountsToReceive List of min amounts to be received from the swap + * @param permitParams List of struct containing the permit signatures + * uint256 permitAmount Amount for the permit signature + * uint256 deadline Deadline for the permit signature + * uint8 v param for the permit signature + * bytes32 r param for the permit signature + * bytes32 s param for the permit signature + * @param useEthPath true if the swap needs to occur using ETH in the routing, false otherwise + */ + function swapAndDeposit( + address[] calldata assetToSwapFromList, + address[] calldata assetToSwapToList, + uint256[] calldata amountToSwapList, + uint256[] calldata minAmountsToReceive, + PermitSignature[] calldata permitParams, + bool[] calldata useEthPath + ) external { + require( + assetToSwapFromList.length == assetToSwapToList.length && + assetToSwapFromList.length == amountToSwapList.length && + assetToSwapFromList.length == minAmountsToReceive.length && + assetToSwapFromList.length == permitParams.length, + 'INCONSISTENT_PARAMS' + ); + + SwapAndDepositLocalVars memory vars; + + for (vars.i = 0; vars.i < assetToSwapFromList.length; vars.i++) { + vars.aToken = _getReserveData(assetToSwapFromList[vars.i]).aTokenAddress; + + vars.aTokenInitiatorBalance = IERC20(vars.aToken).balanceOf(msg.sender); + vars.amountToSwap = amountToSwapList[vars.i] > vars.aTokenInitiatorBalance + ? vars.aTokenInitiatorBalance + : amountToSwapList[vars.i]; + + _pullAToken( + assetToSwapFromList[vars.i], + vars.aToken, + msg.sender, + vars.amountToSwap, + permitParams[vars.i] + ); + + vars.receivedAmount = _swapExactTokensForTokens( + assetToSwapFromList[vars.i], + assetToSwapToList[vars.i], + vars.amountToSwap, + minAmountsToReceive[vars.i], + useEthPath[vars.i] + ); + + // Deposit new reserve + IERC20(assetToSwapToList[vars.i]).safeApprove(address(LENDING_POOL), 0); + IERC20(assetToSwapToList[vars.i]).safeApprove(address(LENDING_POOL), vars.receivedAmount); + LENDING_POOL.deposit(assetToSwapToList[vars.i], vars.receivedAmount, msg.sender, 0); + } + } + + /** + * @dev Swaps an `amountToSwap` of an asset to another and deposits the funds on behalf of the initiator. + * @param assetFrom Address of the underlying asset to be swap from + * @param assetTo Address of the underlying asset to be swap to and deposited + * @param amount Amount from flash loan + * @param premium Premium of the flash loan + * @param minAmountToReceive Min amount to be received from the swap + * @param swapAllBalance Flag indicating if all the user balance should be swapped + * @param permitSignature List of struct containing the permit signature + * @param useEthPath true if the swap needs to occur using ETH in the routing, false otherwise + */ + + struct SwapLiquidityLocalVars { + address aToken; + uint256 aTokenInitiatorBalance; + uint256 amountToSwap; + uint256 receivedAmount; + uint256 flashLoanDebt; + uint256 amountToPull; + } + + function _swapLiquidity( + address assetFrom, + address assetTo, + uint256 amount, + uint256 premium, + address initiator, + uint256 minAmountToReceive, + bool swapAllBalance, + PermitSignature memory permitSignature, + bool useEthPath + ) internal { + + SwapLiquidityLocalVars memory vars; + + vars.aToken = _getReserveData(assetFrom).aTokenAddress; + + vars.aTokenInitiatorBalance = IERC20(vars.aToken).balanceOf(initiator); + vars.amountToSwap = + swapAllBalance && vars.aTokenInitiatorBalance.sub(premium) <= amount + ? vars.aTokenInitiatorBalance.sub(premium) + : amount; + + vars.receivedAmount = + _swapExactTokensForTokens(assetFrom, assetTo, vars.amountToSwap, minAmountToReceive, useEthPath); + + // Deposit new reserve + IERC20(assetTo).safeApprove(address(LENDING_POOL), 0); + IERC20(assetTo).safeApprove(address(LENDING_POOL), vars.receivedAmount); + LENDING_POOL.deposit(assetTo, vars.receivedAmount, initiator, 0); + + vars.flashLoanDebt = amount.add(premium); + vars.amountToPull = vars.amountToSwap.add(premium); + + _pullAToken(assetFrom, vars.aToken, initiator, vars.amountToPull, permitSignature); + + // Repay flash loan + IERC20(assetFrom).safeApprove(address(LENDING_POOL), 0); + IERC20(assetFrom).safeApprove(address(LENDING_POOL), vars.flashLoanDebt); + } + + /** + * @dev Decodes the information encoded in the flash loan params + * @param params Additional variadic field to include extra params. Expected parameters: + * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and deposited + * uint256[] minAmountsToReceive List of min amounts to be received from the swap + * bool[] swapAllBalance Flag indicating if all the user balance should be swapped + * uint256[] permitAmount List of amounts for the permit signature + * uint256[] deadline List of deadlines for the permit signature + * uint8[] v List of v param for the permit signature + * bytes32[] r List of r param for the permit signature + * bytes32[] s List of s param for the permit signature + * bool[] useEthPath true if the swap needs to occur using ETH in the routing, false otherwise + * @return SwapParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal pure returns (SwapParams memory) { + ( + address[] memory assetToSwapToList, + uint256[] memory minAmountsToReceive, + bool[] memory swapAllBalance, + uint256[] memory permitAmount, + uint256[] memory deadline, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s, + bool[] memory useEthPath + ) = + abi.decode( + params, + (address[], uint256[], bool[], uint256[], uint256[], uint8[], bytes32[], bytes32[], bool[]) + ); + + return + SwapParams( + assetToSwapToList, + minAmountsToReceive, + swapAllBalance, + PermitParams(permitAmount, deadline, v, r, s), + useEthPath + ); + } +} \ No newline at end of file diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol new file mode 100644 index 00000000..c4e7817e --- /dev/null +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; + +/** + * @title UniswapRepayAdapter + * @notice Uniswap V2 Adapter to perform a repay of a debt with collateral. + * @author Aave + **/ +contract UniswapRepayAdapter is BaseUniswapAdapter { + struct RepayParams { + address collateralAsset; + uint256 collateralAmount; + uint256 rateMode; + PermitSignature permitSignature; + bool useEthPath; + } + + constructor( + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter, + address wethAddress + ) public BaseUniswapAdapter(addressesProvider, uniswapRouter, wethAddress) {} + + /** + * @dev Uses the received funds from the flash loan to repay a debt on the protocol on behalf of the user. Then pulls + * the collateral from the user and swaps it to the debt asset to repay the flash loan. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset, swap it + * and repay the flash loan. + * Supports only one asset on the flash loan. + * @param assets Address of debt asset + * @param amounts Amount of the debt to be repaid + * @param premiums Fee of the flash loan + * @param initiator Address of the user + * @param params Additional variadic field to include extra params. Expected parameters: + * address collateralAsset Address of the reserve to be swapped + * uint256 collateralAmount Amount of reserve to be swapped + * uint256 rateMode Rate modes of the debt to be repaid + * uint256 permitAmount Amount for the permit signature + * uint256 deadline Deadline for the permit signature + * uint8 v V param for the permit signature + * bytes32 r R param for the permit signature + * bytes32 s S param for the permit signature + */ + function executeOperation( + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata params + ) external override returns (bool) { + require(msg.sender == address(LENDING_POOL), 'CALLER_MUST_BE_LENDING_POOL'); + + RepayParams memory decodedParams = _decodeParams(params); + + _swapAndRepay( + decodedParams.collateralAsset, + assets[0], + amounts[0], + decodedParams.collateralAmount, + decodedParams.rateMode, + initiator, + premiums[0], + decodedParams.permitSignature, + decodedParams.useEthPath + ); + + return true; + } + + /** + * @dev Swaps the user collateral for the debt asset and then repay the debt on the protocol on behalf of the user + * without using flash loans. This method can be used when the temporary transfer of the collateral asset to this + * contract does not affect the user position. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset + * @param collateralAsset Address of asset to be swapped + * @param debtAsset Address of debt asset + * @param collateralAmount Amount of the collateral to be swapped + * @param debtRepayAmount Amount of the debt to be repaid + * @param debtRateMode Rate mode of the debt to be repaid + * @param permitSignature struct containing the permit signature + * @param useEthPath struct containing the permit signature + + */ + function swapAndRepay( + address collateralAsset, + address debtAsset, + uint256 collateralAmount, + uint256 debtRepayAmount, + uint256 debtRateMode, + PermitSignature calldata permitSignature, + bool useEthPath + ) external { + DataTypes.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + DataTypes.ReserveData memory debtReserveData = _getReserveData(debtAsset); + + address debtToken = + DataTypes.InterestRateMode(debtRateMode) == DataTypes.InterestRateMode.STABLE + ? debtReserveData.stableDebtTokenAddress + : debtReserveData.variableDebtTokenAddress; + + uint256 currentDebt = IERC20(debtToken).balanceOf(msg.sender); + uint256 amountToRepay = debtRepayAmount <= currentDebt ? debtRepayAmount : currentDebt; + + if (collateralAsset != debtAsset) { + uint256 maxCollateralToSwap = collateralAmount; + if (amountToRepay < debtRepayAmount) { + maxCollateralToSwap = maxCollateralToSwap.mul(amountToRepay).div(debtRepayAmount); + } + + // Get exact collateral needed for the swap to avoid leftovers + uint256[] memory amounts = + _getAmountsIn(collateralAsset, debtAsset, amountToRepay, useEthPath); + require(amounts[0] <= maxCollateralToSwap, 'slippage too high'); + + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + msg.sender, + amounts[0], + permitSignature + ); + + // Swap collateral for debt asset + _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], amountToRepay, useEthPath); + } else { + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + msg.sender, + amountToRepay, + permitSignature + ); + } + + // Repay debt. Approves 0 first to comply with tokens that implement the anti frontrunning approval fix + IERC20(debtAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(debtAsset).safeApprove(address(LENDING_POOL), amountToRepay); + LENDING_POOL.repay(debtAsset, amountToRepay, debtRateMode, msg.sender); + } + + /** + * @dev Perform the repay of the debt, pulls the initiator collateral and swaps to repay the flash loan + * + * @param collateralAsset Address of token to be swapped + * @param debtAsset Address of debt token to be received from the swap + * @param amount Amount of the debt to be repaid + * @param collateralAmount Amount of the reserve to be swapped + * @param rateMode Rate mode of the debt to be repaid + * @param initiator Address of the user + * @param premium Fee of the flash loan + * @param permitSignature struct containing the permit signature + */ + function _swapAndRepay( + address collateralAsset, + address debtAsset, + uint256 amount, + uint256 collateralAmount, + uint256 rateMode, + address initiator, + uint256 premium, + PermitSignature memory permitSignature, + bool useEthPath + ) internal { + DataTypes.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + + // Repay debt. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(debtAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(debtAsset).safeApprove(address(LENDING_POOL), amount); + uint256 repaidAmount = IERC20(debtAsset).balanceOf(address(this)); + LENDING_POOL.repay(debtAsset, amount, rateMode, initiator); + repaidAmount = repaidAmount.sub(IERC20(debtAsset).balanceOf(address(this))); + + if (collateralAsset != debtAsset) { + uint256 maxCollateralToSwap = collateralAmount; + if (repaidAmount < amount) { + maxCollateralToSwap = maxCollateralToSwap.mul(repaidAmount).div(amount); + } + + uint256 neededForFlashLoanDebt = repaidAmount.add(premium); + uint256[] memory amounts = + _getAmountsIn(collateralAsset, debtAsset, neededForFlashLoanDebt, useEthPath); + require(amounts[0] <= maxCollateralToSwap, 'slippage too high'); + + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + initiator, + amounts[0], + permitSignature + ); + + // Swap collateral asset to the debt asset + _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], neededForFlashLoanDebt, useEthPath); + } else { + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + initiator, + repaidAmount.add(premium), + permitSignature + ); + } + + // Repay flashloan. Approves for 0 first to comply with tokens that implement the anti frontrunning approval fix. + IERC20(debtAsset).safeApprove(address(LENDING_POOL), 0); + IERC20(debtAsset).safeApprove(address(LENDING_POOL), amount.add(premium)); + } + + /** + * @dev Decodes debt information encoded in the flash loan params + * @param params Additional variadic field to include extra params. Expected parameters: + * address collateralAsset Address of the reserve to be swapped + * uint256 collateralAmount Amount of reserve to be swapped + * uint256 rateMode Rate modes of the debt to be repaid + * uint256 permitAmount Amount for the permit signature + * uint256 deadline Deadline for the permit signature + * uint8 v V param for the permit signature + * bytes32 r R param for the permit signature + * bytes32 s S param for the permit signature + * @return RepayParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal pure returns (RepayParams memory) { + ( + address collateralAsset, + uint256 collateralAmount, + uint256 rateMode, + uint256 permitAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s, + bool useEthPath + ) = + abi.decode( + params, + (address, uint256, uint256, uint256, uint256, uint8, bytes32, bytes32, bool) + ); + + return + RepayParams( + collateralAsset, + collateralAmount, + rateMode, + PermitSignature(permitAmount, deadline, v, r, s), + useEthPath + ); + } +} \ No newline at end of file diff --git a/contracts/adapters/interfaces/IBaseUniswapAdapter.sol b/contracts/adapters/interfaces/IBaseUniswapAdapter.sol new file mode 100644 index 00000000..82997b74 --- /dev/null +++ b/contracts/adapters/interfaces/IBaseUniswapAdapter.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; +pragma experimental ABIEncoderV2; + +import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; +import {IUniswapV2Router02} from '../../interfaces/IUniswapV2Router02.sol'; + +interface IBaseUniswapAdapter { + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + + struct PermitSignature { + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + struct AmountCalc { + uint256 calculatedAmount; + uint256 relativePrice; + uint256 amountInUsd; + uint256 amountOutUsd; + address[] path; + } + + function WETH_ADDRESS() external returns (address); + + function MAX_SLIPPAGE_PERCENT() external returns (uint256); + + function FLASHLOAN_PREMIUM_TOTAL() external returns (uint256); + + function USD_ADDRESS() external returns (address); + + function ORACLE() external returns (IPriceOracleGetter); + + function UNISWAP_ROUTER() external returns (IUniswapV2Router02); + + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset and the prices + * @param amountIn Amount of reserveIn + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount out of the reserveOut + * @return uint256 The price of out amount denominated in the reserveIn currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + * @return address[] The exchange path + */ + function getAmountsOut( + uint256 amountIn, + address reserveIn, + address reserveOut + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ); + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount and the prices + * @param amountOut Amount of reserveOut + * @param reserveIn Address of the asset to be swap from + * @param reserveOut Address of the asset to be swap to + * @return uint256 Amount in of the reserveIn + * @return uint256 The price of in amount denominated in the reserveOut currency (18 decimals) + * @return uint256 In amount of reserveIn value denominated in USD (8 decimals) + * @return uint256 Out amount of reserveOut value denominated in USD (8 decimals) + * @return address[] The exchange path + */ + function getAmountsIn( + uint256 amountOut, + address reserveIn, + address reserveOut + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + address[] memory + ); +} diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol new file mode 100644 index 00000000..46466b90 --- /dev/null +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +interface IERC20WithPermit is IERC20 { + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/contracts/interfaces/IUniswapV2Router02.sol b/contracts/interfaces/IUniswapV2Router02.sol new file mode 100644 index 00000000..af0f8280 --- /dev/null +++ b/contracts/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +interface IUniswapV2Router02 { + function swapExactTokensForTokens( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); + + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint256[] memory amounts); + + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); +} diff --git a/contracts/mocks/swap/MockUniswapV2Router02.sol b/contracts/mocks/swap/MockUniswapV2Router02.sol new file mode 100644 index 00000000..b7fd3f80 --- /dev/null +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity 0.6.12; + +import {IUniswapV2Router02} from '../../interfaces/IUniswapV2Router02.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {MintableERC20} from '../tokens/MintableERC20.sol'; + +contract MockUniswapV2Router02 is IUniswapV2Router02 { + mapping(address => uint256) internal _amountToReturn; + mapping(address => uint256) internal _amountToSwap; + mapping(address => mapping(address => mapping(uint256 => uint256))) internal _amountsIn; + mapping(address => mapping(address => mapping(uint256 => uint256))) internal _amountsOut; + uint256 internal defaultMockValue; + + function setAmountToReturn(address reserve, uint256 amount) public { + _amountToReturn[reserve] = amount; + } + + function setAmountToSwap(address reserve, uint256 amount) public { + _amountToSwap[reserve] = amount; + } + + function swapExactTokensForTokens( + uint256 amountIn, + uint256, /* amountOutMin */ + address[] calldata path, + address to, + uint256 /* deadline */ + ) external override returns (uint256[] memory amounts) { + IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn); + + MintableERC20(path[1]).mint(_amountToReturn[path[0]]); + IERC20(path[1]).transfer(to, _amountToReturn[path[0]]); + + amounts = new uint256[](path.length); + amounts[0] = amountIn; + amounts[1] = _amountToReturn[path[0]]; + } + + function swapTokensForExactTokens( + uint256 amountOut, + uint256, /* amountInMax */ + address[] calldata path, + address to, + uint256 /* deadline */ + ) external override returns (uint256[] memory amounts) { + IERC20(path[0]).transferFrom(msg.sender, address(this), _amountToSwap[path[0]]); + + MintableERC20(path[1]).mint(amountOut); + IERC20(path[1]).transfer(to, amountOut); + + amounts = new uint256[](path.length); + amounts[0] = _amountToSwap[path[0]]; + amounts[1] = amountOut; + } + + function setAmountOut( + uint256 amountIn, + address reserveIn, + address reserveOut, + uint256 amountOut + ) public { + _amountsOut[reserveIn][reserveOut][amountIn] = amountOut; + } + + function setAmountIn( + uint256 amountOut, + address reserveIn, + address reserveOut, + uint256 amountIn + ) public { + _amountsIn[reserveIn][reserveOut][amountOut] = amountIn; + } + + function setDefaultMockValue(uint256 value) public { + defaultMockValue = value; + } + + function getAmountsOut(uint256 amountIn, address[] calldata path) + external + view + override + returns (uint256[] memory) + { + uint256[] memory amounts = new uint256[](path.length); + amounts[0] = amountIn; + amounts[1] = _amountsOut[path[0]][path[1]][amountIn] > 0 + ? _amountsOut[path[0]][path[1]][amountIn] + : defaultMockValue; + return amounts; + } + + function getAmountsIn(uint256 amountOut, address[] calldata path) + external + view + override + returns (uint256[] memory) + { + uint256[] memory amounts = new uint256[](path.length); + amounts[0] = _amountsIn[path[0]][path[1]][amountOut] > 0 + ? _amountsIn[path[0]][path[1]][amountOut] + : defaultMockValue; + amounts[1] = amountOut; + return amounts; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 1a5b2763..a6d411ea 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -6,6 +6,8 @@ import { accounts } from './test-wallets.js'; import { eEthereumNetwork } from './helpers/types'; import { BUIDLEREVM_CHAINID, COVERAGE_CHAINID } from './helpers/buidler-constants'; +require('dotenv').config(); + import '@nomiclabs/hardhat-ethers'; import '@nomiclabs/hardhat-waffle'; import 'temp-hardhat-etherscan'; @@ -16,7 +18,7 @@ import '@tenderly/hardhat-tenderly'; const SKIP_LOAD = process.env.SKIP_LOAD === 'true'; const DEFAULT_BLOCK_GAS_LIMIT = 12450000; const DEFAULT_GAS_MUL = 5; -const DEFAULT_GAS_PRICE = 2000000000; +const DEFAULT_GAS_PRICE = 65000000000; const HARDFORK = 'istanbul'; const INFURA_KEY = process.env.INFURA_KEY || ''; const ALCHEMY_KEY = process.env.ALCHEMY_KEY || ''; @@ -27,14 +29,16 @@ const MAINNET_FORK = process.env.MAINNET_FORK === 'true'; // Prevent to load scripts before compilation and typechain if (!SKIP_LOAD) { - ['misc', 'migrations', 'dev', 'full', 'verifications', 'helpers'].forEach((folder) => { - const tasksPath = path.join(__dirname, 'tasks', folder); - fs.readdirSync(tasksPath) - .filter((pth) => pth.includes('.ts')) - .forEach((task) => { - require(`${tasksPath}/${task}`); - }); - }); + ['misc', 'migrations', 'dev', 'full', 'verifications', 'deployments', 'helpers'].forEach( + (folder) => { + const tasksPath = path.join(__dirname, 'tasks', folder); + fs.readdirSync(tasksPath) + .filter((pth) => pth.includes('.ts')) + .forEach((task) => { + require(`${tasksPath}/${task}`); + }); + } + ); } require(`${path.join(__dirname, 'tasks/misc')}/set-bre.ts`); diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 322e4f18..4d1f0617 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -38,10 +38,13 @@ import { MockFlashLoanReceiverFactory, MockStableDebtTokenFactory, MockVariableDebtTokenFactory, + MockUniswapV2Router02Factory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, StableDebtTokenFactory, + UniswapLiquiditySwapAdapterFactory, + UniswapRepayAdapterFactory, VariableDebtTokenFactory, WalletBalanceProviderFactory, WETH9MockedFactory, @@ -318,7 +321,7 @@ export const deployVariableDebtToken = async ( ); export const deployGenericAToken = async ( - [poolAddress, underlyingAssetAddress, treasuryAddress, name, symbol,incentivesController]: [ + [poolAddress, underlyingAssetAddress, treasuryAddress, name, symbol, incentivesController]: [ tEthereumAddress, tEthereumAddress, tEthereumAddress, @@ -335,7 +338,6 @@ export const deployGenericAToken = async ( string, tEthereumAddress, tEthereumAddress - ] = [poolAddress, underlyingAssetAddress, treasuryAddress, name, symbol, incentivesController]; return withSaveAndVerify( await new ATokenFactory(await getFirstSigner()).deploy(...args), @@ -493,3 +495,33 @@ export const deploySelfdestructTransferMock = async (verify?: boolean) => [], verify ); + +export const deployMockUniswapRouter = async (verify?: boolean) => + withSaveAndVerify( + await new MockUniswapV2Router02Factory(await getFirstSigner()).deploy(), + eContractid.MockUniswapV2Router02, + [], + verify + ); + +export const deployUniswapLiquiditySwapAdapter = async ( + args: [tEthereumAddress, tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new UniswapLiquiditySwapAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.UniswapLiquiditySwapAdapter, + args, + verify + ); + +export const deployUniswapRepayAdapter = async ( + args: [tEthereumAddress, tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new UniswapRepayAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.UniswapRepayAdapter, + args, + verify + ); diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 65834cc6..04ac662a 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -17,11 +17,14 @@ import { MockFlashLoanReceiverFactory, MockStableDebtTokenFactory, MockVariableDebtTokenFactory, + MockUniswapV2Router02Factory, PriceOracleFactory, ReserveLogicFactory, SelfdestructTransferFactory, StableAndVariableTokensHelperFactory, StableDebtTokenFactory, + UniswapLiquiditySwapAdapterFactory, + UniswapRepayAdapterFactory, VariableDebtTokenFactory, WalletBalanceProviderFactory, WETH9MockedFactory, @@ -328,3 +331,26 @@ export const getAaveOracle = async (address?: tEthereumAddress) => address || (await getDb().get(`${eContractid.AaveOracle}.${DRE.network.name}`).value()).address, await getFirstSigner() ); + +export const getMockUniswapRouter = async (address?: tEthereumAddress) => + await MockUniswapV2Router02Factory.connect( + address || + (await getDb().get(`${eContractid.MockUniswapV2Router02}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getUniswapLiquiditySwapAdapter = async (address?: tEthereumAddress) => + await UniswapLiquiditySwapAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${DRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => + await UniswapRepayAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.UniswapRepayAdapter}.${DRE.network.name}`).value()).address, + await getFirstSigner() + ); diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 93f6bb30..e18de87d 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -1,4 +1,4 @@ -import { Contract, Signer, utils, ethers } from 'ethers'; +import { Contract, Signer, utils, ethers, BigNumberish } from 'ethers'; import { signTypedData_v4 } from 'eth-sig-util'; import { fromRpcSig, ECDSASignature } from 'ethereumjs-util'; import BigNumber from 'bignumber.js'; @@ -232,3 +232,57 @@ export const getSignatureFromTypedData = ( }); return fromRpcSig(signature); }; + +export const buildLiquiditySwapParams = ( + assetToSwapToList: tEthereumAddress[], + minAmountsToReceive: BigNumberish[], + swapAllBalances: BigNumberish[], + permitAmounts: BigNumberish[], + deadlines: BigNumberish[], + v: BigNumberish[], + r: (string | Buffer)[], + s: (string | Buffer)[], + useEthPath: boolean[] +) => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256[]', + 'bool[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + 'bool[]', + ], + [ + assetToSwapToList, + minAmountsToReceive, + swapAllBalances, + permitAmounts, + deadlines, + v, + r, + s, + useEthPath, + ] + ); +}; + +export const buildRepayAdapterParams = ( + collateralAsset: tEthereumAddress, + collateralAmount: BigNumberish, + rateMode: BigNumberish, + permitAmount: BigNumberish, + deadline: BigNumberish, + v: BigNumberish, + r: string | Buffer, + s: string | Buffer, + useEthPath: boolean +) => { + return ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint256', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32', 'bool'], + [collateralAsset, collateralAmount, rateMode, permitAmount, deadline, v, r, s, useEthPath] + ); +}; diff --git a/helpers/types.ts b/helpers/types.ts index 35211dd1..7b2e6662 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -67,6 +67,9 @@ export enum eContractid { LendingPoolImpl = 'LendingPoolImpl', LendingPoolConfiguratorImpl = 'LendingPoolConfiguratorImpl', LendingPoolCollateralManagerImpl = 'LendingPoolCollateralManagerImpl', + MockUniswapV2Router02 = 'MockUniswapV2Router02', + UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter', + UniswapRepayAdapter = 'UniswapRepayAdapter', } /* diff --git a/package-lock.json b/package-lock.json index 32738e30..3ea8f296 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9504,14 +9504,6 @@ "prr": "~1.0.1", "semver": "~5.4.1", "xtend": "~4.0.0" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "merkle-patricia-tree": { @@ -9901,14 +9893,6 @@ "prr": "~1.0.1", "semver": "~5.4.1", "xtend": "~4.0.0" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "merkle-patricia-tree": { @@ -10200,14 +10184,6 @@ "prr": "~1.0.1", "semver": "~5.4.1", "xtend": "~4.0.0" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "merkle-patricia-tree": { @@ -10459,14 +10435,6 @@ "prr": "~1.0.1", "semver": "~5.4.1", "xtend": "~4.0.0" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "merkle-patricia-tree": { @@ -12428,6 +12396,17 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "safe-buffer": { @@ -12436,6 +12415,11 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -14250,6 +14234,12 @@ "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", "dev": true }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, "semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -16306,14 +16296,6 @@ "prr": "~1.0.1", "semver": "~5.4.1", "xtend": "~4.0.0" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } } }, "merkle-patricia-tree": { diff --git a/package.json b/package.json index 7314ef39..4fefea1b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test-stable-and-atokens": "hardhat test test/__setup.spec.ts test/atoken-transfer.spec.ts test/stable-token.spec.ts", "test-subgraph:scenarios": "hardhat --network hardhatevm_docker test test/__setup.spec.ts test/subgraph-scenarios.spec.ts", "test-weth": "hardhat test test/__setup.spec.ts test/weth-gateway.spec.ts", + "test-uniswap": "hardhat test test/__setup.spec.ts test/uniswapAdapters*.spec.ts", "test:main:check-list": "MAINNET_FORK=true TS_NODE_TRANSPILE_ONLY=1 hardhat test test/__setup.spec.ts test/mainnet/check-list.spec.ts", "dev:coverage": "buidler compile --force && buidler coverage --network coverage", "aave:evm:dev:migration": "npm run compile && hardhat aave:dev", @@ -45,6 +46,10 @@ "print-contracts:main": "npm run hardhat:main -- print-contracts", "print-contracts:ropsten": "npm run hardhat:main -- print-contracts", "dev:deployUIProvider": "npm run hardhat:kovan deploy-UiPoolDataProvider", + "dev:deployUniswapRepayAdapter": "hardhat --network kovan deploy-UniswapRepayAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", + "dev:UniswapLiquiditySwapAdapter": "hardhat --network kovan deploy-UniswapLiquiditySwapAdapter --provider 0x88757f2f99175387aB4C6a4b3067c77A695b0349 --router 0xfcd87315f0e4067070ade8682fcdbc3006631441 --weth 0xd0a1e359811322d97991e03f863a0c30c2cf029c", + "main:deployUniswapRepayAdapter": "hardhat --network main deploy-UniswapRepayAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "main:UniswapLiquiditySwapAdapter": "hardhat --network main deploy-UniswapLiquiditySwapAdapter --provider 0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5 --router 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D --weth 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "kovan:verify": "npm run hardhat:kovan verify:general -- --all --pool Aave", "ropsten:verify": "npm run hardhat:ropsten verify:general -- --all --pool Aave", "mainnet:verify": "npm run hardhat:main verify:general -- --all --pool Aave", diff --git a/specs/harness/LendingPoolHarnessForVariableDebtToken.sol b/specs/harness/LendingPoolHarnessForVariableDebtToken.sol index 02b5a4b7..e013885c 100644 --- a/specs/harness/LendingPoolHarnessForVariableDebtToken.sol +++ b/specs/harness/LendingPoolHarnessForVariableDebtToken.sol @@ -96,17 +96,17 @@ contract LendingPoolHarnessForVariableDebtToken is ILendingPool { } function getUserAccountData(address user) - external - view - override - returns ( - uint256 totalCollateralETH, - uint256 totalDebtETH, - uint256 availableBorrowsETH, - uint256 currentLiquidationThreshold, - uint256 ltv, - uint256 healthFactor - ) + external + view + override + returns ( + uint256 totalCollateralETH, + uint256 totalDebtETH, + uint256 availableBorrowsETH, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ) { return originalPool.getUserAccountData(user); } diff --git a/specs/harness/StableDebtTokenHarness.sol b/specs/harness/StableDebtTokenHarness.sol index 1218e0d9..69d8052f 100644 --- a/specs/harness/StableDebtTokenHarness.sol +++ b/specs/harness/StableDebtTokenHarness.sol @@ -11,24 +11,23 @@ contract StableDebtTokenHarness is StableDebtToken { string memory symbol, address incentivesController ) public StableDebtToken(pool, underlyingAsset, name, symbol, incentivesController) {} - - - /** + + /** Simplification: The user accumulates no interest (the balance increase is always 0). **/ - function balanceOf(address account) public override view returns (uint256) { + function balanceOf(address account) public view override returns (uint256) { return IncentivizedERC20.balanceOf(account); } - function _calcTotalSupply(uint256 avgRate) internal override view returns (uint256) { + function _calcTotalSupply(uint256 avgRate) internal view override returns (uint256) { return IncentivizedERC20.totalSupply(); } function getIncentivesController() public view returns (address) { return address(_incentivesController); } - - function rayWadMul(uint256 aRay, uint256 bWad) external view returns(uint256) { - return aRay.rayMul(bWad.wadToRay()); + + function rayWadMul(uint256 aRay, uint256 bWad) external view returns (uint256) { + return aRay.rayMul(bWad.wadToRay()); } } diff --git a/specs/harness/UserConfigurationHarness.sol b/specs/harness/UserConfigurationHarness.sol index 4487773b..da5f0113 100644 --- a/specs/harness/UserConfigurationHarness.sol +++ b/specs/harness/UserConfigurationHarness.sol @@ -1,35 +1,26 @@ pragma solidity 0.6.12; pragma experimental ABIEncoderV2; -import {UserConfiguration} from '../../contracts/protocol/libraries/configuration/UserConfiguration.sol'; +import { + UserConfiguration +} from '../../contracts/protocol/libraries/configuration/UserConfiguration.sol'; import {DataTypes} from '../../contracts/protocol/libraries/types/DataTypes.sol'; /* A wrapper contract for calling functions from the library UserConfiguration. */ contract UserConfigurationHarness { - DataTypes.UserConfigurationMap internal usersConfig; - function setBorrowing( - uint256 reserveIndex, - bool borrowing - ) public { + function setBorrowing(uint256 reserveIndex, bool borrowing) public { UserConfiguration.setBorrowing(usersConfig, reserveIndex, borrowing); } - function setUsingAsCollateral( - uint256 reserveIndex, - bool _usingAsCollateral - ) public { + function setUsingAsCollateral(uint256 reserveIndex, bool _usingAsCollateral) public { UserConfiguration.setUsingAsCollateral(usersConfig, reserveIndex, _usingAsCollateral); } - function isUsingAsCollateralOrBorrowing(uint256 reserveIndex) - public - view - returns (bool) - { + function isUsingAsCollateralOrBorrowing(uint256 reserveIndex) public view returns (bool) { return UserConfiguration.isUsingAsCollateralOrBorrowing(usersConfig, reserveIndex); } diff --git a/tasks/deployments/deploy-UiPoolDataProvider.ts b/tasks/deployments/deploy-UiPoolDataProvider.ts index b03ca70d..2d696702 100644 --- a/tasks/deployments/deploy-UiPoolDataProvider.ts +++ b/tasks/deployments/deploy-UiPoolDataProvider.ts @@ -1,13 +1,13 @@ -import {task} from '@nomiclabs/buidler/config'; +import { task } from 'hardhat/config'; -import {UiPoolDataProviderFactory} from '../../types'; -import {verifyContract} from '../../helpers/etherscan-verification'; -import {eContractid} from '../../helpers/types'; +import { UiPoolDataProviderFactory } from '../../types'; +import { verifyContract } from '../../helpers/etherscan-verification'; +import { eContractid } from '../../helpers/types'; task(`deploy-${eContractid.UiPoolDataProvider}`, `Deploys the UiPoolDataProvider contract`) .addFlag('verify', 'Verify UiPoolDataProvider contract via Etherscan API.') - .setAction(async ({verify}, localBRE) => { - await localBRE.run('set-bre'); + .setAction(async ({ verify }, localBRE) => { + await localBRE.run('set-DRE'); if (!localBRE.network.config.chainId) { throw new Error('INVALID_CHAIN_ID'); @@ -21,7 +21,7 @@ task(`deploy-${eContractid.UiPoolDataProvider}`, `Deploys the UiPoolDataProvider ).deploy(); await uiPoolDataProvider.deployTransaction.wait(); console.log('uiPoolDataProvider.address', uiPoolDataProvider.address); - await verifyContract(eContractid.UiPoolDataProvider, uiPoolDataProvider.address, []); + await verifyContract(uiPoolDataProvider.address, []); console.log(`\tFinished UiPoolDataProvider proxy and implementation deployment`); }); diff --git a/tasks/deployments/deploy-UniswapLiquiditySwapAdapter.ts b/tasks/deployments/deploy-UniswapLiquiditySwapAdapter.ts new file mode 100644 index 00000000..a41cd975 --- /dev/null +++ b/tasks/deployments/deploy-UniswapLiquiditySwapAdapter.ts @@ -0,0 +1,35 @@ +import { task } from 'hardhat/config'; + +import { UniswapLiquiditySwapAdapterFactory } from '../../types'; +import { verifyContract } from '../../helpers/etherscan-verification'; +import { getFirstSigner } from '../../helpers/contracts-getters'; + +const CONTRACT_NAME = 'UniswapLiquiditySwapAdapter'; + +task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) + .addParam('provider', 'Address of the LendingPoolAddressesProvider') + .addParam('router', 'Address of the uniswap router') + .addParam('weth', 'Address of the weth token') + .addFlag('verify', `Verify ${CONTRACT_NAME} contract via Etherscan API.`) + .setAction(async ({ provider, router, weth, verify }, localBRE) => { + await localBRE.run('set-DRE'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + console.log(`\n- ${CONTRACT_NAME} deployment`); + /*const args = [ + '0x88757f2f99175387aB4C6a4b3067c77A695b0349', // lending provider kovan address + '0xfcd87315f0e4067070ade8682fcdbc3006631441', // uniswap router address + ]; + */ + const uniswapRepayAdapter = await new UniswapLiquiditySwapAdapterFactory( + await getFirstSigner() + ).deploy(provider, router, weth); + await uniswapRepayAdapter.deployTransaction.wait(); + console.log(`${CONTRACT_NAME}.address`, uniswapRepayAdapter.address); + await verifyContract(uniswapRepayAdapter.address, [provider, router, weth]); + + console.log(`\tFinished ${CONTRACT_NAME} proxy and implementation deployment`); + }); diff --git a/tasks/deployments/deploy-UniswapRepayAdapter.ts b/tasks/deployments/deploy-UniswapRepayAdapter.ts new file mode 100644 index 00000000..58ba23a1 --- /dev/null +++ b/tasks/deployments/deploy-UniswapRepayAdapter.ts @@ -0,0 +1,38 @@ +import { task } from 'hardhat/config'; + +import { UniswapRepayAdapterFactory } from '../../types'; +import { verifyContract } from '../../helpers/etherscan-verification'; +import { getFirstSigner } from '../../helpers/contracts-getters'; + +const CONTRACT_NAME = 'UniswapRepayAdapter'; + +task(`deploy-${CONTRACT_NAME}`, `Deploys the ${CONTRACT_NAME} contract`) + .addParam('provider', 'Address of the LendingPoolAddressesProvider') + .addParam('router', 'Address of the uniswap router') + .addParam('weth', 'Address of the weth token') + .addFlag('verify', `Verify ${CONTRACT_NAME} contract via Etherscan API.`) + .setAction(async ({ provider, router, weth, verify }, localBRE) => { + await localBRE.run('set-DRE'); + + if (!localBRE.network.config.chainId) { + throw new Error('INVALID_CHAIN_ID'); + } + + console.log(`\n- ${CONTRACT_NAME} deployment`); + // const args = [ + // '0x88757f2f99175387aB4C6a4b3067c77A695b0349', // lending provider kovan address + // '0xfcd87315f0e4067070ade8682fcdbc3006631441', // uniswap router address + // ]; + const uniswapRepayAdapter = await new UniswapRepayAdapterFactory(await getFirstSigner()).deploy( + provider, + router, + weth + ); + await uniswapRepayAdapter.deployTransaction.wait(); + console.log(`${CONTRACT_NAME}.address`, uniswapRepayAdapter.address); + await verifyContract(uniswapRepayAdapter.address, [provider, router, weth]); + + console.log( + `\tFinished ${CONTRACT_NAME}${CONTRACT_NAME}lDataProvider proxy and implementation deployment` + ); + }); diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 45479bb7..de3478af 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -22,11 +22,19 @@ import { deployATokensAndRatesHelper, deployWETHGateway, deployWETHMocked, + deployMockUniswapRouter, + deployUniswapLiquiditySwapAdapter, + deployUniswapRepayAdapter, } from '../helpers/contracts-deployments'; import { Signer } from 'ethers'; import { TokenContractId, eContractid, tEthereumAddress, AavePools } from '../helpers/types'; import { MintableERC20 } from '../types/MintableERC20'; -import { ConfigNames, getReservesConfigByPool, getTreasuryAddress, loadPoolConfig } from '../helpers/configuration'; +import { + ConfigNames, + getReservesConfigByPool, + getTreasuryAddress, + loadPoolConfig, +} from '../helpers/configuration'; import { initializeMakeSuite } from './helpers/make-suite'; import { @@ -35,10 +43,7 @@ import { setInitialMarketRatesInRatesOracleByHelper, } from '../helpers/oracles-helpers'; import { DRE, waitForTx } from '../helpers/misc-utils'; -import { - initReservesByHelper, - configureReservesByHelper, -} from '../helpers/init-helpers'; +import { initReservesByHelper, configureReservesByHelper } from '../helpers/init-helpers'; import AaveConfig from '../markets/aave'; import { ZERO_ADDRESS } from '../helpers/constants'; import { @@ -213,13 +218,15 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const treasuryAddress = await getTreasuryAddress(config); - await initReservesByHelper(reservesParams, allReservesAddresses, admin, treasuryAddress, ZERO_ADDRESS, false); - await configureReservesByHelper( + await initReservesByHelper( reservesParams, allReservesAddresses, - testHelpers, - admin + admin, + treasuryAddress, + ZERO_ADDRESS, + false ); + await configureReservesByHelper(reservesParams, allReservesAddresses, testHelpers, admin); const collateralManager = await deployLendingPoolCollateralManager(); await waitForTx( @@ -229,6 +236,26 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); + const mockUniswapRouter = await deployMockUniswapRouter(); + await insertContractAddressInDb(eContractid.MockUniswapV2Router02, mockUniswapRouter.address); + + const UniswapLiquiditySwapAdapter = await deployUniswapLiquiditySwapAdapter([ + addressesProvider.address, + mockUniswapRouter.address, + mockTokens.WETH.address, + ]); + await insertContractAddressInDb( + eContractid.UniswapLiquiditySwapAdapter, + UniswapLiquiditySwapAdapter.address + ); + + const UniswapRepayAdapter = await deployUniswapRepayAdapter([ + addressesProvider.address, + mockUniswapRouter.address, + mockTokens.WETH.address, + ]); + await insertContractAddressInDb(eContractid.UniswapRepayAdapter, UniswapRepayAdapter.address); + await deployWalletBalancerProvider(); await deployWETHGateway([mockTokens.WETH.address, lendingPoolAddress]); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index b30111b8..5395c903 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -11,6 +11,8 @@ import { getLendingPoolAddressesProviderRegistry, getWETHMocked, getWETHGateway, + getUniswapLiquiditySwapAdapter, + getUniswapRepayAdapter, } from '../../helpers/contracts-getters'; import { eEthereumNetwork, tEthereumAddress } from '../../helpers/types'; import { LendingPool } from '../../types/LendingPool'; @@ -26,7 +28,10 @@ import { almostEqual } from './almost-equal'; import { PriceOracle } from '../../types/PriceOracle'; import { LendingPoolAddressesProvider } from '../../types/LendingPoolAddressesProvider'; import { LendingPoolAddressesProviderRegistry } from '../../types/LendingPoolAddressesProviderRegistry'; -import { getEthersSigners, getParamPerNetwork } from '../../helpers/contracts-helpers'; +import { getEthersSigners } from '../../helpers/contracts-helpers'; +import { UniswapLiquiditySwapAdapter } from '../../types/UniswapLiquiditySwapAdapter'; +import { UniswapRepayAdapter } from '../../types/UniswapRepayAdapter'; +import { getParamPerNetwork } from '../../helpers/contracts-helpers'; import { WETH9Mocked } from '../../types/WETH9Mocked'; import { WETHGateway } from '../../types/WETHGateway'; import { solidity } from 'ethereum-waffle'; @@ -54,6 +59,8 @@ export interface TestEnv { usdc: MintableERC20; aave: MintableERC20; addressesProvider: LendingPoolAddressesProvider; + uniswapLiquiditySwapAdapter: UniswapLiquiditySwapAdapter; + uniswapRepayAdapter: UniswapRepayAdapter; registry: LendingPoolAddressesProviderRegistry; wethGateway: WETHGateway; } @@ -79,6 +86,8 @@ const testEnv: TestEnv = { usdc: {} as MintableERC20, aave: {} as MintableERC20, addressesProvider: {} as LendingPoolAddressesProvider, + uniswapLiquiditySwapAdapter: {} as UniswapLiquiditySwapAdapter, + uniswapRepayAdapter: {} as UniswapRepayAdapter, registry: {} as LendingPoolAddressesProviderRegistry, wethGateway: {} as WETHGateway, } as TestEnv; @@ -141,6 +150,9 @@ export async function initializeMakeSuite() { testEnv.aave = await getMintableERC20(aaveAddress); testEnv.weth = await getWETHMocked(wethAddress); testEnv.wethGateway = await getWETHGateway(); + + testEnv.uniswapLiquiditySwapAdapter = await getUniswapLiquiditySwapAdapter(); + testEnv.uniswapRepayAdapter = await getUniswapRepayAdapter(); } export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { diff --git a/test/uniswapAdapters.base.spec.ts b/test/uniswapAdapters.base.spec.ts new file mode 100644 index 00000000..80f76725 --- /dev/null +++ b/test/uniswapAdapters.base.spec.ts @@ -0,0 +1,227 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; +import { getMockUniswapRouter } from '../helpers/contracts-getters'; +import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; +import BigNumber from 'bignumber.js'; +import { evmRevert, evmSnapshot } from '../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { USD_ADDRESS } from '../helpers/constants'; +const { parseEther } = ethers.utils; + +const { expect } = require('chai'); + +makeSuite('Uniswap adapters', (testEnv: TestEnv) => { + let mockUniswapRouter: MockUniswapV2Router02; + let evmSnapshotId: string; + + before(async () => { + mockUniswapRouter = await getMockUniswapRouter(); + }); + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + + describe('BaseUniswapAdapter', () => { + describe('getAmountsOut', () => { + it('should return the estimated amountOut and prices for the asset swap', async () => { + const { weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; + + const amountIn = parseEther('1'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); + + const wethPrice = await oracle.getAssetPrice(weth.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); + + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountToSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const outPerInPrice = amountToSwap + .mul(parseEther('1')) + .mul(parseEther('1')) + .div(expectedDaiAmount.mul(parseEther('1'))); + const ethUsdValue = amountIn + .mul(wethPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + const daiUsdValue = expectedDaiAmount + .mul(daiPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountOut( + amountToSwap, + weth.address, + dai.address, + expectedDaiAmount + ); + + const result = await uniswapLiquiditySwapAdapter.getAmountsOut( + amountIn, + weth.address, + dai.address + ); + + expect(result['0']).to.be.eq(expectedDaiAmount); + expect(result['1']).to.be.eq(outPerInPrice); + expect(result['2']).to.be.eq(ethUsdValue); + expect(result['3']).to.be.eq(daiUsdValue); + }); + it('should work correctly with different decimals', async () => { + const { aave, usdc, uniswapLiquiditySwapAdapter, oracle } = testEnv; + + const amountIn = parseEther('10'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); + + const aavePrice = await oracle.getAssetPrice(aave.address); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); + + const expectedUSDCAmount = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(amountToSwap.toString()).div(usdcPrice.toString()).toFixed(0) + ); + + const outPerInPrice = amountToSwap + .mul(parseEther('1')) + .mul('1000000') // usdc 6 decimals + .div(expectedUSDCAmount.mul(parseEther('1'))); + + const aaveUsdValue = amountIn + .mul(aavePrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + const usdcUsdValue = expectedUSDCAmount + .mul(usdcPrice) + .div('1000000') // usdc 6 decimals + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountOut( + amountToSwap, + aave.address, + usdc.address, + expectedUSDCAmount + ); + + const result = await uniswapLiquiditySwapAdapter.getAmountsOut( + amountIn, + aave.address, + usdc.address + ); + + expect(result['0']).to.be.eq(expectedUSDCAmount); + expect(result['1']).to.be.eq(outPerInPrice); + expect(result['2']).to.be.eq(aaveUsdValue); + expect(result['3']).to.be.eq(usdcUsdValue); + }); + }); + + describe('getAmountsIn', () => { + it('should return the estimated required amountIn for the asset swap', async () => { + const { weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; + + const amountIn = parseEther('1'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.add(flashloanPremium); + + const wethPrice = await oracle.getAssetPrice(weth.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); + + const amountOut = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountIn.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const inPerOutPrice = amountOut + .mul(parseEther('1')) + .mul(parseEther('1')) + .div(amountToSwap.mul(parseEther('1'))); + + const ethUsdValue = amountToSwap + .mul(wethPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + const daiUsdValue = amountOut + .mul(daiPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountIn(amountOut, weth.address, dai.address, amountIn); + + const result = await uniswapLiquiditySwapAdapter.getAmountsIn( + amountOut, + weth.address, + dai.address + ); + + expect(result['0']).to.be.eq(amountToSwap); + expect(result['1']).to.be.eq(inPerOutPrice); + expect(result['2']).to.be.eq(ethUsdValue); + expect(result['3']).to.be.eq(daiUsdValue); + }); + it('should work correctly with different decimals', async () => { + const { aave, usdc, uniswapLiquiditySwapAdapter, oracle } = testEnv; + + const amountIn = parseEther('10'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.add(flashloanPremium); + + const aavePrice = await oracle.getAssetPrice(aave.address); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); + + const amountOut = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(amountToSwap.toString()).div(usdcPrice.toString()).toFixed(0) + ); + + const inPerOutPrice = amountOut + .mul(parseEther('1')) + .mul(parseEther('1')) + .div(amountToSwap.mul('1000000')); // usdc 6 decimals + + const aaveUsdValue = amountToSwap + .mul(aavePrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + const usdcUsdValue = amountOut + .mul(usdcPrice) + .div('1000000') // usdc 6 decimals + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountIn(amountOut, aave.address, usdc.address, amountIn); + + const result = await uniswapLiquiditySwapAdapter.getAmountsIn( + amountOut, + aave.address, + usdc.address + ); + + expect(result['0']).to.be.eq(amountToSwap); + expect(result['1']).to.be.eq(inPerOutPrice); + expect(result['2']).to.be.eq(aaveUsdValue); + expect(result['3']).to.be.eq(usdcUsdValue); + }); + }); + }); +}); diff --git a/test/uniswapAdapters.liquiditySwap.spec.ts b/test/uniswapAdapters.liquiditySwap.spec.ts new file mode 100644 index 00000000..1e30b2b3 --- /dev/null +++ b/test/uniswapAdapters.liquiditySwap.spec.ts @@ -0,0 +1,1854 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, + buildLiquiditySwapParams, +} from '../helpers/contracts-helpers'; +import { getMockUniswapRouter } from '../helpers/contracts-getters'; +import { deployUniswapLiquiditySwapAdapter } from '../helpers/contracts-deployments'; +import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; +import { Zero } from '@ethersproject/constants'; +import BigNumber from 'bignumber.js'; +import { DRE, evmRevert, evmSnapshot } from '../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { eContractid } from '../helpers/types'; +import { AToken } from '../types/AToken'; +import { BUIDLEREVM_CHAINID } from '../helpers/buidler-constants'; +import { MAX_UINT_AMOUNT } from '../helpers/constants'; +const { parseEther } = ethers.utils; + +const { expect } = require('chai'); + +makeSuite('Uniswap adapters', (testEnv: TestEnv) => { + let mockUniswapRouter: MockUniswapV2Router02; + let evmSnapshotId: string; + + before(async () => { + mockUniswapRouter = await getMockUniswapRouter(); + }); + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + + describe('UniswapLiquiditySwapAdapter', () => { + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const { addressesProvider, weth } = testEnv; + await deployUniswapLiquiditySwapAdapter([ + addressesProvider.address, + mockUniswapRouter.address, + weth.address, + ]); + }); + + it('should revert if not valid addresses provider', async () => { + const { weth } = testEnv; + expect( + deployUniswapLiquiditySwapAdapter([ + mockUniswapRouter.address, + mockUniswapRouter.address, + weth.address, + ]) + ).to.be.reverted; + }); + }); + + describe('executeOperation', () => { + beforeEach(async () => { + const { users, weth, dai, usdc, pool, deployer } = testEnv; + const userAddress = users[0].address; + + // Provide liquidity + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + + const usdcAmount = await convertToCurrencyDecimals(usdc.address, '10'); + await usdc.mint(usdcAmount); + await usdc.approve(pool.address, usdcAmount); + await pool.deposit(usdc.address, usdcAmount, deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap and deposit multiple tokens', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + usdc, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmountForUsdc = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Make a deposit for user + await usdc.connect(user).mint(amountUSDCtoSwap); + await usdc.connect(user).approve(pool.address, amountUSDCtoSwap); + await pool.connect(user).deposit(usdc.address, amountUSDCtoSwap, userAddress, 0); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmountForEth); + await mockUniswapRouter.setAmountToReturn(usdc.address, expectedDaiAmountForUsdc); + + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aUsdc.connect(user).approve(uniswapLiquiditySwapAdapter.address, amountUSDCtoSwap); + const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); + + // Subtract the FL fee from the amount to be swapped 0,09% + const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) + .div(1.0009) + .toFixed(0); + const usdcFlashloanAmount = new BigNumber(amountUSDCtoSwap.toString()) + .div(1.0009) + .toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [false, false] + ); + + await pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address, usdc.address], + [wethFlashloanAmount.toString(), usdcFlashloanAmount.toString()], + [0, 0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const userAUsdcBalance = await aUsdc.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmountForEth.add(expectedDaiAmountForUsdc)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAUsdcBalance).to.be.lt(userAUsdcBalanceBefore); + expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); + }); + + it('should correctly swap and deposit multiple tokens using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + usdc, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmountForUsdc = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Make a deposit for user + await usdc.connect(user).mint(amountUSDCtoSwap); + await usdc.connect(user).approve(pool.address, amountUSDCtoSwap); + await pool.connect(user).deposit(usdc.address, amountUSDCtoSwap, userAddress, 0); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmountForEth); + await mockUniswapRouter.setAmountToReturn(usdc.address, expectedDaiAmountForUsdc); + + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); + + const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) + .div(1.0009) + .toFixed(0); + + const usdcFlashloanAmount = new BigNumber(amountUSDCtoSwap.toString()) + .div(1.0009) + .toFixed(0); + + const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); + const aWethMsgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aWethNonce, + deadline, + amountWETHtoSwap.toString() + ); + const { v: aWETHv, r: aWETHr, s: aWETHs } = getSignatureFromTypedData( + ownerPrivateKey, + aWethMsgParams + ); + + const aUsdcNonce = (await aUsdc._nonces(userAddress)).toNumber(); + const aUsdcMsgParams = buildPermitParams( + chainId, + aUsdc.address, + '1', + await aUsdc.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aUsdcNonce, + deadline, + amountUSDCtoSwap.toString() + ); + const { v: aUsdcv, r: aUsdcr, s: aUsdcs } = getSignatureFromTypedData( + ownerPrivateKey, + aUsdcMsgParams + ); + const params = buildLiquiditySwapParams( + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], + [amountWETHtoSwap, amountUSDCtoSwap], + [deadline, deadline], + [aWETHv, aUsdcv], + [aWETHr, aUsdcr], + [aWETHs, aUsdcs], + [false, false] + ); + + await pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address, usdc.address], + [wethFlashloanAmount.toString(), usdcFlashloanAmount.toString()], + [0, 0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const userAUsdcBalance = await aUsdc.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmountForEth.add(expectedDaiAmountForUsdc)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAUsdcBalance).to.be.lt(userAUsdcBalanceBefore); + expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); + }); + + it('should correctly swap tokens with permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [liquidityToSwap], + [deadline], + [v], + [r], + [s], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should revert if inconsistent params', async () => { + const { users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params2 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0, 0], + [0, 0], + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params2, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params3 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0, 0], + [0], + [0, 0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params3, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params4 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params4, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params5 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params5, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params6 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount, expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params6, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params7 = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0, 0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params7, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params8 = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0, 0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params8, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params9 = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false, false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params9, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + + it('should revert if caller not lending pool', async () => { + const { users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .executeOperation( + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params + ) + ).to.be.revertedWith('CALLER_MUST_BE_LENDING_POOL'); + }); + + it('should work correctly with tokens of different decimals', async () => { + const { + users, + usdc, + oracle, + dai, + aDai, + uniswapLiquiditySwapAdapter, + pool, + deployer, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const liquidity = await convertToCurrencyDecimals(usdc.address, '20000'); + + // Provide liquidity + await usdc.mint(liquidity); + await usdc.approve(pool.address, liquidity); + await pool.deposit(usdc.address, liquidity, deployer.address, 0); + + // Make a deposit for user + await usdc.connect(user).mint(amountUSDCtoSwap); + await usdc.connect(user).approve(pool.address, amountUSDCtoSwap); + await pool.connect(user).deposit(usdc.address, amountUSDCtoSwap, userAddress, 0); + + const usdcPrice = await oracle.getAssetPrice(usdc.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + + // usdc 6 + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + await mockUniswapRouter.connect(user).setAmountToReturn(usdc.address, expectedDaiAmount); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + const aUsdcBalance = await aUsdc.balanceOf(userAddress); + await aUsdc.connect(user).approve(uniswapLiquiditySwapAdapter.address, aUsdcBalance); + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(amountUSDCtoSwap.toString()).div(1.0009).toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [usdc.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(usdc.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterUsdcBalance = await usdc.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const aDaiBalance = await aDai.balanceOf(userAddress); + + expect(adapterUsdcBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(aDaiBalance).to.be.eq(expectedDaiAmount); + }); + + it('should revert when min amount to receive exceeds the max slippage amount', async () => { + const { users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + const smallExpectedDaiAmount = expectedDaiAmount.div(2); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const params = buildLiquiditySwapParams( + [dai.address], + [smallExpectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('minAmountOut exceed max slippage'); + }); + + it('should correctly swap tokens all the balance', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [false] + ); + + // Flashloan + premium > aToken balance. Then it will only swap the balance - premium + const flashloanFee = liquidityToSwap.mul(9).div(10000); + const swappedAmount = liquidityToSwap.sub(flashloanFee); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [liquidityToSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, swappedAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapLiquiditySwapAdapter.address); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + expect(adapterAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap tokens all the balance using permit', async () => { + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const liquidityToSwap = parseEther('10'); + expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [1], + [liquidityToSwap], + [deadline], + [v], + [r], + [s], + [false] + ); + + // Flashloan + premium > aToken balance. Then it will only swap the balance - premium + const flashloanFee = liquidityToSwap.mul(9).div(10000); + const swappedAmount = liquidityToSwap.sub(flashloanFee); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [liquidityToSwap.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, swappedAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapLiquiditySwapAdapter.address); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + expect(adapterAEthBalance).to.be.eq(Zero); + }); + }); + + describe('swapAndDeposit', () => { + beforeEach(async () => { + const { users, weth, dai, pool, deployer } = testEnv; + const userAddress = users[0].address; + + // Provide liquidity + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + }); + + it('should correctly swap tokens and deposit the out tokens in the pool', async () => { + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ + { + amount: liquidityToSwap, + deadline, + v, + r, + s, + }, + ], + [false] + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should revert if inconsistent params', async () => { + const { users, weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; + const user = users[0].signer; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address, dai.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address, weth.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap, amountWETHtoSwap], + [expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [], + [false] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount, expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + + it('should revert when min amount to receive exceeds the max slippage amount', async () => { + const { users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + const smallExpectedDaiAmount = expectedDaiAmount.div(2); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [smallExpectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ).to.be.revertedWith('minAmountOut exceed max slippage'); + }); + + it('should correctly swap tokens and deposit multiple tokens', async () => { + const { + users, + weth, + usdc, + oracle, + dai, + aDai, + aWETH, + uniswapLiquiditySwapAdapter, + pool, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmountForUsdc = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Make a deposit for user + await usdc.connect(user).mint(amountUSDCtoSwap); + await usdc.connect(user).approve(pool.address, amountUSDCtoSwap); + await pool.connect(user).deposit(usdc.address, amountUSDCtoSwap, userAddress, 0); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmountForEth); + await mockUniswapRouter.setAmountToReturn(usdc.address, expectedDaiAmountForUsdc); + + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + await aUsdc.connect(user).approve(uniswapLiquiditySwapAdapter.address, amountUSDCtoSwap); + const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); + + await uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address, usdc.address], + [dai.address, dai.address], + [amountWETHtoSwap, amountUSDCtoSwap], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false, false] + ); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const userAUsdcBalance = await aUsdc.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmountForEth.add(expectedDaiAmountForUsdc)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAUsdcBalance).to.be.lt(userAUsdcBalanceBefore); + expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); + }); + + it('should correctly swap tokens and deposit multiple tokens using permit', async () => { + const { + users, + weth, + usdc, + oracle, + dai, + aDai, + aWETH, + uniswapLiquiditySwapAdapter, + pool, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const amountUSDCtoSwap = await convertToCurrencyDecimals(usdc.address, '10'); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await usdc.decimals()).toString(); + const principalDecimals = (await dai.decimals()).toString(); + + const expectedDaiAmountForUsdc = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountUSDCtoSwap.toString()) + .times( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(daiPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Make a deposit for user + await usdc.connect(user).mint(amountUSDCtoSwap); + await usdc.connect(user).approve(pool.address, amountUSDCtoSwap); + await pool.connect(user).deposit(usdc.address, amountUSDCtoSwap, userAddress, 0); + + const aUsdcData = await pool.getReserveData(usdc.address); + const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmountForEth); + await mockUniswapRouter.setAmountToReturn(usdc.address, expectedDaiAmountForUsdc); + + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); + + const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); + const aWethMsgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aWethNonce, + deadline, + amountWETHtoSwap.toString() + ); + const { v: aWETHv, r: aWETHr, s: aWETHs } = getSignatureFromTypedData( + ownerPrivateKey, + aWethMsgParams + ); + + const aUsdcNonce = (await aUsdc._nonces(userAddress)).toNumber(); + const aUsdcMsgParams = buildPermitParams( + chainId, + aUsdc.address, + '1', + await aUsdc.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aUsdcNonce, + deadline, + amountUSDCtoSwap.toString() + ); + const { v: aUsdcv, r: aUsdcr, s: aUsdcs } = getSignatureFromTypedData( + ownerPrivateKey, + aUsdcMsgParams + ); + + await uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address, usdc.address], + [dai.address, dai.address], + [amountWETHtoSwap, amountUSDCtoSwap], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [ + { + amount: amountWETHtoSwap, + deadline, + v: aWETHv, + r: aWETHr, + s: aWETHs, + }, + { + amount: amountUSDCtoSwap, + deadline, + v: aUsdcv, + r: aUsdcr, + s: aUsdcs, + }, + ], + [false, false] + ); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const userAUsdcBalance = await aUsdc.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmountForEth.add(expectedDaiAmountForUsdc)); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(amountWETHtoSwap)); + expect(userAUsdcBalance).to.be.lt(userAUsdcBalanceBefore); + expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); + }); + + it('should correctly swap all the balance when using a bigger amount', async () => { + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); + + // User will swap liquidity 10 aEth to aDai + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + + // Only has 10 atokens, so all the balance will be swapped + const bigAmountToSwap = parseEther('100'); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [bigAmountToSwap], + [expectedDaiAmount], + [ + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ], + [false] + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapLiquiditySwapAdapter.address); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + expect(adapterAEthBalance).to.be.eq(Zero); + }); + + it('should correctly swap all the balance when using permit', async () => { + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); + + // Remove other balance + await aWETH.connect(user).transfer(users[1].address, parseEther('90')); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); + + // Only has 10 atokens, so all the balance will be swapped + const bigAmountToSwap = parseEther('100'); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); + const aWethMsgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aWethNonce, + deadline, + bigAmountToSwap.toString() + ); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, aWethMsgParams); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [bigAmountToSwap], + [expectedDaiAmount], + [ + { + amount: bigAmountToSwap, + deadline, + v, + r, + s, + }, + ], + [false] + ) + ) + .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') + .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapLiquiditySwapAdapter.address); + const adapterDaiAllowance = await dai.allowance( + uniswapLiquiditySwapAdapter.address, + userAddress + ); + const userADaiBalance = await aDai.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapLiquiditySwapAdapter.address); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(adapterDaiAllowance).to.be.eq(Zero); + expect(userADaiBalance).to.be.eq(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + expect(adapterAEthBalance).to.be.eq(Zero); + }); + }); + }); +}); diff --git a/test/uniswapAdapters.repay.spec.ts b/test/uniswapAdapters.repay.spec.ts new file mode 100644 index 00000000..fbae1d00 --- /dev/null +++ b/test/uniswapAdapters.repay.spec.ts @@ -0,0 +1,1441 @@ +import { makeSuite, TestEnv } from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, + buildLiquiditySwapParams, + buildRepayAdapterParams, +} from '../helpers/contracts-helpers'; +import { getMockUniswapRouter } from '../helpers/contracts-getters'; +import { + deployUniswapLiquiditySwapAdapter, + deployUniswapRepayAdapter, +} from '../helpers/contracts-deployments'; +import { MockUniswapV2Router02 } from '../types/MockUniswapV2Router02'; +import { Zero } from '@ethersproject/constants'; +import BigNumber from 'bignumber.js'; +import { DRE, evmRevert, evmSnapshot } from '../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { eContractid } from '../helpers/types'; +import { StableDebtToken } from '../types/StableDebtToken'; +import { BUIDLEREVM_CHAINID } from '../helpers/buidler-constants'; +import { MAX_UINT_AMOUNT } from '../helpers/constants'; +const { parseEther } = ethers.utils; + +const { expect } = require('chai'); + +makeSuite('Uniswap adapters', (testEnv: TestEnv) => { + let mockUniswapRouter: MockUniswapV2Router02; + let evmSnapshotId: string; + + before(async () => { + mockUniswapRouter = await getMockUniswapRouter(); + }); + + beforeEach(async () => { + evmSnapshotId = await evmSnapshot(); + }); + + afterEach(async () => { + await evmRevert(evmSnapshotId); + }); + + describe('UniswapRepayAdapter', () => { + beforeEach(async () => { + const { users, weth, dai, usdc, aave, pool, deployer } = testEnv; + const userAddress = users[0].address; + + // Provide liquidity + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + + const usdcLiquidity = await convertToCurrencyDecimals(usdc.address, '2000000'); + await usdc.mint(usdcLiquidity); + await usdc.approve(pool.address, usdcLiquidity); + await pool.deposit(usdc.address, usdcLiquidity, deployer.address, 0); + + await weth.mint(parseEther('100')); + await weth.approve(pool.address, parseEther('100')); + await pool.deposit(weth.address, parseEther('100'), deployer.address, 0); + + await aave.mint(parseEther('1000000')); + await aave.approve(pool.address, parseEther('1000000')); + await pool.deposit(aave.address, parseEther('1000000'), deployer.address, 0); + + // Make a deposit for user + await weth.mint(parseEther('1000')); + await weth.approve(pool.address, parseEther('1000')); + await pool.deposit(weth.address, parseEther('1000'), userAddress, 0); + + await aave.mint(parseEther('1000000')); + await aave.approve(pool.address, parseEther('1000000')); + await pool.deposit(aave.address, parseEther('1000000'), userAddress, 0); + + await usdc.mint(usdcLiquidity); + await usdc.approve(pool.address, usdcLiquidity); + await pool.deposit(usdc.address, usdcLiquidity, userAddress, 0); + }); + + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const { addressesProvider, weth } = testEnv; + await deployUniswapRepayAdapter([ + addressesProvider.address, + mockUniswapRouter.address, + weth.address, + ]); + }); + + it('should revert if not valid addresses provider', async () => { + const { weth } = testEnv; + expect( + deployUniswapRepayAdapter([ + mockUniswapRouter.address, + mockUniswapRouter.address, + weth.address, + ]) + ).to.be.reverted; + }); + }); + + describe('executeOperation', () => { + it('should correctly swap tokens and repay debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + liquidityToSwap + ); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, liquidityToSwap.toString(), flashLoanDebt); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay debt with permit', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapRepayAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + liquidityToSwap + ); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + liquidityToSwap, + deadline, + v, + r, + s, + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, liquidityToSwap.toString(), flashLoanDebt); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should revert if caller not lending pool', async () => { + const { users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + uniswapRepayAdapter + .connect(user) + .executeOperation( + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params + ) + ).to.be.revertedWith('CALLER_MUST_BE_LENDING_POOL'); + }); + + it('should revert if there is not debt to repay with the specified rate mode', async () => { + const { users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + await weth.connect(user).mint(amountWETHtoSwap); + await weth.connect(user).transfer(uniswapRepayAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert if there is not debt to repay', async () => { + const { users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + await weth.connect(user).mint(amountWETHtoSwap); + await weth.connect(user).transfer(uniswapRepayAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert when max amount allowed to swap is bigger than max slippage', async () => { + const { users, pool, weth, oracle, dai, aWETH, uniswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); + await aWETH.connect(user).approve(uniswapRepayAdapter.address, bigMaxAmountToSwap); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, bigMaxAmountToSwap); + + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + bigMaxAmountToSwap + ); + + const params = buildRepayAdapterParams( + weth.address, + bigMaxAmountToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); + }); + + it('should swap, repay debt and pull the needed ATokens leaving no leftovers', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userWethBalanceBefore = await weth.balanceOf(userAddress); + + const actualWEthSwapped = new BigNumber(liquidityToSwap.toString()) + .multipliedBy(0.995) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); + + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + actualWEthSwapped + ); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [expectedDaiAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, actualWEthSwapped.toString(), flashLoanDebt); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + const userWethBalance = await weth.balanceOf(userAddress); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(actualWEthSwapped)); + expect(userWethBalance).to.be.eq(userWethBalanceBefore); + }); + + it('should correctly swap tokens and repay the whole stable debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [amountToRepay.toString()], + [0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay the whole variable debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + const daiStableVariableTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).variableDebtTokenAddress; + + const daiVariableDebtContract = await getContract( + eContractid.VariableDebtToken, + daiStableVariableTokenAddress + ); + + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); + + const params = buildRepayAdapterParams( + weth.address, + liquidityToSwap, + 2, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [amountToRepay.toString()], + [0], + userAddress, + params, + 0 + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiVariableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiVariableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly repay debt using the same asset as collateral', async () => { + const { users, pool, aDai, dai, uniswapRepayAdapter, helpersContract } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + // Add deposit for user + await dai.mint(parseEther('20')); + await dai.approve(pool.address, parseEther('20')); + await pool.deposit(dai.address, parseEther('20'), userAddress, 0); + + const amountCollateralToSwap = parseEther('10'); + const debtAmount = parseEther('10'); + + // Open user Debt + await pool.connect(user).borrow(dai.address, debtAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const flashLoanDebt = new BigNumber(amountCollateralToSwap.toString()) + .multipliedBy(1.0009) + .toFixed(0); + + await aDai.connect(user).approve(uniswapRepayAdapter.address, flashLoanDebt); + const userADaiBalanceBefore = await aDai.balanceOf(userAddress); + const userDaiBalanceBefore = await dai.balanceOf(userAddress); + + const params = buildRepayAdapterParams( + dai.address, + amountCollateralToSwap, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + false + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [dai.address], + [amountCollateralToSwap.toString()], + [0], + userAddress, + params, + 0 + ); + + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userADaiBalance = await aDai.balanceOf(userAddress); + const adapterADaiBalance = await aDai.balanceOf(uniswapRepayAdapter.address); + const userDaiBalance = await dai.balanceOf(userAddress); + + expect(adapterADaiBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(debtAmount); + expect(userDaiStableDebtAmount).to.be.lt(debtAmount); + expect(userADaiBalance).to.be.lt(userADaiBalanceBefore); + expect(userADaiBalance).to.be.gte(userADaiBalanceBefore.sub(flashLoanDebt)); + expect(userDaiBalance).to.be.eq(userDaiBalanceBefore); + }); + }); + + describe('swapAndRepay', () => { + it('should correctly swap tokens and repay debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await mockUniswapRouter.setAmountToSwap(weth.address, liquidityToSwap); + + await mockUniswapRouter.setDefaultMockValue(liquidityToSwap); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay debt with permit', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + await mockUniswapRouter.setAmountToSwap(weth.address, liquidityToSwap); + + await mockUniswapRouter.setDefaultMockValue(liquidityToSwap); + + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aWETH._nonces(userAddress)).toNumber(); + const msgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapRepayAdapter.address, + nonce, + deadline, + liquidityToSwap.toString() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + { + amount: liquidityToSwap, + deadline, + v, + r, + s, + }, + false + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should revert if there is not debt to repay', async () => { + const { users, weth, aWETH, oracle, dai, uniswapRepayAdapter } = testEnv; + const user = users[0].signer; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + + await mockUniswapRouter.setAmountToSwap(weth.address, liquidityToSwap); + + await mockUniswapRouter.setDefaultMockValue(liquidityToSwap); + + await expect( + uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ) + ).to.be.reverted; + }); + + it('should revert when max amount allowed to swap is bigger than max slippage', async () => { + const { users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); + await aWETH.connect(user).approve(uniswapRepayAdapter.address, bigMaxAmountToSwap); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, bigMaxAmountToSwap); + + await mockUniswapRouter.setDefaultMockValue(bigMaxAmountToSwap); + + await expect( + uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + bigMaxAmountToSwap, + expectedDaiAmount, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ) + ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); + }); + + it('should swap, repay debt and pull the needed ATokens leaving no leftovers', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userWethBalanceBefore = await weth.balanceOf(userAddress); + + const actualWEthSwapped = new BigNumber(liquidityToSwap.toString()) + .multipliedBy(0.995) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); + + await mockUniswapRouter.setDefaultMockValue(actualWEthSwapped); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + expectedDaiAmount, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + const userWethBalance = await weth.balanceOf(userAddress); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(actualWEthSwapped)); + expect(userWethBalance).to.be.eq(userWethBalanceBefore); + }); + + it('should correctly swap tokens and repay the whole stable debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + amountToRepay, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiStableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly swap tokens and repay the whole variable debt', async () => { + const { + users, + pool, + weth, + aWETH, + oracle, + dai, + uniswapRepayAdapter, + helpersContract, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); + + const daiStableVariableTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).variableDebtTokenAddress; + + const daiVariableDebtContract = await getContract( + eContractid.VariableDebtToken, + daiStableVariableTokenAddress + ); + + const userDaiVariableDebtAmountBefore = await daiVariableDebtContract.balanceOf( + userAddress + ); + + // Add a % to repay on top of the debt + const liquidityToSwap = new BigNumber(amountWETHtoSwap.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // Add a % to repay on top of the debt + const amountToRepay = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.1) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + weth.address, + dai.address, + liquidityToSwap, + amountToRepay, + 2, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiVariableDebtAmount = await daiVariableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); + const adapterAEthBalance = await aWETH.balanceOf(uniswapRepayAdapter.address); + + expect(adapterAEthBalance).to.be.eq(Zero); + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiVariableDebtAmountBefore).to.be.gte(expectedDaiAmount); + expect(userDaiVariableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); + expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); + }); + + it('should correctly repay debt using the same asset as collateral', async () => { + const { users, pool, dai, uniswapRepayAdapter, helpersContract, aDai } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + // Add deposit for user + await dai.mint(parseEther('20')); + await dai.approve(pool.address, parseEther('20')); + await pool.deposit(dai.address, parseEther('20'), userAddress, 0); + + const amountCollateralToSwap = parseEther('4'); + + const debtAmount = parseEther('3'); + + // Open user Debt + await pool.connect(user).borrow(dai.address, debtAmount, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + + await aDai.connect(user).approve(uniswapRepayAdapter.address, amountCollateralToSwap); + const userADaiBalanceBefore = await aDai.balanceOf(userAddress); + const userDaiBalanceBefore = await dai.balanceOf(userAddress); + + await uniswapRepayAdapter.connect(user).swapAndRepay( + dai.address, + dai.address, + amountCollateralToSwap, + amountCollateralToSwap, + 1, + { + amount: 0, + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + false + ); + + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userADaiBalance = await aDai.balanceOf(userAddress); + const adapterADaiBalance = await aDai.balanceOf(uniswapRepayAdapter.address); + const userDaiBalance = await dai.balanceOf(userAddress); + + expect(adapterADaiBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(debtAmount); + expect(userDaiStableDebtAmount).to.be.lt(debtAmount); + expect(userADaiBalance).to.be.lt(userADaiBalanceBefore); + expect(userADaiBalance).to.be.gte(userADaiBalanceBefore.sub(amountCollateralToSwap)); + expect(userDaiBalance).to.be.eq(userDaiBalanceBefore); + }); + }); + }); +});