From 7c51597282d7ff6f5a4522701f76e12f7e52210f Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 27 Oct 2020 09:17:33 -0300 Subject: [PATCH 01/34] Add uniswap adapters for liquidity swap and repay with collateral --- contracts/adapters/BaseUniswapAdapter.sol | 215 ++++++++++++++++++ .../adapters/UniswapLiquiditySwapAdapter.sol | 63 +++++ contracts/adapters/UniswapRepayAdapter.sol | 71 ++++++ 3 files changed, 349 insertions(+) create mode 100644 contracts/adapters/BaseUniswapAdapter.sol create mode 100644 contracts/adapters/UniswapLiquiditySwapAdapter.sol create mode 100644 contracts/adapters/UniswapRepayAdapter.sol diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol new file mode 100644 index 00000000..79b55df2 --- /dev/null +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; +pragma experimental ABIEncoderV2; + +import {PercentageMath} from '../libraries/math/PercentageMath.sol'; +import {SafeMath} from '../dependencies/openzeppelin/contracts/SafeMath.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {SafeERC20} from '../dependencies/openzeppelin/contracts/SafeERC20.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {ILendingPool} from '../interfaces/ILendingPool.sol'; +import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; +import {ReserveConfiguration} from '../libraries/configuration/ReserveConfiguration.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; + + +/** + * @title BaseUniswapAdapter + * @notice Implements the logic for performing assets swaps in Uniswap V2 + * @author Aave + **/ +contract BaseUniswapAdapter { + using SafeMath for uint256; + using PercentageMath for uint256; + using SafeERC20 for IERC20; + using ReserveConfiguration for ReserveConfiguration.Map; + + // Max slippage percent allow by param + uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% + // Min slippage percent allow by param + uint256 public constant MIN_SLIPPAGE_PERCENT = 10; // 0,1% + + ILendingPoolAddressesProvider public immutable addressesProvider; + IUniswapV2Router02 public immutable uniswapRouter; + ILendingPool public immutable pool; + + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + + constructor(ILendingPoolAddressesProvider _addressesProvider, IUniswapV2Router02 _uniswapRouter) public { + addressesProvider = _addressesProvider; + pool = ILendingPool(_addressesProvider.getLendingPool()); + uniswapRouter = _uniswapRouter; + } + + /** + * @dev Swaps an `amountToSwap` of an asset to another + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap Exact amount of `assetToSwapFrom` to be swapped + * @param slippage the max slippage percentage allowed for the swap + * @return the amount received from the swap + */ + function swapExactTokensForTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + uint256 slippage + ) + internal + returns (uint256) + { + uint256 fromAssetDecimals = getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = getDecimals(assetToSwapTo); + + (uint256 fromAssetPrice, uint256 toAssetPrice) = getPrices(assetToSwapFrom, assetToSwapTo); + + uint256 amountOutMin = amountToSwap + .mul(fromAssetPrice.mul(10**toAssetDecimals)) + .div(toAssetPrice.mul(10**fromAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(slippage)); + + IERC20(assetToSwapFrom).approve(address(uniswapRouter), amountToSwap); + + address[] memory path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + uint256[] memory amounts = uniswapRouter.swapExactTokensForTokens(amountToSwap, amountOutMin, path, address(this), block.timestamp); + + require(amounts[1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); + + return amounts[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 received from the swap + */ + function swapTokensForExactTokens( + address assetToSwapFrom, + address assetToSwapTo, + uint256 maxAmountToSwap, + uint256 amountToReceive + ) + internal + returns (uint256) + { + uint256 fromAssetDecimals = getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = getDecimals(assetToSwapTo); + + (uint256 fromAssetPrice, uint256 toAssetPrice) = getPrices(assetToSwapFrom, assetToSwapTo); + + uint256 expectedMaxAmountToSwap = amountToReceive + .mul(toAssetPrice.mul(10**fromAssetDecimals)) + .div(fromAssetPrice.mul(10**toAssetDecimals)) + .percentMul(PercentageMath.PERCENTAGE_FACTOR.add(MAX_SLIPPAGE_PERCENT)); + + require(maxAmountToSwap < expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage'); + + IERC20(assetToSwapFrom).approve(address(uniswapRouter), maxAmountToSwap); + + address[] memory path = new address[](2); + path[0] = assetToSwapFrom; + path[1] = assetToSwapTo; + uint256[] memory amounts = uniswapRouter.swapTokensForExactTokens(amountToReceive, maxAmountToSwap, path, address(this), block.timestamp); + + require(amounts[1] >= amountToReceive, 'INSUFFICIENT_OUTPUT_AMOUNT'); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); + + return amounts[1]; + } + + /** + * @dev Get assets prices from the oracle denominated in eth + * @param assetToSwapFrom first asset + * @param assetToSwapTo second asset + * @return fromAssetPrice eth price for the first asset + * @return toAssetPrice eth price for the second asset + */ + function getPrices( + address assetToSwapFrom, + address assetToSwapTo + ) + internal + view + returns (uint256 fromAssetPrice, uint256 toAssetPrice) + { + IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); + fromAssetPrice = oracle.getAssetPrice(assetToSwapFrom); + toAssetPrice = oracle.getAssetPrice(assetToSwapTo); + } + + /** + * @dev Get the decimals of an asset + * @return number of decimals of the asset + */ + function getDecimals(address asset) internal view returns (uint256) { + ReserveConfiguration.Map memory configuration = pool.getConfiguration(asset); + (, , , uint256 decimals, ) = configuration.getParamsMemory(); + + return decimals; + } + + /** + * @dev Get the aToken associated to the asset + * @return address of the aToken + */ + function getAToken(address asset) internal view returns (address) { + ReserveLogic.ReserveData memory reserve = pool.getReserveData(asset); + return reserve.aTokenAddress; + } + + /** + * @dev Take action with the swap left overs as configured in the parameters + * @param asset address of the asset + * @param reservedAmount Amount reserved to be used by the contract to repay the flash loan + * @param leftOverAction Flag indicating what to do with the left over balance from the swap: + * (0) Deposit back + * (1) Direct transfer to user + * @param user address + */ + function sendLeftOver(address asset, uint256 reservedAmount, uint256 leftOverAction, address user) internal { + uint256 balance = IERC20(asset).balanceOf(address(this)); + uint256 assetLeftOver = balance.sub(reservedAmount); + + if (assetLeftOver > 0) { + if (leftOverAction == 0) { + IERC20(asset).approve(address(pool), balance); + pool.deposit(asset, assetLeftOver, user, 0); + } else { + IERC20(asset).transfer(user, assetLeftOver); + } + } + } + + /** + * @dev Take action with the swap left overs as configured in the parameters + * @param reserve address of the asset + * @param user address + * @param flashLoanDebt need to be repaid + */ + function pullATokenAndRepayFlashLoan( + address reserve, + address user, + uint256 flashLoanDebt + ) internal { + address reserveAToken = getAToken(reserve); + + // transfer from user to adapter + IERC20(reserveAToken).safeTransferFrom(user, address(this), flashLoanDebt); + + // withdraw reserve + pool.withdraw(reserve, flashLoanDebt); + + // Repay flashloan + IERC20(reserve).approve(address(pool), flashLoanDebt); + } +} diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol new file mode 100644 index 00000000..62125264 --- /dev/null +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; +pragma experimental ABIEncoderV2; + +import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +/** + * @title UniswapLiquiditySwapAdapter + * @notice Uniswap V2 Adapter to swap liquidity using a flash loan. + * @author Aave + **/ +contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + + constructor( + ILendingPoolAddressesProvider _addressesProvider, + IUniswapV2Router02 _uniswapRouter + ) + public + BaseUniswapAdapter(_addressesProvider, _uniswapRouter) + {} + + /** + * @dev Swaps the received reserve amount from the flashloan 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 reserve Address to be swapped + * @param amount Amount of the reserve to be swapped + * @param fee Fee of the flash loan + * @param params Additional variadic field to include extra params. Expected parameters: + * address assetToSwapTo Address of the reserve to be swapped to and deposited + * address user The address of the user + * uint256 slippage The max slippage percentage allowed for the swap + */ + function executeOperation( + address reserve, + uint256 amount, + uint256 fee, + bytes calldata params + ) external override returns (bool) { + ( + address assetToSwapTo, + address user, + uint256 slippage + ) = abi.decode(params, (address, address, uint256)); + require(slippage < MAX_SLIPPAGE_PERCENT && slippage >= MIN_SLIPPAGE_PERCENT, 'SLIPPAGE_OUT_OF_RANGE'); + + uint256 receivedAmount = swapExactTokensForTokens(reserve, assetToSwapTo, amount, slippage); + + // Deposit new reserve + IERC20(assetToSwapTo).approve(address(pool), receivedAmount); + pool.deposit(assetToSwapTo, receivedAmount, user, 0); + + uint256 flashLoanDebt = amount.add(fee); + pullATokenAndRepayFlashLoan(reserve, user, flashLoanDebt); + + return true; + } +} diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol new file mode 100644 index 00000000..2bb8a8a8 --- /dev/null +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; +pragma experimental ABIEncoderV2; + +import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; +import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; +import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; +import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; +import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; + +/** + * @title UniswapRepayAdapter + * @notice Uniswap V2 Adapter to perform a repay of a debt using a flash loan. + * @author Aave + **/ +contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + + constructor( + ILendingPoolAddressesProvider _addressesProvider, + IUniswapV2Router02 _uniswapRouter + ) + public + BaseUniswapAdapter(_addressesProvider, _uniswapRouter) + {} + + /** + * @dev Swaps the received reserve amount into the asset specified in the params. The received funds from the swap are + * then used to repay a debt on 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 reserve Address to be swapped + * @param amount Amount of the reserve to be swapped + * @param fee Fee of the flash loan + * @param params Additional variadic field to include extra params. Expected parameters: + * address assetToSwapTo Address of the reserve to be swapped to and deposited + * address user The address of the user + * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: + * (0) Deposit back + * (1) Direct transfer to user + * uint256 repayAmount Amount of debt to be repaid + * uint256 rateMode The rate modes of the debt to be repaid + */ + function executeOperation( + address reserve, + uint256 amount, + uint256 fee, + bytes calldata params + ) external override returns (bool) { + ( + address assetToSwapTo, + address user, + uint256 leftOverAction, + uint256 repayAmount, + uint256 rateMode + ) = abi.decode(params, (address, address, uint256, uint256, uint256)); + + swapTokensForExactTokens(reserve, assetToSwapTo, amount, repayAmount); + + // Repay debt + IERC20(assetToSwapTo).approve(address(pool), repayAmount); + pool.repay(assetToSwapTo, repayAmount, rateMode, user); + + uint256 flashLoanDebt = amount.add(fee); + pullATokenAndRepayFlashLoan(reserve, user, flashLoanDebt); + + // Take care of reserve leftover from the swap + sendLeftOver(reserve, flashLoanDebt, leftOverAction, user); + + return true; + } +} From a05b75b467ae08f6417942c469bfa87ab4e45f81 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 27 Oct 2020 09:18:30 -0300 Subject: [PATCH 02/34] Add uniswap adapters unit tests --- contracts/interfaces/IUniswapV2Router02.sol | 20 + .../mocks/swap/MockUniswapV2Router02.sol | 53 ++ helpers/contracts-helpers.ts | 53 +- helpers/types.ts | 4 +- test/__setup.spec.ts | 23 +- test/helpers/make-suite.ts | 15 +- test/pausable-functions.spec.ts | 2 +- test/uniswapAdapters.spec.ts | 805 ++++++++++++++++++ 8 files changed, 958 insertions(+), 17 deletions(-) create mode 100644 contracts/interfaces/IUniswapV2Router02.sol create mode 100644 contracts/mocks/swap/MockUniswapV2Router02.sol create mode 100644 test/uniswapAdapters.spec.ts diff --git a/contracts/interfaces/IUniswapV2Router02.sol b/contracts/interfaces/IUniswapV2Router02.sol new file mode 100644 index 00000000..cb04c269 --- /dev/null +++ b/contracts/interfaces/IUniswapV2Router02.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +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); +} diff --git a/contracts/mocks/swap/MockUniswapV2Router02.sol b/contracts/mocks/swap/MockUniswapV2Router02.sol new file mode 100644 index 00000000..64cb935f --- /dev/null +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +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 { + uint256 internal _amountToReturn; + uint256 internal _amountToSwap; + + function setAmountToReturn(uint256 amount) public { + _amountToReturn = amount; + } + + function setAmountToSwap(uint256 amount) public { + _amountToSwap = 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); + IERC20(path[1]).transfer(to, _amountToReturn); + + amounts = new uint[](path.length); + amounts[0] = amountIn; + amounts[1] = _amountToReturn; + } + + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint /* deadline */ + ) external override returns (uint256[] memory amounts) { + IERC20(path[0]).transferFrom(msg.sender, address(this), _amountToSwap); + + MintableERC20(path[1]).mint(_amountToReturn); + IERC20(path[1]).transfer(to, _amountToReturn); + + amounts = new uint[](path.length); + amounts[0] = _amountToSwap; + amounts[1] = _amountToReturn; + } +} diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 77730b04..eaee15ab 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -37,6 +37,9 @@ import BigNumber from 'bignumber.js'; import {Ierc20Detailed} from '../types/Ierc20Detailed'; import {StableDebtToken} from '../types/StableDebtToken'; import {VariableDebtToken} from '../types/VariableDebtToken'; +import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; +import {UniswapLiquiditySwapAdapter} from '../types/UniswapLiquiditySwapAdapter'; +import {UniswapRepayAdapter} from '../types/UniswapRepayAdapter'; import {MockContract} from 'ethereum-waffle'; import {getReservesConfigByPool} from './configuration'; import {verifyContract} from './etherscan-verification'; @@ -47,7 +50,6 @@ const { export type MockTokenMap = {[symbol: string]: MintableERC20}; import {ZERO_ADDRESS} from './constants'; -import {MockSwapAdapter} from '../types/MockSwapAdapter'; import {signTypedData_v4, TypedData} from 'eth-sig-util'; import {fromRpcSig, ECDSASignature} from 'ethereumjs-util'; import {SignerWithAddress} from '../test/helpers/make-suite'; @@ -256,6 +258,27 @@ export const deployChainlinkProxyPriceProvider = async ( return instance; }; +export const deployMockUniswapRouter = async () => + await deployContract(eContractid.MockUniswapV2Router02, []); + +export const deployUniswapLiquiditySwapAdapter = async ( + addressesProvider: tEthereumAddress, + uniswapRouter: tEthereumAddress +) => + await deployContract(eContractid.UniswapLiquiditySwapAdapter, [ + addressesProvider, + uniswapRouter, + ]); + +export const deployUniswapRepayAdapter = async ( + addressesProvider: tEthereumAddress, + uniswapRouter: tEthereumAddress +) => + await deployContract(eContractid.UniswapRepayAdapter, [ + addressesProvider, + uniswapRouter, + ]); + export const getChainlingProxyPriceProvider = async (address?: tEthereumAddress) => await getContract( eContractid.ChainlinkProxyPriceProvider, @@ -321,8 +344,6 @@ export const deployWalletBalancerProvider = async ( } return instance; }; -export const deployMockSwapAdapter = async (addressesProvider: tEthereumAddress) => - await deployContract(eContractid.MockSwapAdapter, [addressesProvider]); export const deployAaveProtocolTestHelpers = async ( addressesProvider: tEthereumAddress, @@ -548,11 +569,29 @@ export const getMockFlashLoanReceiver = async (address?: tEthereumAddress) => { ); }; -export const getMockSwapAdapter = async (address?: tEthereumAddress) => { - return await getContract( - eContractid.MockSwapAdapter, +export const getMockUniswapRouter = async (address?: tEthereumAddress) => { + return await getContract( + eContractid.MockUniswapV2Router02, address || - (await getDb().get(`${eContractid.MockSwapAdapter}.${BRE.network.name}`).value()).address + (await getDb().get(`${eContractid.MockUniswapV2Router02}.${BRE.network.name}`).value()) + .address + ); +}; + +export const getUniswapLiquiditySwapAdapter = async (address?: tEthereumAddress) => { + return await getContract( + eContractid.UniswapLiquiditySwapAdapter, + address || + (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${BRE.network.name}`).value()) + .address + ); +}; + +export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => { + return await getContract( + eContractid.UniswapRepayAdapter, + address || + (await getDb().get(`${eContractid.UniswapRepayAdapter}.${BRE.network.name}`).value()).address ); }; diff --git a/helpers/types.ts b/helpers/types.ts index 26644896..877441ea 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -44,7 +44,6 @@ export enum eContractid { LendingPoolCollateralManager = 'LendingPoolCollateralManager', InitializableAdminUpgradeabilityProxy = 'InitializableAdminUpgradeabilityProxy', MockFlashLoanReceiver = 'MockFlashLoanReceiver', - MockSwapAdapter = 'MockSwapAdapter', WalletBalanceProvider = 'WalletBalanceProvider', AToken = 'AToken', MockAToken = 'MockAToken', @@ -56,6 +55,9 @@ export enum eContractid { VariableDebtToken = 'VariableDebtToken', FeeProvider = 'FeeProvider', TokenDistributor = 'TokenDistributor', + MockUniswapV2Router02 = 'MockUniswapV2Router02', + UniswapLiquiditySwapAdapter = 'UniswapLiquiditySwapAdapter', + UniswapRepayAdapter = 'UniswapRepayAdapter', } export enum ProtocolErrors { diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 676ae52a..40ed2ced 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -19,8 +19,10 @@ import { registerContractInJsonDb, getPairsTokenAggregator, initReserves, - deployMockSwapAdapter, deployLendingRateOracle, + deployMockUniswapRouter, + deployUniswapLiquiditySwapAdapter, + deployUniswapRepayAdapter, } from '../helpers/contracts-helpers'; import {Signer} from 'ethers'; import {TokenContractId, eContractid, tEthereumAddress, AavePools} from '../helpers/types'; @@ -239,8 +241,23 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); - const mockSwapAdapter = await deployMockSwapAdapter(addressesProvider.address); - await insertContractAddressInDb(eContractid.MockSwapAdapter, mockSwapAdapter.address); + const mockUniswapRouter = await deployMockUniswapRouter(); + await insertContractAddressInDb(eContractid.MockUniswapV2Router02, mockUniswapRouter.address); + + const UniswapLiquiditySwapAdapter = await deployUniswapLiquiditySwapAdapter( + addressesProvider.address, + mockUniswapRouter.address + ); + await insertContractAddressInDb( + eContractid.UniswapLiquiditySwapAdapter, + UniswapLiquiditySwapAdapter.address + ); + + const UniswapRepayAdapter = await deployUniswapRepayAdapter( + addressesProvider.address, + mockUniswapRouter.address + ); + await insertContractAddressInDb(eContractid.UniswapRepayAdapter, UniswapRepayAdapter.address); await deployWalletBalancerProvider(addressesProvider.address); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 5eb8788f..de9cfb13 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -9,8 +9,9 @@ import { getMintableErc20, getLendingPoolConfiguratorProxy, getPriceOracle, - getMockSwapAdapter, getLendingPoolAddressesProviderRegistry, + getUniswapLiquiditySwapAdapter, + getUniswapRepayAdapter, } from '../../helpers/contracts-helpers'; import {tEthereumAddress} from '../../helpers/types'; import {LendingPool} from '../../types/LendingPool'; @@ -25,8 +26,9 @@ import bignumberChai from 'chai-bignumber'; import {almostEqual} from './almost-equal'; import {PriceOracle} from '../../types/PriceOracle'; import {LendingPoolAddressesProvider} from '../../types/LendingPoolAddressesProvider'; -import {MockSwapAdapter} from '../../types/MockSwapAdapter'; import {LendingPoolAddressesProviderRegistry} from '../../types/LendingPoolAddressesProviderRegistry'; +import {UniswapLiquiditySwapAdapter} from '../../types/UniswapLiquiditySwapAdapter'; +import {UniswapRepayAdapter} from '../../types/UniswapRepayAdapter'; chai.use(bignumberChai()); chai.use(almostEqual()); @@ -48,7 +50,8 @@ export interface TestEnv { usdc: MintableERC20; lend: MintableERC20; addressesProvider: LendingPoolAddressesProvider; - mockSwapAdapter: MockSwapAdapter; + uniswapLiquiditySwapAdapter: UniswapLiquiditySwapAdapter; + uniswapRepayAdapter: UniswapRepayAdapter; registry: LendingPoolAddressesProviderRegistry; } @@ -73,7 +76,8 @@ const testEnv: TestEnv = { usdc: {} as MintableERC20, lend: {} as MintableERC20, addressesProvider: {} as LendingPoolAddressesProvider, - mockSwapAdapter: {} as MockSwapAdapter, + uniswapLiquiditySwapAdapter: {} as UniswapLiquiditySwapAdapter, + uniswapRepayAdapter: {} as UniswapRepayAdapter, registry: {} as LendingPoolAddressesProviderRegistry, } as TestEnv; @@ -135,7 +139,8 @@ export async function initializeMakeSuite() { testEnv.lend = await getMintableErc20(lendAddress); testEnv.weth = await getMintableErc20(wethAddress); - testEnv.mockSwapAdapter = await getMockSwapAdapter(); + testEnv.uniswapLiquiditySwapAdapter = await getUniswapLiquiditySwapAdapter(); + testEnv.uniswapRepayAdapter = await getUniswapRepayAdapter(); } export function makeSuite(name: string, tests: (testEnv: TestEnv) => void) { diff --git a/test/pausable-functions.spec.ts b/test/pausable-functions.spec.ts index 8e7cc2a7..fdfe5341 100644 --- a/test/pausable-functions.spec.ts +++ b/test/pausable-functions.spec.ts @@ -275,7 +275,7 @@ makeSuite('Pausable Pool', (testEnv: TestEnv) => { }); it('SwapBorrowRateMode', async () => { - const {pool, weth, dai, usdc, users, configurator, mockSwapAdapter} = testEnv; + const {pool, weth, dai, usdc, users, configurator} = testEnv; const user = users[1]; const amountWETHToDeposit = parseEther('10'); const amountDAIToDeposit = parseEther('120'); diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts new file mode 100644 index 00000000..6bf2cbc4 --- /dev/null +++ b/test/uniswapAdapters.spec.ts @@ -0,0 +1,805 @@ +import {makeSuite, TestEnv} from './helpers/make-suite'; +import { + convertToCurrencyDecimals, + deployUniswapLiquiditySwapAdapter, + deployUniswapRepayAdapter, + getContract, + getMockUniswapRouter, +} from '../helpers/contracts-helpers'; +import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; +import {Zero} from '@ethersproject/constants'; +import BigNumber from 'bignumber.js'; +import {evmRevert, evmSnapshot} from '../helpers/misc-utils'; +import {ethers} from 'ethers'; +import {eContractid} from '../helpers/types'; +import {AToken} from '../types/AToken'; +import {StableDebtToken} from '../types/StableDebtToken'; +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} = testEnv; + await deployUniswapLiquiditySwapAdapter( + addressesProvider.address, + mockUniswapRouter.address + ); + }); + + it('should revert if not valid addresses provider', async () => { + expect( + deployUniswapLiquiditySwapAdapter(mockUniswapRouter.address, mockUniswapRouter.address) + ).to.be.reverted; + }); + }); + + describe('executeOperation', () => { + 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, aEth, 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(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [dai.address, userAddress, 50] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + 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 aEth.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 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(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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [dai.address, userAddress, 50] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + usdc.address, + flashloanAmount.toString(), + 0, + 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 if slippage param is not inside limits', async () => { + const {users, pool, weth, oracle, dai, aEth, uniswapLiquiditySwapAdapter} = 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(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aEth.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); + + // 30% slippage + const params1 = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [dai.address, userAddress, 3000] + ); + + // 0,05% slippage + const params2 = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [dai.address, userAddress, 5] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params1, + 0 + ) + ).to.be.revertedWith('SLIPPAGE_OUT_OF_RANGE'); + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params2, + 0 + ) + ).to.be.revertedWith('SLIPPAGE_OUT_OF_RANGE'); + }); + + it('should revert when swap exceed slippage', async () => { + const {users, weth, oracle, dai, aEth, pool, uniswapLiquiditySwapAdapter} = 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(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmount = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + // 1,5% slippage + const returnedDaiAmountWithBigSlippage = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(0.985) + .toFixed(0); + await mockUniswapRouter.connect(user).setAmountToReturn(returnedDaiAmountWithBigSlippage); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aEth.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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256'], + [dai.address, userAddress, 50] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ).to.be.revertedWith('INSUFFICIENT_OUTPUT_AMOUNT'); + }); + }); + }); + + describe('UniswapRepayAdapter', () => { + describe('constructor', () => { + it('should deploy with correct parameters', async () => { + const {addressesProvider} = testEnv; + await deployUniswapRepayAdapter(addressesProvider.address, mockUniswapRouter.address); + }); + + it('should revert if not valid addresses provider', async () => { + expect(deployUniswapRepayAdapter(mockUniswapRouter.address, mockUniswapRouter.address)).to + .be.reverted; + }); + }); + + describe('executeOperation', () => { + 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 repay debt', async () => { + const { + users, + pool, + weth, + aEth, + 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aEth.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 with the specified rate mode', async () => { + const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aEth} = 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert if there is not debt to repay', async () => { + const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aEth} = 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ).to.be.reverted; + }); + + it('should revert when the received amount is less than expected', async () => { + const {users, pool, weth, oracle, dai, aEth, uniswapRepayAdapter, deployer} = 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 aEth.connect(user).approve(uniswapRepayAdapter.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 insufficientOutput = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(0.985) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(insufficientOutput); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ).to.be.revertedWith('INSUFFICIENT_OUTPUT_AMOUNT'); + }); + + it('should revert when max amount allowed to swap is bigger than max slippage', async () => { + const {users, pool, weth, oracle, dai, aEth, 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + + // Subtract the FL fee from the amount to be swapped 0,09% + const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); + const flashloanAmount = new BigNumber(bigMaxAmountToSwap.toString()).div(1.0009).toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); + }); + + it('should swap tokens, repay debt and deposit in pool the left over', async () => { + const { + users, + pool, + weth, + aEth, + 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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 actualWEthSwapped = new BigNumber(flashloanAmount.toString()) + .multipliedBy(0.995) + .toFixed(0); + + const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); + + await mockUniswapRouter.connect(user).setAmountToSwap(actualWEthSwapped); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 0, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, actualWEthSwapped.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aEth.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.gt(userAEthBalanceBefore.sub(liquidityToSwap)); + expect(userAEthBalance).to.be.gte( + userAEthBalanceBefore.sub(liquidityToSwap).add(leftOverWeth.toString()) + ); + }); + + it('should swap tokens, repay debt and transfer to user the left over', async () => { + const { + users, + pool, + weth, + aEth, + 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 aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.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 actualWEthSwapped = new BigNumber(flashloanAmount.toString()) + .multipliedBy(0.995) + .toFixed(0); + + const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); + + await mockUniswapRouter.connect(user).setAmountToSwap(actualWEthSwapped); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const wethBalanceBefore = await weth.balanceOf(userAddress); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256'], + [dai.address, userAddress, 1, expectedDaiAmount, 1] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + weth.address, + flashloanAmount.toString(), + 0, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, actualWEthSwapped.toString(), expectedDaiAmount); + + const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); + const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); + const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); + const userAEthBalance = await aEth.balanceOf(userAddress); + const wethBalance = await weth.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.gt(userAEthBalanceBefore.sub(liquidityToSwap)); + expect(wethBalance).to.be.eq(wethBalanceBefore.add(leftOverWeth.toString())); + }); + }); + }); +}); From 96e74cf7079e61ff503c67f37c390b11af51fbbe Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 27 Oct 2020 11:22:16 -0300 Subject: [PATCH 03/34] Update adapters to use the new flashloan interface --- .../adapters/UniswapLiquiditySwapAdapter.sol | 18 +++--- contracts/adapters/UniswapRepayAdapter.sol | 20 +++---- test/uniswapAdapters.spec.ts | 55 +++++++++---------- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 62125264..4cfd156a 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -28,18 +28,18 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * 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 reserve Address to be swapped - * @param amount Amount of the reserve to be swapped - * @param fee Fee of the flash loan + * @param assets Address to be swapped + * @param amounts Amount of the reserve to be swapped + * @param premiums Fee of the flash loan * @param params Additional variadic field to include extra params. Expected parameters: * address assetToSwapTo Address of the reserve to be swapped to and deposited * address user The address of the user * uint256 slippage The max slippage percentage allowed for the swap */ function executeOperation( - address reserve, - uint256 amount, - uint256 fee, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, bytes calldata params ) external override returns (bool) { ( @@ -49,14 +49,14 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) = abi.decode(params, (address, address, uint256)); require(slippage < MAX_SLIPPAGE_PERCENT && slippage >= MIN_SLIPPAGE_PERCENT, 'SLIPPAGE_OUT_OF_RANGE'); - uint256 receivedAmount = swapExactTokensForTokens(reserve, assetToSwapTo, amount, slippage); + uint256 receivedAmount = swapExactTokensForTokens(assets[0], assetToSwapTo, amounts[0], slippage); // Deposit new reserve IERC20(assetToSwapTo).approve(address(pool), receivedAmount); pool.deposit(assetToSwapTo, receivedAmount, user, 0); - uint256 flashLoanDebt = amount.add(fee); - pullATokenAndRepayFlashLoan(reserve, user, flashLoanDebt); + uint256 flashLoanDebt = amounts[0].add(premiums[0]); + pullATokenAndRepayFlashLoan(assets[0], user, flashLoanDebt); return true; } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 2bb8a8a8..70490fa7 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -28,9 +28,9 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * then used to repay a debt on 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 reserve Address to be swapped - * @param amount Amount of the reserve to be swapped - * @param fee Fee of the flash loan + * @param assets Address to be swapped + * @param amounts Amount of the reserve to be swapped + * @param premiums Fee of the flash loan * @param params Additional variadic field to include extra params. Expected parameters: * address assetToSwapTo Address of the reserve to be swapped to and deposited * address user The address of the user @@ -41,9 +41,9 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * uint256 rateMode The rate modes of the debt to be repaid */ function executeOperation( - address reserve, - uint256 amount, - uint256 fee, + address[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, bytes calldata params ) external override returns (bool) { ( @@ -54,17 +54,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 rateMode ) = abi.decode(params, (address, address, uint256, uint256, uint256)); - swapTokensForExactTokens(reserve, assetToSwapTo, amount, repayAmount); + swapTokensForExactTokens(assets[0], assetToSwapTo, amounts[0], repayAmount); // Repay debt IERC20(assetToSwapTo).approve(address(pool), repayAmount); pool.repay(assetToSwapTo, repayAmount, rateMode, user); - uint256 flashLoanDebt = amount.add(fee); - pullATokenAndRepayFlashLoan(reserve, user, flashLoanDebt); + uint256 flashLoanDebt = amounts[0].add(premiums[0]); + pullATokenAndRepayFlashLoan(assets[0], user, flashLoanDebt); // Take care of reserve leftover from the swap - sendLeftOver(reserve, flashLoanDebt, leftOverAction, user); + sendLeftOver(assets[0], flashLoanDebt, leftOverAction, user); return true; } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 6bf2cbc4..18b78be9 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -101,8 +101,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapLiquiditySwapAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -194,8 +194,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapLiquiditySwapAdapter.address, - usdc.address, - flashloanAmount.toString(), + [usdc.address], + [flashloanAmount.toString()], 0, params, 0 @@ -259,8 +259,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapLiquiditySwapAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params1, 0 @@ -271,8 +271,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapLiquiditySwapAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params2, 0 @@ -319,8 +319,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapLiquiditySwapAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -415,8 +415,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -459,7 +459,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.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); @@ -477,8 +476,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -504,7 +503,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.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); @@ -522,8 +520,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -532,7 +530,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when the received amount is less than expected', async () => { - const {users, pool, weth, oracle, dai, aEth, uniswapRepayAdapter, deployer} = testEnv; + const {users, pool, weth, oracle, dai, aEth, uniswapRepayAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -570,8 +568,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -595,8 +593,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); - const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + await aEth.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); // Subtract the FL fee from the amount to be swapped 0,09% const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); @@ -615,8 +612,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -686,8 +683,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 @@ -776,8 +773,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - weth.address, - flashloanAmount.toString(), + [weth.address], + [flashloanAmount.toString()], 0, params, 0 From e5d37e1a8ce7fb28722d1795b183aea59c247562 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 27 Oct 2020 16:32:09 -0300 Subject: [PATCH 04/34] Add view method to estimate swaps outputs --- contracts/adapters/BaseUniswapAdapter.sol | 72 ++++++++++++++++--- contracts/interfaces/IUniswapV2Router02.sol | 4 ++ .../mocks/swap/MockUniswapV2Router02.sol | 28 +++++++- 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 79b55df2..eb70d2e4 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -42,6 +42,48 @@ contract BaseUniswapAdapter { uniswapRouter = _uniswapRouter; } + /** + * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @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 amountOut + */ + function getAmountOut(uint256 amountIn, address reserveIn, address reserveOut) + public + view + returns (uint256) + { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + uint256[] memory amounts = uniswapRouter.getAmountsOut(amountIn, path); + + return amounts[1]; + } + + /** + * @dev Returns the minimum input asset amount required to buy the given output asset amount + * @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 amountIn + */ + function getAmountIn(uint256 amountOut, address reserveIn, address reserveOut) + public + view + returns (uint256) + { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + uint256[] memory amounts = uniswapRouter.getAmountsIn(amountOut, path); + + return amounts[0]; + } + /** * @dev Swaps an `amountToSwap` of an asset to another * @param assetToSwapFrom Origin asset @@ -191,7 +233,27 @@ contract BaseUniswapAdapter { } /** - * @dev Take action with the swap left overs as configured in the parameters + * @dev Pull the ATokens from the user + * @param reserve address of the asset + * @param user address + * @param amount of tokens to be transferred to the contract + */ + function pullAToken( + address reserve, + address user, + uint256 amount + ) internal { + address reserveAToken = getAToken(reserve); + + // transfer from user to adapter + IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); + + // withdraw reserve + pool.withdraw(reserve, amount); + } + + /** + * @dev Pull the ATokens from the user and use them to repay the flashloan * @param reserve address of the asset * @param user address * @param flashLoanDebt need to be repaid @@ -201,13 +263,7 @@ contract BaseUniswapAdapter { address user, uint256 flashLoanDebt ) internal { - address reserveAToken = getAToken(reserve); - - // transfer from user to adapter - IERC20(reserveAToken).safeTransferFrom(user, address(this), flashLoanDebt); - - // withdraw reserve - pool.withdraw(reserve, flashLoanDebt); + pullAToken(reserve, user, flashLoanDebt); // Repay flashloan IERC20(reserve).approve(address(pool), flashLoanDebt); diff --git a/contracts/interfaces/IUniswapV2Router02.sol b/contracts/interfaces/IUniswapV2Router02.sol index cb04c269..6453e74a 100644 --- a/contracts/interfaces/IUniswapV2Router02.sol +++ b/contracts/interfaces/IUniswapV2Router02.sol @@ -17,4 +17,8 @@ interface IUniswapV2Router02 { 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 index 64cb935f..ef4c22c4 100644 --- a/contracts/mocks/swap/MockUniswapV2Router02.sol +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -8,6 +8,8 @@ import {MintableERC20} from '../tokens/MintableERC20.sol'; contract MockUniswapV2Router02 is IUniswapV2Router02 { uint256 internal _amountToReturn; uint256 internal _amountToSwap; + mapping(address => mapping(address => mapping(uint256 => uint256))) internal _amountsIn; + mapping(address => mapping(address => mapping(uint256 => uint256))) internal _amountsOut; function setAmountToReturn(uint256 amount) public { _amountToReturn = amount; @@ -35,8 +37,8 @@ contract MockUniswapV2Router02 is IUniswapV2Router02 { } function swapTokensForExactTokens( - uint amountOut, - uint amountInMax, + uint /* amountOut */, + uint /* amountInMax */, address[] calldata path, address to, uint /* deadline */ @@ -50,4 +52,26 @@ contract MockUniswapV2Router02 is IUniswapV2Router02 { amounts[0] = _amountToSwap; amounts[1] = _amountToReturn; } + + function setAmountOut(uint amountIn, address reserveIn, address reserveOut, uint amountOut) public { + _amountsOut[reserveIn][reserveOut][amountIn] = amountOut; + } + + function setAmountIn(uint amountOut, address reserveIn, address reserveOut, uint amountIn) public { + _amountsIn[reserveIn][reserveOut][amountOut] = amountIn; + } + + function getAmountsOut(uint amountIn, address[] calldata path) external view override returns (uint[] memory) { + uint256[] memory amounts = new uint256[](2); + amounts[0] = amountIn; + amounts[1] = _amountsOut[path[0]][path[1]][amountIn]; + return amounts; + } + + function getAmountsIn(uint amountOut, address[] calldata path) external view override returns (uint[] memory) { + uint256[] memory amounts = new uint256[](2); + amounts[0] = _amountsIn[path[0]][path[1]][amountOut]; + amounts[1] = amountOut; + return amounts; + } } From 16a28d6223896998ae444ab0fc795d0587704842 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 27 Oct 2020 16:33:07 -0300 Subject: [PATCH 05/34] Add swapAndDeposit method to use without flashloan --- .../adapters/UniswapLiquiditySwapAdapter.sol | 27 ++++++ test/uniswapAdapters.spec.ts | 96 +++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 4cfd156a..d44f50fd 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -60,4 +60,31 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { return true; } + + /** + * @dev Swaps an `amountToSwap` of an asset to another and deposits the funds on behalf of the user without using a flashloan. + * This method can be used when the user has no debts. + * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset and + * perform the swap. + * @param assetToSwapFrom Address of the underlying asset to be swap from + * @param assetToSwapTo Address of the underlying asset to be swap to and deposited + * @param amountToSwap How much `assetToSwapFrom` needs to be swapped + * @param user Address that will be pulling the swapped funds + * @param slippage The max slippage percentage allowed for the swap + */ + function swapAndDeposit( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address user, + uint256 slippage + ) external { + pullAToken(assetToSwapFrom, user, amountToSwap); + + uint256 receivedAmount = swapExactTokensForTokens(assetToSwapFrom, assetToSwapTo, amountToSwap, slippage); + + // Deposit new reserve + IERC20(assetToSwapTo).approve(address(pool), receivedAmount); + pool.deposit(assetToSwapTo, receivedAmount, user, 0); + } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 18b78be9..6fe92b8a 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -34,6 +34,36 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await evmRevert(evmSnapshotId); }); + describe('BaseUniswapAdapter', () => { + describe('getAmountOut', () => { + it('should return the estimated amountOut for the asset swap', async () => { + const {weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + const amountIn = parseEther('1'); + const amountOut = parseEther('2'); + + await mockUniswapRouter.setAmountOut(amountIn, weth.address, dai.address, amountOut); + + expect( + await uniswapLiquiditySwapAdapter.getAmountOut(amountIn, weth.address, dai.address) + ).to.be.eq(amountOut); + }); + }); + + describe('getAmountIn', () => { + it('should return the estimated required amountIn for the asset swap', async () => { + const {weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + const amountIn = parseEther('1'); + const amountOut = parseEther('2'); + + await mockUniswapRouter.setAmountIn(amountOut, weth.address, dai.address, amountIn); + + expect( + await uniswapLiquiditySwapAdapter.getAmountIn(amountOut, weth.address, dai.address) + ).to.be.eq(amountIn); + }); + }); + }); + describe('UniswapLiquiditySwapAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { @@ -328,6 +358,72 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INSUFFICIENT_OUTPUT_AMOUNT'); }); }); + + 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, aEth, 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(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + + await expect( + uniswapLiquiditySwapAdapter.swapAndDeposit( + weth.address, + dai.address, + amountWETHtoSwap, + userAddress, + 50 + ) + ) + .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 aEth.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)); + }); + }); }); describe('UniswapRepayAdapter', () => { From fc358b7c14edd01ec02eec59513dc4efadcd093f Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Thu, 29 Oct 2020 17:29:41 -0300 Subject: [PATCH 06/34] merge fixes --- helpers/contracts-deployments.ts | 33 +++++++++++ helpers/contracts-getters.ts | 26 +++++++++ test/__setup.spec.ts | 12 ++-- test/uniswapAdapters.spec.ts | 95 ++++++++++++++++++-------------- 4 files changed, 118 insertions(+), 48 deletions(-) diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 82ac2d97..6fa0445d 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -34,9 +34,12 @@ import { MintableErc20Factory, MockAggregatorFactory, MockFlashLoanReceiverFactory, + MockUniswapV2Router02Factory, PriceOracleFactory, ReserveLogicFactory, StableDebtTokenFactory, + UniswapLiquiditySwapAdapterFactory, + UniswapRepayAdapterFactory, VariableDebtTokenFactory, WalletBalanceProviderFactory, } from '../types'; @@ -374,3 +377,33 @@ export const deployATokensAndRatesHelper = async ( args, verify ); + +export const deployMockUniswapRouter = async (verify?: boolean) => + withSaveAndVerify( + await new MockUniswapV2Router02Factory(await getFirstSigner()).deploy(), + eContractid.MockUniswapV2Router02, + [], + verify + ); + +export const deployUniswapLiquiditySwapAdapter = async ( + args: [tEthereumAddress, tEthereumAddress], + verify?: boolean +) => + withSaveAndVerify( + await new UniswapLiquiditySwapAdapterFactory(await getFirstSigner()).deploy(...args), + eContractid.UniswapLiquiditySwapAdapter, + args, + verify + ); + +export const deployUniswapRepayAdapter = async ( + args: [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 0b03cb63..e9b60e1b 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -11,10 +11,13 @@ import { LendingRateOracleFactory, MintableErc20Factory, MockFlashLoanReceiverFactory, + MockUniswapV2Router02Factory, PriceOracleFactory, ReserveLogicFactory, StableAndVariableTokensHelperFactory, StableDebtTokenFactory, + UniswapLiquiditySwapAdapterFactory, + UniswapRepayAdapterFactory, VariableDebtTokenFactory, } from '../types'; import {Ierc20DetailedFactory} from '../types/Ierc20DetailedFactory'; @@ -222,3 +225,26 @@ export const getATokensAndRatesHelper = async (address?: tEthereumAddress) => .address, await getFirstSigner() ); + +export const getMockUniswapRouter = async (address?: tEthereumAddress) => + await MockUniswapV2Router02Factory.connect( + address || + (await getDb().get(`${eContractid.MockUniswapV2Router02}.${BRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getUniswapLiquiditySwapAdapter = async (address?: tEthereumAddress) => + await UniswapLiquiditySwapAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${BRE.network.name}`).value()) + .address, + await getFirstSigner() + ); + +export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => + await UniswapRepayAdapterFactory.connect( + address || + (await getDb().get(`${eContractid.UniswapRepayAdapter}.${BRE.network.name}`).value()).address, + await getFirstSigner() + ); diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index f9047181..4a9e7241 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -246,19 +246,19 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockUniswapRouter = await deployMockUniswapRouter(); await insertContractAddressInDb(eContractid.MockUniswapV2Router02, mockUniswapRouter.address); - const UniswapLiquiditySwapAdapter = await deployUniswapLiquiditySwapAdapter( + const UniswapLiquiditySwapAdapter = await deployUniswapLiquiditySwapAdapter([ addressesProvider.address, - mockUniswapRouter.address - ); + mockUniswapRouter.address, + ]); await insertContractAddressInDb( eContractid.UniswapLiquiditySwapAdapter, UniswapLiquiditySwapAdapter.address ); - const UniswapRepayAdapter = await deployUniswapRepayAdapter( + const UniswapRepayAdapter = await deployUniswapRepayAdapter([ addressesProvider.address, - mockUniswapRouter.address - ); + mockUniswapRouter.address, + ]); await insertContractAddressInDb(eContractid.UniswapRepayAdapter, UniswapRepayAdapter.address); await deployWalletBalancerProvider(addressesProvider.address); diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 6fe92b8a..50007af0 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1,11 +1,10 @@ import {makeSuite, TestEnv} from './helpers/make-suite'; +import {convertToCurrencyDecimals, getContract} from '../helpers/contracts-helpers'; +import {getMockUniswapRouter} from '../helpers/contracts-getters'; import { - convertToCurrencyDecimals, deployUniswapLiquiditySwapAdapter, deployUniswapRepayAdapter, - getContract, - getMockUniswapRouter, -} from '../helpers/contracts-helpers'; +} from '../helpers/contracts-deployments'; import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; import {Zero} from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; @@ -68,15 +67,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; - await deployUniswapLiquiditySwapAdapter( + await deployUniswapLiquiditySwapAdapter([ addressesProvider.address, - mockUniswapRouter.address - ); + mockUniswapRouter.address, + ]); }); it('should revert if not valid addresses provider', async () => { expect( - deployUniswapLiquiditySwapAdapter(mockUniswapRouter.address, mockUniswapRouter.address) + deployUniswapLiquiditySwapAdapter([mockUniswapRouter.address, mockUniswapRouter.address]) ).to.be.reverted; }); }); @@ -98,7 +97,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens and deposit the out tokens in the pool', async () => { - const {users, weth, oracle, dai, aDai, aEth, pool, uniswapLiquiditySwapAdapter} = testEnv; + const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -114,8 +113,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); - await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + 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); @@ -134,6 +133,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -148,7 +148,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { userAddress ); const userADaiBalance = await aDai.balanceOf(userAddress); - const userAEthBalance = await aEth.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); @@ -227,6 +227,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [usdc.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -249,7 +250,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if slippage param is not inside limits', async () => { - const {users, pool, weth, oracle, dai, aEth, uniswapLiquiditySwapAdapter} = testEnv; + const {users, pool, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -268,7 +269,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); - await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + 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); @@ -292,6 +293,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params1, 0 ) @@ -304,6 +306,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params2, 0 ) @@ -311,7 +314,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when swap exceed slippage', async () => { - const {users, weth, oracle, dai, aEth, pool, uniswapLiquiditySwapAdapter} = testEnv; + const {users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -334,7 +337,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); - await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + 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); @@ -352,6 +355,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -376,7 +380,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens and deposit the out tokens in the pool', async () => { - const {users, weth, oracle, dai, aDai, aEth, uniswapLiquiditySwapAdapter} = testEnv; + const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -392,8 +396,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); - await aEth.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); await expect( uniswapLiquiditySwapAdapter.swapAndDeposit( @@ -414,7 +418,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { userAddress ); const userADaiBalance = await aDai.balanceOf(userAddress); - const userAEthBalance = await aEth.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); @@ -430,11 +434,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; - await deployUniswapRepayAdapter(addressesProvider.address, mockUniswapRouter.address); + await deployUniswapRepayAdapter([addressesProvider.address, mockUniswapRouter.address]); }); it('should revert if not valid addresses provider', async () => { - expect(deployUniswapRepayAdapter(mockUniswapRouter.address, mockUniswapRouter.address)).to + expect(deployUniswapRepayAdapter([mockUniswapRouter.address, mockUniswapRouter.address])).to .be.reverted; }); }); @@ -460,7 +464,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { users, pool, weth, - aEth, + aWETH, oracle, dai, uniswapRepayAdapter, @@ -492,8 +496,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -514,6 +518,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -524,7 +529,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); - const userAEthBalance = await aEth.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); @@ -535,7 +540,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if there is not debt to repay with the specified rate mode', async () => { - const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aEth} = testEnv; + const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -554,7 +559,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await pool.connect(user).borrow(dai.address, expectedDaiAmount, 2, 0, userAddress); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -575,6 +580,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -582,7 +588,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if there is not debt to repay', async () => { - const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aEth} = testEnv; + const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -598,7 +604,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -619,6 +625,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -626,7 +633,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when the received amount is less than expected', async () => { - const {users, pool, weth, oracle, dai, aEth, uniswapRepayAdapter} = testEnv; + const {users, pool, weth, oracle, dai, aWETH, uniswapRepayAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -642,7 +649,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -667,6 +674,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -674,7 +682,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when max amount allowed to swap is bigger than max slippage', async () => { - const {users, pool, weth, oracle, dai, aEth, uniswapRepayAdapter} = testEnv; + const {users, pool, weth, oracle, dai, aWETH, uniswapRepayAdapter} = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -689,7 +697,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); - await aEth.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); + await aWETH.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); // Subtract the FL fee from the amount to be swapped 0,09% const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); @@ -711,6 +719,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -722,7 +731,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { users, pool, weth, - aEth, + aWETH, oracle, dai, uniswapRepayAdapter, @@ -754,8 +763,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -782,6 +791,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -792,7 +802,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); - const userAEthBalance = await aEth.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); @@ -810,7 +820,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { users, pool, weth, - aEth, + aWETH, oracle, dai, uniswapRepayAdapter, @@ -842,8 +852,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); const liquidityToSwap = amountWETHtoSwap; - await aEth.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); - const userAEthBalanceBefore = await aEth.balanceOf(userAddress); + await aWETH.connect(user).approve(uniswapRepayAdapter.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); @@ -872,6 +882,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [weth.address], [flashloanAmount.toString()], 0, + userAddress, params, 0 ) @@ -882,7 +893,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); - const userAEthBalance = await aEth.balanceOf(userAddress); + const userAEthBalance = await aWETH.balanceOf(userAddress); const wethBalance = await weth.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); From 0eddff493354e0015b4e9ed0d9711b750baa2ab4 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 30 Oct 2020 16:59:25 -0300 Subject: [PATCH 07/34] Apply feedback fixes --- contracts/adapters/BaseUniswapAdapter.sol | 94 ++-- .../adapters/UniswapLiquiditySwapAdapter.sol | 70 +-- contracts/adapters/UniswapRepayAdapter.sol | 114 ++++- test/uniswapAdapters.spec.ts | 402 ++++++++++++------ 4 files changed, 438 insertions(+), 242 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index eb70d2e4..26aa60b5 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -5,15 +5,14 @@ pragma experimental ABIEncoderV2; import {PercentageMath} from '../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 {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; import {ILendingPool} from '../interfaces/ILendingPool.sol'; import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; -import {ReserveConfiguration} from '../libraries/configuration/ReserveConfiguration.sol'; import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; - /** * @title BaseUniswapAdapter * @notice Implements the logic for performing assets swaps in Uniswap V2 @@ -23,23 +22,24 @@ contract BaseUniswapAdapter { using SafeMath for uint256; using PercentageMath for uint256; using SafeERC20 for IERC20; - using ReserveConfiguration for ReserveConfiguration.Map; + + enum LeftoverAction {DEPOSIT, TRANSFER} // Max slippage percent allow by param uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% // Min slippage percent allow by param uint256 public constant MIN_SLIPPAGE_PERCENT = 10; // 0,1% - ILendingPoolAddressesProvider public immutable addressesProvider; - IUniswapV2Router02 public immutable uniswapRouter; - ILendingPool public immutable pool; + ILendingPool public immutable POOL; + IPriceOracleGetter public immutable ORACLE; + IUniswapV2Router02 public immutable UNISWAP_ROUTER; event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); - constructor(ILendingPoolAddressesProvider _addressesProvider, IUniswapV2Router02 _uniswapRouter) public { - addressesProvider = _addressesProvider; - pool = ILendingPool(_addressesProvider.getLendingPool()); - uniswapRouter = _uniswapRouter; + constructor(ILendingPoolAddressesProvider addressesProvider, IUniswapV2Router02 uniswapRouter) public { + POOL = ILendingPool(addressesProvider.getLendingPool()); + ORACLE = IPriceOracleGetter(addressesProvider.getPriceOracle()); + UNISWAP_ROUTER = uniswapRouter; } /** @@ -58,7 +58,7 @@ contract BaseUniswapAdapter { path[0] = reserveIn; path[1] = reserveOut; - uint256[] memory amounts = uniswapRouter.getAmountsOut(amountIn, path); + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); return amounts[1]; } @@ -79,7 +79,7 @@ contract BaseUniswapAdapter { path[0] = reserveIn; path[1] = reserveOut; - uint256[] memory amounts = uniswapRouter.getAmountsIn(amountOut, path); + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsIn(amountOut, path); return amounts[0]; } @@ -101,24 +101,23 @@ contract BaseUniswapAdapter { internal returns (uint256) { - uint256 fromAssetDecimals = getDecimals(assetToSwapFrom); - uint256 toAssetDecimals = getDecimals(assetToSwapTo); + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); - (uint256 fromAssetPrice, uint256 toAssetPrice) = getPrices(assetToSwapFrom, assetToSwapTo); + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); uint256 amountOutMin = amountToSwap .mul(fromAssetPrice.mul(10**toAssetDecimals)) .div(toAssetPrice.mul(10**fromAssetDecimals)) .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(slippage)); - IERC20(assetToSwapFrom).approve(address(uniswapRouter), amountToSwap); + IERC20(assetToSwapFrom).approve(address(UNISWAP_ROUTER), amountToSwap); address[] memory path = new address[](2); path[0] = assetToSwapFrom; path[1] = assetToSwapTo; - uint256[] memory amounts = uniswapRouter.swapExactTokensForTokens(amountToSwap, amountOutMin, path, address(this), block.timestamp); - - require(amounts[1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + uint256[] memory amounts = UNISWAP_ROUTER.swapExactTokensForTokens(amountToSwap, amountOutMin, path, address(this), block.timestamp); emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); @@ -143,10 +142,11 @@ contract BaseUniswapAdapter { internal returns (uint256) { - uint256 fromAssetDecimals = getDecimals(assetToSwapFrom); - uint256 toAssetDecimals = getDecimals(assetToSwapTo); + uint256 fromAssetDecimals = _getDecimals(assetToSwapFrom); + uint256 toAssetDecimals = _getDecimals(assetToSwapTo); - (uint256 fromAssetPrice, uint256 toAssetPrice) = getPrices(assetToSwapFrom, assetToSwapTo); + uint256 fromAssetPrice = _getPrice(assetToSwapFrom); + uint256 toAssetPrice = _getPrice(assetToSwapTo); uint256 expectedMaxAmountToSwap = amountToReceive .mul(toAssetPrice.mul(10**fromAssetDecimals)) @@ -155,14 +155,12 @@ contract BaseUniswapAdapter { require(maxAmountToSwap < expectedMaxAmountToSwap, 'maxAmountToSwap exceed max slippage'); - IERC20(assetToSwapFrom).approve(address(uniswapRouter), maxAmountToSwap); + IERC20(assetToSwapFrom).approve(address(UNISWAP_ROUTER), maxAmountToSwap); address[] memory path = new address[](2); path[0] = assetToSwapFrom; path[1] = assetToSwapTo; - uint256[] memory amounts = uniswapRouter.swapTokensForExactTokens(amountToReceive, maxAmountToSwap, path, address(this), block.timestamp); - - require(amounts[1] >= amountToReceive, 'INSUFFICIENT_OUTPUT_AMOUNT'); + uint256[] memory amounts = UNISWAP_ROUTER.swapTokensForExactTokens(amountToReceive, maxAmountToSwap, path, address(this), block.timestamp); emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); @@ -170,34 +168,20 @@ contract BaseUniswapAdapter { } /** - * @dev Get assets prices from the oracle denominated in eth - * @param assetToSwapFrom first asset - * @param assetToSwapTo second asset - * @return fromAssetPrice eth price for the first asset - * @return toAssetPrice eth price for the second asset + * @dev Get the price of the asset from the oracle denominated in eth + * @param asset address + * @return eth price for the asset */ - function getPrices( - address assetToSwapFrom, - address assetToSwapTo - ) - internal - view - returns (uint256 fromAssetPrice, uint256 toAssetPrice) - { - IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle()); - fromAssetPrice = oracle.getAssetPrice(assetToSwapFrom); - toAssetPrice = oracle.getAssetPrice(assetToSwapTo); + 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) { - ReserveConfiguration.Map memory configuration = pool.getConfiguration(asset); - (, , , uint256 decimals, ) = configuration.getParamsMemory(); - - return decimals; + function _getDecimals(address asset) internal view returns (uint256) { + return IERC20Detailed(asset).decimals(); } /** @@ -205,7 +189,7 @@ contract BaseUniswapAdapter { * @return address of the aToken */ function getAToken(address asset) internal view returns (address) { - ReserveLogic.ReserveData memory reserve = pool.getReserveData(asset); + ReserveLogic.ReserveData memory reserve = POOL.getReserveData(asset); return reserve.aTokenAddress; } @@ -213,19 +197,19 @@ contract BaseUniswapAdapter { * @dev Take action with the swap left overs as configured in the parameters * @param asset address of the asset * @param reservedAmount Amount reserved to be used by the contract to repay the flash loan - * @param leftOverAction Flag indicating what to do with the left over balance from the swap: + * @param leftOverAction enum indicating what to do with the left over balance from the swap: * (0) Deposit back * (1) Direct transfer to user * @param user address */ - function sendLeftOver(address asset, uint256 reservedAmount, uint256 leftOverAction, address user) internal { + function sendLeftovers(address asset, uint256 reservedAmount, LeftoverAction leftOverAction, address user) internal { uint256 balance = IERC20(asset).balanceOf(address(this)); uint256 assetLeftOver = balance.sub(reservedAmount); if (assetLeftOver > 0) { - if (leftOverAction == 0) { - IERC20(asset).approve(address(pool), balance); - pool.deposit(asset, assetLeftOver, user, 0); + if (leftOverAction == LeftoverAction.DEPOSIT) { + IERC20(asset).approve(address(POOL), balance); + POOL.deposit(asset, assetLeftOver, user, 0); } else { IERC20(asset).transfer(user, assetLeftOver); } @@ -249,7 +233,7 @@ contract BaseUniswapAdapter { IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); // withdraw reserve - pool.withdraw(reserve, amount); + POOL.withdraw(reserve, amount, address(this)); } /** @@ -266,6 +250,6 @@ contract BaseUniswapAdapter { pullAToken(reserve, user, flashLoanDebt); // Repay flashloan - IERC20(reserve).approve(address(pool), flashLoanDebt); + IERC20(reserve).approve(address(POOL), flashLoanDebt); } } diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index d44f50fd..2a40e0bc 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -16,11 +16,11 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { constructor( - ILendingPoolAddressesProvider _addressesProvider, - IUniswapV2Router02 _uniswapRouter + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter ) public - BaseUniswapAdapter(_addressesProvider, _uniswapRouter) + BaseUniswapAdapter(addressesProvider, uniswapRouter) {} /** @@ -31,32 +31,34 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param assets Address to be swapped * @param amounts Amount of the reserve 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 assetToSwapTo Address of the reserve to be swapped to and deposited - * address user The address of the user + * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and deposited * uint256 slippage The max slippage percentage allowed for the swap */ function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, + address initiator, bytes calldata params ) external override returns (bool) { - ( - address assetToSwapTo, - address user, - uint256 slippage - ) = abi.decode(params, (address, address, uint256)); + require(msg.sender == address(POOL), "CALLER_MUST_BE_LENDING_POOL"); + + (address[] memory assetToSwapToList, uint256 slippage) = abi.decode(params, (address[], uint256)); require(slippage < MAX_SLIPPAGE_PERCENT && slippage >= MIN_SLIPPAGE_PERCENT, 'SLIPPAGE_OUT_OF_RANGE'); + require(assetToSwapToList.length == assets.length, 'INCONSISTENT_PARAMS'); - uint256 receivedAmount = swapExactTokensForTokens(assets[0], assetToSwapTo, amounts[0], slippage); + for (uint256 i = 0; i < assets.length; i++) { + uint256 receivedAmount = swapExactTokensForTokens(assets[i], assetToSwapToList[i], amounts[i], slippage); - // Deposit new reserve - IERC20(assetToSwapTo).approve(address(pool), receivedAmount); - pool.deposit(assetToSwapTo, receivedAmount, user, 0); + // Deposit new reserve + IERC20(assetToSwapToList[i]).approve(address(POOL), receivedAmount); + POOL.deposit(assetToSwapToList[i], receivedAmount, initiator, 0); - uint256 flashLoanDebt = amounts[0].add(premiums[0]); - pullATokenAndRepayFlashLoan(assets[0], user, flashLoanDebt); + uint256 flashLoanDebt = amounts[i].add(premiums[i]); + pullATokenAndRepayFlashLoan(assets[i], initiator, flashLoanDebt); + } return true; } @@ -66,25 +68,35 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * This method can be used when the user has no debts. * The user should give this contract allowance to pull the ATokens in order to withdraw the underlying asset and * perform the swap. - * @param assetToSwapFrom Address of the underlying asset to be swap from - * @param assetToSwapTo Address of the underlying asset to be swap to and deposited - * @param amountToSwap How much `assetToSwapFrom` needs to be swapped - * @param user Address that will be pulling the swapped funds + * @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 * @param slippage The max slippage percentage allowed for the swap */ function swapAndDeposit( - address assetToSwapFrom, - address assetToSwapTo, - uint256 amountToSwap, - address user, + address[] calldata assetToSwapFromList, + address[] calldata assetToSwapToList, + uint256[] calldata amountToSwapList, uint256 slippage ) external { - pullAToken(assetToSwapFrom, user, amountToSwap); + require( + assetToSwapFromList.length == assetToSwapToList.length && assetToSwapFromList.length == amountToSwapList.length, + 'INCONSISTENT_PARAMS' + ); - uint256 receivedAmount = swapExactTokensForTokens(assetToSwapFrom, assetToSwapTo, amountToSwap, slippage); + for (uint256 i = 0; i < assetToSwapFromList.length; i++) { + pullAToken(assetToSwapFromList[i], msg.sender, amountToSwapList[i]); - // Deposit new reserve - IERC20(assetToSwapTo).approve(address(pool), receivedAmount); - pool.deposit(assetToSwapTo, receivedAmount, user, 0); + uint256 receivedAmount = swapExactTokensForTokens( + assetToSwapFromList[i], + assetToSwapToList[i], + amountToSwapList[i], + slippage + ); + + // Deposit new reserve + IERC20(assetToSwapToList[i]).approve(address(POOL), receivedAmount); + POOL.deposit(assetToSwapToList[i], receivedAmount, msg.sender, 0); + } } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 70490fa7..755bfb13 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -15,12 +15,19 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; **/ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + struct RepayParams { + address[] assetToSwapToList; + LeftoverAction leftOverAction; + uint256[] repayAmounts; + uint256[] rateModes; + } + constructor( - ILendingPoolAddressesProvider _addressesProvider, - IUniswapV2Router02 _uniswapRouter + ILendingPoolAddressesProvider addressesProvider, + IUniswapV2Router02 uniswapRouter ) public - BaseUniswapAdapter(_addressesProvider, _uniswapRouter) + BaseUniswapAdapter(addressesProvider, uniswapRouter) {} /** @@ -31,41 +38,102 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param assets Address to be swapped * @param amounts Amount of the reserve 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 assetToSwapTo Address of the reserve to be swapped to and deposited - * address user The address of the user + * address[] assetToSwapToList List of the addresses of the reserve to be swapped to and repay * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: * (0) Deposit back * (1) Direct transfer to user - * uint256 repayAmount Amount of debt to be repaid - * uint256 rateMode The rate modes of the debt to be repaid + * uint256[] repayAmounts List of amounts of debt to be repaid + * uint256[] rateModes List of the rate modes of the debt to be repaid */ function executeOperation( address[] calldata assets, uint256[] calldata amounts, uint256[] calldata premiums, + address initiator, bytes calldata params ) external override returns (bool) { - ( - address assetToSwapTo, - address user, - uint256 leftOverAction, - uint256 repayAmount, - uint256 rateMode - ) = abi.decode(params, (address, address, uint256, uint256, uint256)); + require(msg.sender == address(POOL), "CALLER_MUST_BE_LENDING_POOL"); - swapTokensForExactTokens(assets[0], assetToSwapTo, amounts[0], repayAmount); + RepayParams memory decodedParams = _decodeParams(params); - // Repay debt - IERC20(assetToSwapTo).approve(address(pool), repayAmount); - pool.repay(assetToSwapTo, repayAmount, rateMode, user); + require( + assets.length == decodedParams.assetToSwapToList.length + && assets.length == decodedParams.repayAmounts.length + && assets.length == decodedParams.rateModes.length, + 'INCONSISTENT_PARAMS'); - uint256 flashLoanDebt = amounts[0].add(premiums[0]); - pullATokenAndRepayFlashLoan(assets[0], user, flashLoanDebt); - - // Take care of reserve leftover from the swap - sendLeftOver(assets[0], flashLoanDebt, leftOverAction, user); + for (uint256 i = 0; i < assets.length; i++) { + _swapAndRepay( + assets[i], + decodedParams.assetToSwapToList[i], + amounts[i], + decodedParams.repayAmounts[i], + decodedParams.rateModes[i], + initiator, + decodedParams.leftOverAction, + premiums[i] + ); + } return true; } + + /** + * @dev Perform the swap, the repay of the debt and send back the left overs + * + * @param assetFrom Address of token to be swapped + * @param assetTo Address of token to be received + * @param amount Amount of the reserve to be swapped + * @param repayAmount Amount of the debt to be repaid + * @param rateMode Rate mode of the debt to be repaid + * @param initiator Address of the user + * @param leftOverAction enum indicating what to do with the left over balance from the swap + * @param premium Fee of the flash loan + */ + function _swapAndRepay( + address assetFrom, + address assetTo, + uint256 amount, + uint256 repayAmount, + uint256 rateMode, + address initiator, + LeftoverAction leftOverAction, + uint256 premium + ) internal { + swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); + + // Repay debt + IERC20(assetTo).approve(address(POOL), repayAmount); + POOL.repay(assetTo, repayAmount, rateMode, initiator); + + uint256 flashLoanDebt = amount.add(premium); + pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt); + + // Take care of reserve leftover from the swap + sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); + } + + /** + * @dev Decodes debt information encoded in flashloan 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 repay + * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: + * (0) Deposit back + * (1) Direct transfer to user + * uint256[] repayAmounts List of amounts of debt to be repaid + * uint256[] rateModes List of the rate modes of the debt to be repaid + * @return RepayParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal returns (RepayParams memory) { + ( + address[] memory assetToSwapToList, + LeftoverAction leftOverAction, + uint256[] memory repayAmounts, + uint256[] memory rateModes + ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[])); + + return RepayParams(assetToSwapToList, leftOverAction, repayAmounts, rateModes); + } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 50007af0..555f0ef0 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -121,8 +121,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [dai.address, userAddress, 50] + ['address[]', 'uint256'], + [[dai.address], 50] ); await expect( @@ -132,7 +132,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -158,6 +158,90 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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(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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256'], + [[dai.address, weth.address], 50] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 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(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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256'], + [[dai.address, weth.address], 50] + ); + + 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, @@ -215,8 +299,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [dai.address, userAddress, 50] + ['address[]', 'uint256'], + [[dai.address], 50] ); await expect( @@ -226,7 +310,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, [usdc.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -275,14 +359,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 30% slippage const params1 = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [dai.address, userAddress, 3000] + ['address[]', 'uint256'], + [[dai.address], 3000] ); // 0,05% slippage const params2 = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [dai.address, userAddress, 5] + ['address[]', 'uint256'], + [[dai.address], 5] ); await expect( @@ -292,7 +376,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params1, 0 @@ -305,62 +389,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params2, 0 ) ).to.be.revertedWith('SLIPPAGE_OUT_OF_RANGE'); }); - - it('should revert when swap exceed slippage', 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'); - - await weth.connect(user).mint(amountWETHtoSwap); - await weth.connect(user).transfer(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); - - const daiPrice = await oracle.getAssetPrice(dai.address); - const expectedDaiAmount = await convertToCurrencyDecimals( - dai.address, - new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) - ); - - // 1,5% slippage - const returnedDaiAmountWithBigSlippage = new BigNumber(expectedDaiAmount.toString()) - .multipliedBy(0.985) - .toFixed(0); - await mockUniswapRouter.connect(user).setAmountToReturn(returnedDaiAmountWithBigSlippage); - - // 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); - - // 0,5% slippage - const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256'], - [dai.address, userAddress, 50] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapLiquiditySwapAdapter.address, - [weth.address], - [flashloanAmount.toString()], - 0, - userAddress, - params, - 0 - ) - ).to.be.revertedWith('INSUFFICIENT_OUTPUT_AMOUNT'); - }); }); describe('swapAndDeposit', () => { @@ -400,13 +435,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); await expect( - uniswapLiquiditySwapAdapter.swapAndDeposit( - weth.address, - dai.address, - amountWETHtoSwap, - userAddress, - 50 - ) + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50) ) .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); @@ -427,6 +458,30 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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} = testEnv; + const user = users[0].signer; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address, dai.address], [dai.address], [amountWETHtoSwap], 50) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap, amountWETHtoSwap], 50) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); }); }); @@ -506,8 +561,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] ); await expect( @@ -517,7 +572,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -539,6 +594,132 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + it('should revert if inconsistent params', 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); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params1 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address, weth.address], 0, [expectedDaiAmount], [1]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params1, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params2 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount, expectedDaiAmount], [1]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params2, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params3 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1, 1]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params3, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + }); + + 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); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] + ); + + await expect( + uniswapRepayAdapter + .connect(user) + .executeOperation( + [weth.address], + [flashloanAmount.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; @@ -568,8 +749,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] ); await expect( @@ -579,7 +760,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -613,8 +794,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] ); await expect( @@ -624,7 +805,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -632,55 +813,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.reverted; }); - it('should revert when the received amount is less than expected', 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 liquidityToSwap = amountWETHtoSwap; - await aWETH.connect(user).approve(uniswapRepayAdapter.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 insufficientOutput = new BigNumber(expectedDaiAmount.toString()) - .multipliedBy(0.985) - .toFixed(0); - - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(insufficientOutput); - - const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - 0, - userAddress, - params, - 0 - ) - ).to.be.revertedWith('INSUFFICIENT_OUTPUT_AMOUNT'); - }); - 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; @@ -707,8 +839,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] ); await expect( @@ -718,7 +850,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -779,8 +911,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 0, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 0, [expectedDaiAmount], [1]] ); await expect( @@ -790,7 +922,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 @@ -870,8 +1002,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethBalanceBefore = await weth.balanceOf(userAddress); const params = ethers.utils.defaultAbiCoder.encode( - ['address', 'address', 'uint256', 'uint256', 'uint256'], - [dai.address, userAddress, 1, expectedDaiAmount, 1] + ['address[]', 'uint256', 'uint256[]', 'uint256[]'], + [[dai.address], 1, [expectedDaiAmount], [1]] ); await expect( @@ -881,7 +1013,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, [weth.address], [flashloanAmount.toString()], - 0, + [0], userAddress, params, 0 From fa7fa9f9486f9d217843484bae4a6e611705975b Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Mon, 2 Nov 2020 17:33:00 -0300 Subject: [PATCH 08/34] Add permit support in swap adapters --- contracts/adapters/BaseUniswapAdapter.sol | 48 +- .../adapters/UniswapLiquiditySwapAdapter.sol | 94 +- contracts/adapters/UniswapRepayAdapter.sol | 50 +- contracts/interfaces/IERC20WithPermit.sol | 16 + test/uniswapAdapters.spec.ts | 902 +++++++++++++++++- 5 files changed, 1052 insertions(+), 58 deletions(-) create mode 100644 contracts/interfaces/IERC20WithPermit.sol diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 26aa60b5..48fe5db9 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -12,6 +12,7 @@ import {ILendingPool} from '../interfaces/ILendingPool.sol'; import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; +import {IERC20WithPermit} from '../interfaces/IERC20WithPermit.sol'; /** * @title BaseUniswapAdapter @@ -25,6 +26,20 @@ contract BaseUniswapAdapter { enum LeftoverAction {DEPOSIT, TRANSFER} + struct PermitParams { + uint256[] deadline; + uint8[] v; + bytes32[] r; + bytes32[] s; + } + + struct PermitSignature { + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + // Max slippage percent allow by param uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% // Min slippage percent allow by param @@ -221,14 +236,28 @@ contract BaseUniswapAdapter { * @param reserve address of the asset * @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 user, - uint256 amount + uint256 amount, + PermitSignature memory permitSignature ) internal { address reserveAToken = getAToken(reserve); + if (_usePermit(permitSignature)) { + IERC20WithPermit(reserveAToken).permit( + user, + address(this), + amount, + permitSignature.deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s + ); + } + // transfer from user to adapter IERC20(reserveAToken).safeTransferFrom(user, address(this), amount); @@ -241,15 +270,28 @@ contract BaseUniswapAdapter { * @param reserve address of the asset * @param user address * @param flashLoanDebt need to be repaid + * @param permitSignature struct containing the permit signature */ function pullATokenAndRepayFlashLoan( address reserve, address user, - uint256 flashLoanDebt + uint256 flashLoanDebt, + PermitSignature memory permitSignature ) internal { - pullAToken(reserve, user, flashLoanDebt); + pullAToken(reserve, user, flashLoanDebt, permitSignature); // Repay flashloan IERC20(reserve).approve(address(POOL), flashLoanDebt); } + + /** + * @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); + } } diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 2a40e0bc..221baada 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -15,6 +15,12 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; **/ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + struct SwapParams { + address[] assetToSwapToList; + uint256 slippage; + PermitParams permitParams; + } + constructor( ILendingPoolAddressesProvider addressesProvider, IUniswapV2Router02 uniswapRouter @@ -35,6 +41,10 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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 slippage The max slippage percentage allowed for the swap + * 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, @@ -45,19 +55,46 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) external override returns (bool) { require(msg.sender == address(POOL), "CALLER_MUST_BE_LENDING_POOL"); - (address[] memory assetToSwapToList, uint256 slippage) = abi.decode(params, (address[], uint256)); - require(slippage < MAX_SLIPPAGE_PERCENT && slippage >= MIN_SLIPPAGE_PERCENT, 'SLIPPAGE_OUT_OF_RANGE'); - require(assetToSwapToList.length == assets.length, 'INCONSISTENT_PARAMS'); + SwapParams memory decodedParams = _decodeParams(params); + + require( + decodedParams.slippage < MAX_SLIPPAGE_PERCENT && decodedParams.slippage >= MIN_SLIPPAGE_PERCENT, + 'SLIPPAGE_OUT_OF_RANGE' + ); + + require( + decodedParams.assetToSwapToList.length == assets.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, + 'INCONSISTENT_PARAMS' + ); for (uint256 i = 0; i < assets.length; i++) { - uint256 receivedAmount = swapExactTokensForTokens(assets[i], assetToSwapToList[i], amounts[i], slippage); + uint256 receivedAmount = swapExactTokensForTokens( + assets[i], + decodedParams.assetToSwapToList[i], + amounts[i], + decodedParams.slippage + ); // Deposit new reserve - IERC20(assetToSwapToList[i]).approve(address(POOL), receivedAmount); - POOL.deposit(assetToSwapToList[i], receivedAmount, initiator, 0); + IERC20(decodedParams.assetToSwapToList[i]).approve(address(POOL), receivedAmount); + POOL.deposit(decodedParams.assetToSwapToList[i], receivedAmount, initiator, 0); uint256 flashLoanDebt = amounts[i].add(premiums[i]); - pullATokenAndRepayFlashLoan(assets[i], initiator, flashLoanDebt); + pullATokenAndRepayFlashLoan( + assets[i], + initiator, + flashLoanDebt, + PermitSignature( + decodedParams.permitParams.deadline[i], + decodedParams.permitParams.v[i], + decodedParams.permitParams.r[i], + decodedParams.permitParams.s[i] + ) + ); } return true; @@ -72,20 +109,32 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param assetToSwapToList List of addresses of the underlying asset to be swap to and deposited * @param amountToSwapList List of amounts to be swapped * @param slippage The max slippage percentage allowed for the swap + * 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 swapAndDeposit( address[] calldata assetToSwapFromList, address[] calldata assetToSwapToList, uint256[] calldata amountToSwapList, - uint256 slippage + uint256 slippage, + PermitSignature[] calldata permitParams ) external { require( - assetToSwapFromList.length == assetToSwapToList.length && assetToSwapFromList.length == amountToSwapList.length, + assetToSwapFromList.length == assetToSwapToList.length + && assetToSwapFromList.length == amountToSwapList.length + && assetToSwapFromList.length == permitParams.length, 'INCONSISTENT_PARAMS' ); for (uint256 i = 0; i < assetToSwapFromList.length; i++) { - pullAToken(assetToSwapFromList[i], msg.sender, amountToSwapList[i]); + pullAToken( + assetToSwapFromList[i], + msg.sender, + amountToSwapList[i], + permitParams[i] + ); uint256 receivedAmount = swapExactTokensForTokens( assetToSwapFromList[i], @@ -99,4 +148,29 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.deposit(assetToSwapToList[i], receivedAmount, msg.sender, 0); } } + + /** + * @dev Decodes debt information encoded in flashloan 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 slippage The max slippage percentage allowed for the swap + * uint256[] deadline List of deadlines 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 + * @return SwapParams struct containing decoded params + */ + function _decodeParams(bytes memory params) internal returns (SwapParams memory) { + ( + address[] memory assetToSwapToList, + uint256 slippage, + uint256[] memory deadline, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) = abi.decode(params, (address[], uint256, uint256[], uint8[], bytes32[], bytes32[])); + + return SwapParams(assetToSwapToList, slippage, PermitParams(deadline, v, r, s)); + } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 755bfb13..d105cb5c 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -20,6 +20,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { LeftoverAction leftOverAction; uint256[] repayAmounts; uint256[] rateModes; + PermitParams permitParams; } constructor( @@ -46,6 +47,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * 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, @@ -61,7 +66,11 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { require( assets.length == decodedParams.assetToSwapToList.length && assets.length == decodedParams.repayAmounts.length - && assets.length == decodedParams.rateModes.length, + && assets.length == decodedParams.rateModes.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, 'INCONSISTENT_PARAMS'); for (uint256 i = 0; i < assets.length; i++) { @@ -73,7 +82,13 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.rateModes[i], initiator, decodedParams.leftOverAction, - premiums[i] + premiums[i], + PermitSignature( + decodedParams.permitParams.deadline[i], + decodedParams.permitParams.v[i], + decodedParams.permitParams.r[i], + decodedParams.permitParams.s[i] + ) ); } @@ -91,6 +106,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param initiator Address of the user * @param leftOverAction enum indicating what to do with the left over balance from the swap * @param premium Fee of the flash loan + * @param permitSignature struct containing the permit signature */ function _swapAndRepay( address assetFrom, @@ -100,7 +116,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 rateMode, address initiator, LeftoverAction leftOverAction, - uint256 premium + uint256 premium, + PermitSignature memory permitSignature ) internal { swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); @@ -109,7 +126,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.repay(assetTo, repayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); - pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt); + pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt, permitSignature); // Take care of reserve leftover from the swap sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); @@ -124,6 +141,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * 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 * @return RepayParams struct containing decoded params */ function _decodeParams(bytes memory params) internal returns (RepayParams memory) { @@ -131,9 +152,24 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address[] memory assetToSwapToList, LeftoverAction leftOverAction, uint256[] memory repayAmounts, - uint256[] memory rateModes - ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[])); + uint256[] memory rateModes, + uint256[] memory deadline, + uint8[] memory v, + bytes32[] memory r, + bytes32[] memory s + ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); - return RepayParams(assetToSwapToList, leftOverAction, repayAmounts, rateModes); + return RepayParams( + assetToSwapToList, + leftOverAction, + repayAmounts, + rateModes, + PermitParams( + deadline, + v, + r, + s + ) + ); } } diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol new file mode 100644 index 00000000..448b383b --- /dev/null +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +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/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 555f0ef0..3d9e43aa 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1,5 +1,10 @@ import {makeSuite, TestEnv} from './helpers/make-suite'; -import {convertToCurrencyDecimals, getContract} from '../helpers/contracts-helpers'; +import { + convertToCurrencyDecimals, + getContract, + buildPermitParams, + getSignatureFromTypedData, +} from '../helpers/contracts-helpers'; import {getMockUniswapRouter} from '../helpers/contracts-getters'; import { deployUniswapLiquiditySwapAdapter, @@ -8,11 +13,13 @@ import { import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; import {Zero} from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; -import {evmRevert, evmSnapshot} from '../helpers/misc-utils'; +import {BRE, evmRevert, evmSnapshot} from '../helpers/misc-utils'; import {ethers} from 'ethers'; import {eContractid} from '../helpers/types'; import {AToken} from '../types/AToken'; 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'); @@ -121,8 +128,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -158,6 +172,102 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + 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(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); + const flashloanAmount = flashloanAmountBN.toFixed(0); + const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); + const amountToPermit = flashloanAmountBN.plus(flashloanFee); + + const chainId = BRE.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, + amountToPermit.toFixed(0).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); + + // 0,5% slippage + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [[dai.address], 50, [deadline], [v], [r], [s]] + ); + + 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)); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should revert if inconsistent params', async () => { const {users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; @@ -182,8 +292,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address, weth.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -199,6 +316,116 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params2 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params2, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params3 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params3, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params4 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params4, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params5 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params5, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -225,8 +452,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address, weth.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -299,8 +533,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 50] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 50, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -359,14 +600,28 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 30% slippage const params1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 3000] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 3000, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); // 0,05% slippage const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256'], - [[dai.address], 5] + ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + 5, + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -437,7 +692,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ) .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); @@ -458,6 +720,80 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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(expectedDaiAmount); + + // User will swap liquidity 10 aEth to aDai + const liquidityToSwap = parseEther('10'); + const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + + const chainId = BRE.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], 50, [ + { + deadline, + v, + r, + s, + }, + ]) + ) + .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} = testEnv; const user = users[0].signer; @@ -467,19 +803,52 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address, dai.address], [dai.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address, dai.address], [dai.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50) + .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50, [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ]) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( uniswapLiquiditySwapAdapter .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap, amountWETHtoSwap], 50) + .swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap, amountWETHtoSwap], + 50, + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter + .connect(user) + .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, []) ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); }); @@ -561,8 +930,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -594,6 +981,129 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); + const flashloanAmount = flashloanAmountBN.toFixed(0); + const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); + const amountToPermit = flashloanAmountBN.plus(flashloanFee); + + const chainId = BRE.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, + amountToPermit.toFixed(0).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(flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [[dai.address], 0, [expectedDaiAmount], [1], [deadline], [v], [r], [s]] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params, + 0 + ) + ) + .to.emit(uniswapRepayAdapter, 'Swapped') + .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + + 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)); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should revert if inconsistent params', async () => { const {users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter} = testEnv; const user = users[0].signer; @@ -620,8 +1130,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address, weth.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address, weth.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -639,8 +1167,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount, expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount, expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -658,8 +1204,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params3 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1, 1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1, 1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -675,6 +1239,160 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params4 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params4, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params5 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params5, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params6 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params6, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params7 = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params7, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -703,8 +1421,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -749,8 +1485,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -794,8 +1548,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -839,8 +1611,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -911,8 +1701,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 0, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( @@ -1002,8 +1810,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethBalanceBefore = await weth.balanceOf(userAddress); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint256[]'], - [[dai.address], 1, [expectedDaiAmount], [1]] + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address], + 1, + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] ); await expect( From 94a6a8688cab2c1f0a3033f4dc4defaadd9f493e Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 3 Nov 2020 12:22:15 -0300 Subject: [PATCH 09/34] Update liquidity swap adapter params to include min amount to receive --- contracts/adapters/BaseUniswapAdapter.sol | 22 +- .../adapters/UniswapLiquiditySwapAdapter.sol | 30 +-- test/uniswapAdapters.spec.ts | 254 +++++++++++------- 3 files changed, 188 insertions(+), 118 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 48fe5db9..1b11a8b7 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -40,10 +40,8 @@ contract BaseUniswapAdapter { bytes32 s; } - // Max slippage percent allow by param + // Max slippage percent allowed uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% - // Min slippage percent allow by param - uint256 public constant MIN_SLIPPAGE_PERCENT = 10; // 0,1% ILendingPool public immutable POOL; IPriceOracleGetter public immutable ORACLE; @@ -100,18 +98,18 @@ contract BaseUniswapAdapter { } /** - * @dev Swaps an `amountToSwap` of an asset to another + * @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 slippage the max slippage percentage allowed for the swap + * @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 slippage + uint256 minAmountOut ) internal returns (uint256) @@ -122,17 +120,19 @@ contract BaseUniswapAdapter { uint256 fromAssetPrice = _getPrice(assetToSwapFrom); uint256 toAssetPrice = _getPrice(assetToSwapTo); - uint256 amountOutMin = amountToSwap + uint256 expectedMinAmountOut = amountToSwap .mul(fromAssetPrice.mul(10**toAssetDecimals)) .div(toAssetPrice.mul(10**fromAssetDecimals)) - .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(slippage)); + .percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(MAX_SLIPPAGE_PERCENT)); + + require(expectedMinAmountOut < minAmountOut, 'minAmountOut exceed max slippage'); IERC20(assetToSwapFrom).approve(address(UNISWAP_ROUTER), amountToSwap); address[] memory path = new address[](2); path[0] = assetToSwapFrom; path[1] = assetToSwapTo; - uint256[] memory amounts = UNISWAP_ROUTER.swapExactTokensForTokens(amountToSwap, amountOutMin, path, address(this), block.timestamp); + uint256[] memory amounts = UNISWAP_ROUTER.swapExactTokensForTokens(amountToSwap, minAmountOut, path, address(this), block.timestamp); emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); @@ -146,7 +146,7 @@ contract BaseUniswapAdapter { * @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 received from the swap + * @return the amount swapped */ function swapTokensForExactTokens( address assetToSwapFrom, @@ -179,7 +179,7 @@ contract BaseUniswapAdapter { emit Swapped(assetToSwapFrom, assetToSwapTo, amounts[0], amounts[1]); - return amounts[1]; + return amounts[0]; } /** diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 221baada..5291cb85 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -17,7 +17,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { struct SwapParams { address[] assetToSwapToList; - uint256 slippage; + uint256[] minAmountsToReceive; PermitParams permitParams; } @@ -40,7 +40,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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 slippage The max slippage percentage allowed for the swap + * uint256[] minAmountsToReceive List of min amounts to be received from the swap * 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 @@ -58,12 +58,8 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { SwapParams memory decodedParams = _decodeParams(params); require( - decodedParams.slippage < MAX_SLIPPAGE_PERCENT && decodedParams.slippage >= MIN_SLIPPAGE_PERCENT, - 'SLIPPAGE_OUT_OF_RANGE' - ); - - require( - decodedParams.assetToSwapToList.length == assets.length + assets.length == decodedParams.assetToSwapToList.length + && assets.length == decodedParams.minAmountsToReceive.length && assets.length == decodedParams.permitParams.deadline.length && assets.length == decodedParams.permitParams.v.length && assets.length == decodedParams.permitParams.r.length @@ -76,7 +72,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { assets[i], decodedParams.assetToSwapToList[i], amounts[i], - decodedParams.slippage + decodedParams.minAmountsToReceive[i] ); // Deposit new reserve @@ -108,7 +104,8 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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 - * @param slippage The max slippage percentage allowed 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[] 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 @@ -118,12 +115,13 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address[] calldata assetToSwapFromList, address[] calldata assetToSwapToList, uint256[] calldata amountToSwapList, - uint256 slippage, + uint256[] calldata minAmountsToReceive, PermitSignature[] calldata permitParams ) external { require( assetToSwapFromList.length == assetToSwapToList.length && assetToSwapFromList.length == amountToSwapList.length + && assetToSwapFromList.length == minAmountsToReceive.length && assetToSwapFromList.length == permitParams.length, 'INCONSISTENT_PARAMS' ); @@ -140,7 +138,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { assetToSwapFromList[i], assetToSwapToList[i], amountToSwapList[i], - slippage + minAmountsToReceive[i] ); // Deposit new reserve @@ -153,7 +151,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @dev Decodes debt information encoded in flashloan 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 slippage The max slippage percentage allowed for the swap + * uint256[] minAmountsToReceive List of min amounts to be received from the swap * uint256[] deadline List of deadlines for the permit signature * uint256[] deadline List of deadlines for the permit signature * uint8[] v List of v param for the permit signature @@ -164,13 +162,13 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { function _decodeParams(bytes memory params) internal returns (SwapParams memory) { ( address[] memory assetToSwapToList, - uint256 slippage, + uint256[] memory minAmountsToReceive, uint256[] memory deadline, uint8[] memory v, bytes32[] memory r, bytes32[] memory s - ) = abi.decode(params, (address[], uint256, uint256[], uint8[], bytes32[], bytes32[])); + ) = abi.decode(params, (address[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); - return SwapParams(assetToSwapToList, slippage, PermitParams(deadline, v, r, s)); + return SwapParams(assetToSwapToList, minAmountsToReceive, PermitParams(deadline, v, r, s)); } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 3d9e43aa..259a4ec3 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -126,12 +126,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], - 50, + [expectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -224,10 +223,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); - // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [[dai.address], 50, [deadline], [v], [r], [s]] + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [[dai.address], [expectedDaiAmount], [deadline], [v], [r], [s]] ); await expect( @@ -290,12 +288,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], - 50, + [expectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -318,10 +315,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], - 50, + [expectedDaiAmount], [0, 0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -344,10 +341,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params3 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], - 50, + [expectedDaiAmount], [0], [0, 0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -370,10 +367,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params4 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], - 50, + [expectedDaiAmount], [0], [0], [ @@ -399,10 +396,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params5 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], - 50, + [expectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -426,6 +423,32 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params6 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, weth.address], + [expectedDaiAmount, expectedDaiAmount], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params6, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -450,12 +473,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], - 50, + [expectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -531,12 +553,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(amountUSDCtoSwap.toString()).div(1.0009).toFixed(0); - // 0,5% slippage const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], - 50, + [expectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -574,49 +595,34 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(aDaiBalance).to.be.eq(expectedDaiAmount); }); - it('should revert if slippage param is not inside limits', async () => { - const {users, pool, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + 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'); - await weth.connect(user).mint(amountWETHtoSwap); - await weth.connect(user).transfer(uniswapLiquiditySwapAdapter.address, amountWETHtoSwap); - const daiPrice = await oracle.getAssetPrice(dai.address); const expectedDaiAmount = await convertToCurrencyDecimals( dai.address, new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(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); - // 30% slippage - const params1 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], - 3000, - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] - ); - - // 0,05% slippage - const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - 5, + [smallExpectedDaiAmount], [0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -633,23 +639,10 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [flashloanAmount.toString()], [0], userAddress, - params1, + params, 0 ) - ).to.be.revertedWith('SLIPPAGE_OUT_OF_RANGE'); - await expect( - pool - .connect(user) - .flashLoan( - uniswapLiquiditySwapAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params2, - 0 - ) - ).to.be.revertedWith('SLIPPAGE_OUT_OF_RANGE'); + ).to.be.revertedWith('minAmountOut exceed max slippage'); }); }); @@ -690,16 +683,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); await expect( - uniswapLiquiditySwapAdapter - .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, [ + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ { deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', s: '0x0000000000000000000000000000000000000000000000000000000000000000', }, - ]) + ] + ) ) .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); @@ -763,16 +760,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); await expect( - uniswapLiquiditySwapAdapter - .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, [ + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ { deadline, v, r, s, }, - ]) + ] + ) ) .to.emit(uniswapLiquiditySwapAdapter, 'Swapped') .withArgs(weth.address, dai.address, amountWETHtoSwap.toString(), expectedDaiAmount); @@ -795,35 +796,65 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if inconsistent params', async () => { - const {users, weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + 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], 50, [ + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address, dai.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ { deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', s: '0x0000000000000000000000000000000000000000000000000000000000000000', }, - ]) + ] + ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( - uniswapLiquiditySwapAdapter - .connect(user) - .swapAndDeposit([weth.address], [dai.address, weth.address], [amountWETHtoSwap], 50, [ + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address, weth.address], + [amountWETHtoSwap], + [expectedDaiAmount], + [ { deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', s: '0x0000000000000000000000000000000000000000000000000000000000000000', }, - ]) + ] + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + await expect( + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap, amountWETHtoSwap], + [expectedDaiAmount], + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( @@ -832,25 +863,66 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .swapAndDeposit( [weth.address], [dai.address], - [amountWETHtoSwap, amountWETHtoSwap], - 50, - [ - { - deadline: 0, - v: 0, - r: '0x0000000000000000000000000000000000000000000000000000000000000000', - s: '0x0000000000000000000000000000000000000000000000000000000000000000', - }, - ] + [amountWETHtoSwap], + [expectedDaiAmount], + [] ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); await expect( - uniswapLiquiditySwapAdapter - .connect(user) - .swapAndDeposit([weth.address], [dai.address], [amountWETHtoSwap], 50, []) + uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( + [weth.address], + [dai.address], + [amountWETHtoSwap], + [expectedDaiAmount, expectedDaiAmount], + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) ).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(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], + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) + ).to.be.revertedWith('minAmountOut exceed max slippage'); + }); }); }); From 847ad3b12a9f008e828530d25e60f6c84482e3f3 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 3 Nov 2020 15:37:06 -0300 Subject: [PATCH 10/34] Add batch swaps unit tests --- .../adapters/UniswapLiquiditySwapAdapter.sol | 2 +- contracts/adapters/UniswapRepayAdapter.sol | 2 +- .../mocks/swap/MockUniswapV2Router02.sol | 30 +- test/uniswapAdapters.spec.ts | 917 +++++++++++++++++- 4 files changed, 905 insertions(+), 46 deletions(-) diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 5291cb85..365089a8 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -159,7 +159,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * bytes32[] s List of s param for the permit signature * @return SwapParams struct containing decoded params */ - function _decodeParams(bytes memory params) internal returns (SwapParams memory) { + function _decodeParams(bytes memory params) internal pure returns (SwapParams memory) { ( address[] memory assetToSwapToList, uint256[] memory minAmountsToReceive, diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index d105cb5c..550241ea 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -147,7 +147,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * bytes32[] s List of s param for the permit signature * @return RepayParams struct containing decoded params */ - function _decodeParams(bytes memory params) internal returns (RepayParams memory) { + function _decodeParams(bytes memory params) internal pure returns (RepayParams memory) { ( address[] memory assetToSwapToList, LeftoverAction leftOverAction, diff --git a/contracts/mocks/swap/MockUniswapV2Router02.sol b/contracts/mocks/swap/MockUniswapV2Router02.sol index ef4c22c4..6d771cda 100644 --- a/contracts/mocks/swap/MockUniswapV2Router02.sol +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -6,17 +6,17 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {MintableERC20} from '../tokens/MintableERC20.sol'; contract MockUniswapV2Router02 is IUniswapV2Router02 { - uint256 internal _amountToReturn; - uint256 internal _amountToSwap; + 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; - function setAmountToReturn(uint256 amount) public { - _amountToReturn = amount; + function setAmountToReturn(address reserve, uint256 amount) public { + _amountToReturn[reserve] = amount; } - function setAmountToSwap(uint256 amount) public { - _amountToSwap = amount; + function setAmountToSwap(address reserve, uint256 amount) public { + _amountToSwap[reserve] = amount; } function swapExactTokensForTokens( @@ -28,29 +28,29 @@ contract MockUniswapV2Router02 is IUniswapV2Router02 { ) external override returns (uint256[] memory amounts) { IERC20(path[0]).transferFrom(msg.sender, address(this), amountIn); - MintableERC20(path[1]).mint(_amountToReturn); - IERC20(path[1]).transfer(to, _amountToReturn); + MintableERC20(path[1]).mint(_amountToReturn[path[0]]); + IERC20(path[1]).transfer(to, _amountToReturn[path[0]]); amounts = new uint[](path.length); amounts[0] = amountIn; - amounts[1] = _amountToReturn; + amounts[1] = _amountToReturn[path[0]]; } function swapTokensForExactTokens( - uint /* amountOut */, + uint amountOut, uint /* amountInMax */, address[] calldata path, address to, uint /* deadline */ ) external override returns (uint256[] memory amounts) { - IERC20(path[0]).transferFrom(msg.sender, address(this), _amountToSwap); + IERC20(path[0]).transferFrom(msg.sender, address(this), _amountToSwap[path[0]]); - MintableERC20(path[1]).mint(_amountToReturn); - IERC20(path[1]).transfer(to, _amountToReturn); + MintableERC20(path[1]).mint(amountOut); + IERC20(path[1]).transfer(to, amountOut); amounts = new uint[](path.length); - amounts[0] = _amountToSwap; - amounts[1] = _amountToReturn; + amounts[0] = _amountToSwap[path[0]]; + amounts[1] = amountOut; } function setAmountOut(uint amountIn, address reserveIn, address reserveOut, uint amountOut) public { diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 259a4ec3..35803467 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -89,7 +89,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('executeOperation', () => { beforeEach(async () => { - const {users, weth, dai, pool, deployer} = testEnv; + const {users, weth, dai, usdc, pool, deployer} = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -97,6 +97,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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')); @@ -116,7 +121,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -171,6 +176,281 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], + [0, 0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + 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 = BRE.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); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + const wethFlashloanAmountBN = new BigNumber(amountWETHtoSwap.toString()).div(1.0009); + const wethFlashloanAmount = wethFlashloanAmountBN.toFixed(0); + const wethFlashloanFee = wethFlashloanAmountBN.multipliedBy(9).div(10000); + const wethAmountToPermit = wethFlashloanAmountBN.plus(wethFlashloanFee).toFixed(0); + + const usdcFlashloanAmountBN = new BigNumber(amountUSDCtoSwap.toString()).div(1.0009); + const usdcFlashloanAmount = usdcFlashloanAmountBN.toFixed(0); + const usdcFlashloanFee = usdcFlashloanAmountBN.multipliedBy(9).div(10000); + const usdcAmountToPermit = usdcFlashloanAmountBN.plus(usdcFlashloanFee).toFixed(0); + + const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); + const aWethMsgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapLiquiditySwapAdapter.address, + aWethNonce, + deadline, + wethAmountToPermit.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, + usdcAmountToPermit.toString() + ); + const {v: aUsdcv, r: aUsdcr, s: aUsdcs} = getSignatureFromTypedData( + ownerPrivateKey, + aUsdcMsgParams + ); + + const params = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [deadline, deadline], + [aWETHv, aUsdcv], + [aWETHr, aUsdcr], + [aWETHs, aUsdcs], + ] + ); + + 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)); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should correctly swap tokens with permit', async () => { const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; const user = users[0].signer; @@ -184,7 +464,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -279,7 +559,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -464,7 +744,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -544,7 +824,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .toFixed(0) ); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToReturn(usdc.address, expectedDaiAmount); const aUsdcData = await pool.getReserveData(usdc.address); const aUsdc = await getContract(eContractid.AToken, aUsdcData.aTokenAddress); @@ -608,7 +888,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); const smallExpectedDaiAmount = expectedDaiAmount.div(2); // User will swap liquidity 10 aEth to aDai @@ -675,7 +955,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -731,7 +1011,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); // User will swap liquidity 10 aEth to aDai const liquidityToSwap = parseEther('10'); @@ -899,7 +1179,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - await mockUniswapRouter.setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.setAmountToReturn(weth.address, expectedDaiAmount); const smallExpectedDaiAmount = expectedDaiAmount.div(2); // User will swap liquidity 10 aEth to aDai @@ -923,6 +1203,241 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).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], + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ); + + 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 = BRE.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], + [ + { + deadline, + v: aWETHv, + r: aWETHr, + s: aWETHs, + }, + { + deadline, + v: aUsdcv, + r: aUsdcr, + s: aUsdcs, + }, + ] + ); + + 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)); + }); }); }); @@ -941,7 +1456,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('executeOperation', () => { beforeEach(async () => { - const {users, weth, dai, pool, deployer} = testEnv; + const {users, weth, dai, usdc, lend, pool, deployer} = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -949,10 +1464,27 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await dai.approve(pool.address, parseEther('20000')); await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + const usdcLiquidity = await convertToCurrencyDecimals(usdc.address, '20000'); + 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 lend.mint(parseEther('1000000')); + await lend.approve(pool.address, parseEther('1000000')); + await pool.deposit(lend.address, parseEther('1000000'), 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); + + await lend.mint(parseEther('1000000')); + await lend.approve(pool.address, parseEther('1000000')); + await pool.deposit(lend.address, parseEther('1000000'), userAddress, 0); }); it('should correctly swap tokens and repay debt', async () => { @@ -998,8 +1530,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1125,8 +1656,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1176,6 +1706,342 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); + it('should correctly swap tokens and repay debt for multiple tokens', async () => { + const { + users, + pool, + weth, + oracle, + dai, + uniswapRepayAdapter, + lend, + usdc, + helpersContract, + aWETH, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + + const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); + const amountLendToSwap = await convertToCurrencyDecimals(lend.address, '1'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const lendPrice = await oracle.getAssetPrice(lend.address); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await lend.decimals()).toString(); + const principalDecimals = (await usdc.decimals()).toString(); + + const expectedUsdcAmountForLend = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(amountLendToSwap.toString()) + .times( + new BigNumber(lendPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); + await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForLend, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const usdcStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(usdc.address) + ).stableDebtTokenAddress; + + const usdcStableDebtContract = await getContract( + eContractid.StableDebtToken, + usdcStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + const userUsdcStableDebtAmountBefore = await usdcStableDebtContract.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 lendFlashloanAmount = new BigNumber(amountLendToSwap.toString()) + .div(1.0009) + .toFixed(0); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(lend.address, lendFlashloanAmount); + + await aWETH.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); + + const lendData = await pool.getReserveData(lend.address); + const aLend = await getContract(eContractid.AToken, lendData.aTokenAddress); + await aLend.connect(user).approve(uniswapRepayAdapter.address, amountLendToSwap); + + const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); + const aLendBalanceBefore = await aLend.balanceOf(userAddress); + + const params = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address, usdc.address], + 0, + [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [1, 1], + [0, 0], + [0, 0], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ] + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address, lend.address], + [wethFlashloanAmount.toString(), lendFlashloanAmount.toString()], + [0, 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 userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); + const aWETHBalance = await aWETH.balanceOf(userAddress); + const aLendBalance = await aLend.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); + expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForLend); + expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForLend); + expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); + expect(aLendBalance).to.be.lt(aLendBalanceBefore); + }); + + it('should swap tokens and repay debt for multiple tokens using permit', async () => { + const { + users, + pool, + weth, + oracle, + dai, + uniswapRepayAdapter, + lend, + usdc, + helpersContract, + aWETH, + } = testEnv; + const user = users[0].signer; + const userAddress = users[0].address; + const chainId = BRE.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 amountLendToSwap = await convertToCurrencyDecimals(lend.address, '1'); + + const daiPrice = await oracle.getAssetPrice(dai.address); + const expectedDaiAmountForEth = await convertToCurrencyDecimals( + dai.address, + new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) + ); + + const lendPrice = await oracle.getAssetPrice(lend.address); + const usdcPrice = await oracle.getAssetPrice(usdc.address); + + const collateralDecimals = (await lend.decimals()).toString(); + const principalDecimals = (await usdc.decimals()).toString(); + + const expectedUsdcAmountForLend = await convertToCurrencyDecimals( + usdc.address, + new BigNumber(amountLendToSwap.toString()) + .times( + new BigNumber(lendPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + ) + .div( + new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) + ) + .toFixed(0) + ); + + // Open user Debt + await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); + await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForLend, 1, 0, userAddress); + + const daiStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(dai.address) + ).stableDebtTokenAddress; + + const daiStableDebtContract = await getContract( + eContractid.StableDebtToken, + daiStableDebtTokenAddress + ); + + const usdcStableDebtTokenAddress = ( + await helpersContract.getReserveTokensAddresses(usdc.address) + ).stableDebtTokenAddress; + + const usdcStableDebtContract = await getContract( + eContractid.StableDebtToken, + usdcStableDebtTokenAddress + ); + + const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); + const userUsdcStableDebtAmountBefore = await usdcStableDebtContract.balanceOf(userAddress); + + const lendData = await pool.getReserveData(lend.address); + const aLend = await getContract(eContractid.AToken, lendData.aTokenAddress); + + const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); + const aLendBalanceBefore = await aLend.balanceOf(userAddress); + + // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call + BigNumber.config({ + ROUNDING_MODE: 1, //round down + }); + + const wethFlashloanAmountBN = new BigNumber(amountWETHtoSwap.toString()).div(1.0009); + const wethFlashloanAmount = wethFlashloanAmountBN.toFixed(0); + const wethFlashloanFee = wethFlashloanAmountBN.multipliedBy(9).div(10000); + const wethAmountToPermit = wethFlashloanAmountBN.plus(wethFlashloanFee).toFixed(0); + + const lendFlashloanAmountBN = new BigNumber(amountLendToSwap.toString()).div(1.0009); + const lendFlashloanAmount = lendFlashloanAmountBN.toFixed(0); + const lendFlashloanFee = lendFlashloanAmountBN.multipliedBy(9).div(10000); + const lendAmountToPermit = lendFlashloanAmountBN.plus(lendFlashloanFee).toFixed(0); + + const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); + const aWethMsgParams = buildPermitParams( + chainId, + aWETH.address, + '1', + await aWETH.name(), + userAddress, + uniswapRepayAdapter.address, + aWethNonce, + deadline, + wethAmountToPermit.toString() + ); + const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( + ownerPrivateKey, + aWethMsgParams + ); + + const aLendNonce = (await aLend._nonces(userAddress)).toNumber(); + const aLendMsgParams = buildPermitParams( + chainId, + aLend.address, + '1', + await aLend.name(), + userAddress, + uniswapRepayAdapter.address, + aLendNonce, + deadline, + lendAmountToPermit.toString() + ); + const {v: aLendv, r: aLendr, s: aLends} = getSignatureFromTypedData( + ownerPrivateKey, + aLendMsgParams + ); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(lend.address, lendFlashloanAmount); + + const params = ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [ + [dai.address, usdc.address], + 0, + [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [1, 1], + [deadline, deadline], + [aWETHv, aLendv], + [aWETHr, aLendr], + [aWETHs, aLends], + ] + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address, lend.address], + [wethFlashloanAmount.toString(), lendFlashloanAmount.toString()], + [0, 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 userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); + const aWETHBalance = await aWETH.balanceOf(userAddress); + const aLendBalance = await aLend.balanceOf(userAddress); + + expect(adapterWethBalance).to.be.eq(Zero); + expect(adapterDaiBalance).to.be.eq(Zero); + expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); + expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); + expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForLend); + expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForLend); + expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); + expect(aLendBalance).to.be.lt(aLendBalanceBefore); + + // Restore round up + BigNumber.config({ + ROUNDING_MODE: 0, //round up + }); + }); + it('should revert if inconsistent params', async () => { const {users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter} = testEnv; const user = users[0].signer; @@ -1198,8 +2064,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params1 = ethers.utils.defaultAbiCoder.encode( [ @@ -1489,8 +2354,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1553,8 +2417,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1616,8 +2479,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Subtract the FL fee from the amount to be swapped 0,09% const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1679,8 +2541,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); const flashloanAmount = new BigNumber(bigMaxAmountToSwap.toString()).div(1.0009).toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(flashloanAmount); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1769,8 +2630,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); - await mockUniswapRouter.connect(user).setAmountToSwap(actualWEthSwapped); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); const params = ethers.utils.defaultAbiCoder.encode( [ @@ -1876,8 +2736,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); - await mockUniswapRouter.connect(user).setAmountToSwap(actualWEthSwapped); - await mockUniswapRouter.connect(user).setAmountToReturn(expectedDaiAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); const wethBalanceBefore = await weth.balanceOf(userAddress); From 5d9cd6ebd1ce2dbd96e1c19ab6a17178f9ad8a21 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Wed, 4 Nov 2020 16:51:21 -0300 Subject: [PATCH 11/34] Update getAmountsOut to return prices --- contracts/adapters/BaseUniswapAdapter.sol | 82 ++++++++++++++--- test/uniswapAdapters.spec.ts | 107 ++++++++++++++++++++-- 2 files changed, 166 insertions(+), 23 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 1b11a8b7..2e52742b 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -42,6 +42,10 @@ contract BaseUniswapAdapter { // Max slippage percent allowed uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% + // FLash Loan fee set in lending pool + uint256 public constant FLASHLOAN_PREMIUM_TOTAL = 9; + // USD oracle asset address + address public constant USD_ADDRESS = 0x10F7Fc1F91Ba351f9C629c5947AD69bD03C05b96; ILendingPool public immutable POOL; IPriceOracleGetter public immutable ORACLE; @@ -56,24 +60,39 @@ contract BaseUniswapAdapter { } /** - * @dev Given an input asset amount, returns the maximum output amount of the other asset + * @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 amountOut + * @return uint256 Amount out fo 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 getAmountOut(uint256 amountIn, address reserveIn, address reserveOut) - public - view - returns (uint256) + function getAmountsOut(uint256 amountIn, address reserveIn, address reserveOut) + external + view + returns (uint256, uint256, uint256, uint256) { - address[] memory path = new address[](2); - path[0] = reserveIn; - path[1] = reserveOut; + // Subtract flash loan fee + uint256 finalAmountIn = amountIn.sub(amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); - uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); + uint256 amountOut = _getAmountsOut(reserveIn, reserveOut, finalAmountIn); - return amounts[1]; + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 outPerInPrice = finalAmountIn + .mul(10**18) + .mul(10**reserveOutDecimals) + .div(amountOut.mul(10**reserveInDecimals)); + + return ( + amountOut, + outPerInPrice, + _calcUsdValue(reserveIn, amountIn, reserveInDecimals), + _calcUsdValue(reserveOut, amountOut, reserveOutDecimals) + ); } /** @@ -84,9 +103,9 @@ contract BaseUniswapAdapter { * @return uint256 amountIn */ function getAmountIn(uint256 amountOut, address reserveIn, address reserveOut) - public - view - returns (uint256) + external + view + returns (uint256) { address[] memory path = new address[](2); path[0] = reserveIn; @@ -294,4 +313,39 @@ contract BaseUniswapAdapter { 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 the output amount + */ + function _getAmountsOut(address reserveIn, address reserveOut, uint256 amountIn) internal view returns (uint256) { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); + + return amounts[1]; + } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 35803467..1ebc21f8 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -19,7 +19,7 @@ import {eContractid} from '../helpers/types'; import {AToken} from '../types/AToken'; import {StableDebtToken} from '../types/StableDebtToken'; import {BUIDLEREVM_CHAINID} from '../helpers/buidler-constants'; -import {MAX_UINT_AMOUNT} from '../helpers/constants'; +import {MAX_UINT_AMOUNT, USD_ADDRESS} from '../helpers/constants'; const {parseEther} = ethers.utils; const {expect} = require('chai'); @@ -41,17 +41,106 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); describe('BaseUniswapAdapter', () => { - describe('getAmountOut', () => { - it('should return the estimated amountOut for the asset swap', async () => { - const {weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + 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 amountOut = parseEther('2'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); - await mockUniswapRouter.setAmountOut(amountIn, weth.address, dai.address, amountOut); + const wethPrice = await oracle.getAssetPrice(weth.address); + const daiPrice = await oracle.getAssetPrice(dai.address); + const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); - expect( - await uniswapLiquiditySwapAdapter.getAmountOut(amountIn, weth.address, dai.address) - ).to.be.eq(amountOut); + 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 {lend, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + + const amountIn = parseEther('10'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); + + const lendPrice = await oracle.getAssetPrice(lend.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') + .div(expectedUSDCAmount.mul(parseEther('1'))); + + const lendUsdValue = amountIn + .mul(lendPrice) + .div(parseEther('1')) + .mul(usdPrice) + .div(parseEther('1')); + + const usdcUsdValue = expectedUSDCAmount + .mul(usdcPrice) + .div('1000000') + .mul(usdPrice) + .div(parseEther('1')); + + await mockUniswapRouter.setAmountOut( + amountToSwap, + lend.address, + usdc.address, + expectedUSDCAmount + ); + + const result = await uniswapLiquiditySwapAdapter.getAmountsOut( + amountIn, + lend.address, + usdc.address + ); + + expect(result['0']).to.be.eq(expectedUSDCAmount); + expect(result['1']).to.be.eq(outPerInPrice); + expect(result['2']).to.be.eq(lendUsdValue); + expect(result['3']).to.be.eq(usdcUsdValue); }); }); From 48b9a603a79678f90a30de6bc03ff3370a1dcd42 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Thu, 5 Nov 2020 11:33:11 -0300 Subject: [PATCH 12/34] Update getAmountsIn to return prices --- contracts/adapters/BaseUniswapAdapter.sol | 119 ++++++++++++++++------ test/uniswapAdapters.spec.ts | 96 +++++++++++++++-- 2 files changed, 176 insertions(+), 39 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 2e52742b..9dbdfbdf 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -40,6 +40,13 @@ contract BaseUniswapAdapter { bytes32 s; } + struct AmountCalc { + uint256 calculatedAmount; + uint256 relativePrice; + uint256 amountInUsd; + uint256 amountOutUsd; + } + // Max slippage percent allowed uint256 public constant MAX_SLIPPAGE_PERCENT = 3000; // 30% // FLash Loan fee set in lending pool @@ -64,7 +71,7 @@ contract BaseUniswapAdapter { * @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 fo the reserveOut + * @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) @@ -74,46 +81,39 @@ contract BaseUniswapAdapter { view returns (uint256, uint256, uint256, uint256) { - // Subtract flash loan fee - uint256 finalAmountIn = amountIn.sub(amountIn.mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); - - uint256 amountOut = _getAmountsOut(reserveIn, reserveOut, finalAmountIn); - - uint256 reserveInDecimals = _getDecimals(reserveIn); - uint256 reserveOutDecimals = _getDecimals(reserveOut); - - uint256 outPerInPrice = finalAmountIn - .mul(10**18) - .mul(10**reserveOutDecimals) - .div(amountOut.mul(10**reserveInDecimals)); + AmountCalc memory results = _getAmountsOut(reserveIn, reserveOut, amountIn); return ( - amountOut, - outPerInPrice, - _calcUsdValue(reserveIn, amountIn, reserveInDecimals), - _calcUsdValue(reserveOut, amountOut, reserveOutDecimals) + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd ); } /** - * @dev Returns the minimum input asset amount required to buy the given output asset amount + * @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 amountIn + * @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 getAmountIn(uint256 amountOut, address reserveIn, address reserveOut) + function getAmountsIn(uint256 amountOut, address reserveIn, address reserveOut) external view - returns (uint256) + returns (uint256, uint256, uint256, uint256) { - address[] memory path = new address[](2); - path[0] = reserveIn; - path[1] = reserveOut; + AmountCalc memory results = _getAmountsIn(reserveIn, reserveOut, amountOut); - uint256[] memory amounts = UNISWAP_ROUTER.getAmountsIn(amountOut, path); - - return amounts[0]; + return ( + results.calculatedAmount, + results.relativePrice, + results.amountInUsd, + results.amountOutUsd + ); } /** @@ -337,15 +337,72 @@ contract BaseUniswapAdapter { * @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 the output amount + * @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 _getAmountsOut(address reserveIn, address reserveOut, uint256 amountIn) internal view returns (uint256) { + function _getAmountsOut(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 path = new address[](2); path[0] = reserveIn; path[1] = reserveOut; - uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(amountIn, path); + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsOut(finalAmountIn, path); - return amounts[1]; + uint256 reserveInDecimals = _getDecimals(reserveIn); + uint256 reserveOutDecimals = _getDecimals(reserveOut); + + uint256 outPerInPrice = finalAmountIn + .mul(10**18) + .mul(10**reserveOutDecimals) + .div(amounts[1].mul(10**reserveInDecimals)); + + return AmountCalc( + amounts[1], + outPerInPrice, + _calcUsdValue(reserveIn, amountIn, reserveInDecimals), + _calcUsdValue(reserveOut, amounts[1], reserveOutDecimals) + ); + } + + /** + * @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 _getAmountsIn(address reserveIn, address reserveOut, uint256 amountOut) internal view returns (AmountCalc memory) { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + uint256[] memory amounts = UNISWAP_ROUTER.getAmountsIn(amountOut, path); + + // Subtract flash loan fee + uint256 finalAmountIn = amounts[0].sub(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) + ); } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 1ebc21f8..7dc7f03c 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -109,7 +109,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const outPerInPrice = amountToSwap .mul(parseEther('1')) - .mul('1000000') + .mul('1000000') // usdc 6 decimals .div(expectedUSDCAmount.mul(parseEther('1'))); const lendUsdValue = amountIn @@ -120,7 +120,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const usdcUsdValue = expectedUSDCAmount .mul(usdcPrice) - .div('1000000') + .div('1000000') // usdc 6 decimals .mul(usdPrice) .div(parseEther('1')); @@ -144,17 +144,97 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe('getAmountIn', () => { + describe('getAmountsIn', () => { it('should return the estimated required amountIn for the asset swap', async () => { - const {weth, dai, uniswapLiquiditySwapAdapter} = testEnv; + const {weth, dai, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const amountIn = parseEther('1'); - const amountOut = parseEther('2'); + 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 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); - expect( - await uniswapLiquiditySwapAdapter.getAmountIn(amountOut, weth.address, dai.address) - ).to.be.eq(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 {lend, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + + const amountIn = parseEther('10'); + const flashloanPremium = amountIn.mul(9).div(10000); + const amountToSwap = amountIn.sub(flashloanPremium); + + const lendPrice = await oracle.getAssetPrice(lend.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 lendUsdValue = amountToSwap + .mul(lendPrice) + .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, lend.address, usdc.address, amountIn); + + const result = await uniswapLiquiditySwapAdapter.getAmountsIn( + amountOut, + lend.address, + usdc.address + ); + + expect(result['0']).to.be.eq(amountToSwap); + expect(result['1']).to.be.eq(inPerOutPrice); + expect(result['2']).to.be.eq(lendUsdValue); + expect(result['3']).to.be.eq(usdcUsdValue); }); }); }); From 20bbae88d39929a84d2a0374007dea8e8346da40 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Thu, 5 Nov 2020 16:49:55 -0300 Subject: [PATCH 13/34] Add swapAllBalance parameter for liquidity swap --- contracts/adapters/BaseUniswapAdapter.sol | 33 +-- .../adapters/UniswapLiquiditySwapAdapter.sol | 114 ++++++++-- contracts/adapters/UniswapRepayAdapter.sol | 44 +++- test/uniswapAdapters.spec.ts | 208 ++++++++++++++++-- 4 files changed, 327 insertions(+), 72 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 9dbdfbdf..96ad36ae 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -124,7 +124,7 @@ contract BaseUniswapAdapter { * @param minAmountOut the min amount of `assetToSwapTo` to be received from the swap * @return the amount received from the swap */ - function swapExactTokensForTokens( + function _swapExactTokensForTokens( address assetToSwapFrom, address assetToSwapTo, uint256 amountToSwap, @@ -167,7 +167,7 @@ contract BaseUniswapAdapter { * @param amountToReceive Exact amount of `assetToSwapTo` to receive * @return the amount swapped */ - function swapTokensForExactTokens( + function _swapTokensForExactTokens( address assetToSwapFrom, address assetToSwapTo, uint256 maxAmountToSwap, @@ -222,7 +222,7 @@ contract BaseUniswapAdapter { * @dev Get the aToken associated to the asset * @return address of the aToken */ - function getAToken(address asset) internal view returns (address) { + function _getAToken(address asset) internal view returns (address) { ReserveLogic.ReserveData memory reserve = POOL.getReserveData(asset); return reserve.aTokenAddress; } @@ -236,7 +236,7 @@ contract BaseUniswapAdapter { * (1) Direct transfer to user * @param user address */ - function sendLeftovers(address asset, uint256 reservedAmount, LeftoverAction leftOverAction, address user) internal { + function _sendLeftovers(address asset, uint256 reservedAmount, LeftoverAction leftOverAction, address user) internal { uint256 balance = IERC20(asset).balanceOf(address(this)); uint256 assetLeftOver = balance.sub(reservedAmount); @@ -253,18 +253,18 @@ contract BaseUniswapAdapter { /** * @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( + function _pullAToken( address reserve, + address reserveAToken, address user, uint256 amount, PermitSignature memory permitSignature ) internal { - address reserveAToken = getAToken(reserve); - if (_usePermit(permitSignature)) { IERC20WithPermit(reserveAToken).permit( user, @@ -284,25 +284,6 @@ contract BaseUniswapAdapter { POOL.withdraw(reserve, amount, address(this)); } - /** - * @dev Pull the ATokens from the user and use them to repay the flashloan - * @param reserve address of the asset - * @param user address - * @param flashLoanDebt need to be repaid - * @param permitSignature struct containing the permit signature - */ - function pullATokenAndRepayFlashLoan( - address reserve, - address user, - uint256 flashLoanDebt, - PermitSignature memory permitSignature - ) internal { - pullAToken(reserve, user, flashLoanDebt, permitSignature); - - // Repay flashloan - IERC20(reserve).approve(address(POOL), flashLoanDebt); - } - /** * @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. diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 365089a8..7dd365d6 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -18,6 +18,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { struct SwapParams { address[] assetToSwapToList; uint256[] minAmountsToReceive; + bool[] swapAllBalance; PermitParams permitParams; } @@ -41,6 +42,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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[] 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 @@ -60,6 +62,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { require( assets.length == decodedParams.assetToSwapToList.length && assets.length == decodedParams.minAmountsToReceive.length + && assets.length == decodedParams.swapAllBalance.length && assets.length == decodedParams.permitParams.deadline.length && assets.length == decodedParams.permitParams.v.length && assets.length == decodedParams.permitParams.r.length @@ -68,22 +71,14 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ); for (uint256 i = 0; i < assets.length; i++) { - uint256 receivedAmount = swapExactTokensForTokens( + _swapLiquidity( assets[i], decodedParams.assetToSwapToList[i], amounts[i], - decodedParams.minAmountsToReceive[i] - ); - - // Deposit new reserve - IERC20(decodedParams.assetToSwapToList[i]).approve(address(POOL), receivedAmount); - POOL.deposit(decodedParams.assetToSwapToList[i], receivedAmount, initiator, 0); - - uint256 flashLoanDebt = amounts[i].add(premiums[i]); - pullATokenAndRepayFlashLoan( - assets[i], + premiums[i], initiator, - flashLoanDebt, + decodedParams.minAmountsToReceive[i], + decodedParams.swapAllBalance[i], PermitSignature( decodedParams.permitParams.deadline[i], decodedParams.permitParams.v[i], @@ -103,7 +98,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * 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 + * @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[] deadline List of deadlines for the permit signature @@ -127,17 +122,23 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ); for (uint256 i = 0; i < assetToSwapFromList.length; i++) { - pullAToken( + address aToken = _getAToken(assetToSwapFromList[i]); + + uint256 aTokenInitiatorBalance = IERC20(aToken).balanceOf(msg.sender); + uint256 amountToSwap = amountToSwapList[i] > aTokenInitiatorBalance ? aTokenInitiatorBalance : amountToSwapList[i]; + + _pullAToken( assetToSwapFromList[i], + aToken, msg.sender, - amountToSwapList[i], + amountToSwap, permitParams[i] ); - uint256 receivedAmount = swapExactTokensForTokens( + uint256 receivedAmount = _swapExactTokensForTokens( assetToSwapFromList[i], assetToSwapToList[i], - amountToSwapList[i], + amountToSwap, minAmountsToReceive[i] ); @@ -147,12 +148,61 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { } } + /** + * @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 flashloan + * @param premium Premium of the flashloan + * @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 + */ + function _swapLiquidity( + address assetFrom, + address assetTo, + uint256 amount, + uint256 premium, + address initiator, + uint256 minAmountToReceive, + bool swapAllBalance, + PermitSignature memory permitSignature + ) internal { + address aToken = _getAToken(assetFrom); + + uint256 aTokenInitiatorBalance = IERC20(aToken).balanceOf(initiator); + uint256 amountToSwap = swapAllBalance ? aTokenInitiatorBalance.sub(premium) : amount; + + uint256 receivedAmount = _swapExactTokensForTokens( + assetFrom, + assetTo, + amountToSwap, + minAmountToReceive + ); + + // Deposit new reserve + IERC20(assetTo).approve(address(POOL), receivedAmount); + POOL.deposit(assetTo, receivedAmount, initiator, 0); + + uint256 flashLoanDebt = amount.add(premium); + uint256 amountToPull = swapAllBalance ? aTokenInitiatorBalance : flashLoanDebt; + + _pullATokenAndRepayFlashLoan( + assetFrom, + aToken, + initiator, + amountToPull, + flashLoanDebt, + permitSignature + ); + } + /** * @dev Decodes debt information encoded in flashloan 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 - * uint256[] deadline List of deadlines for the permit signature + * bool[] swapAllBalance Flag indicating if all the user balance should be swapped * 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 @@ -163,12 +213,36 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ( address[] memory assetToSwapToList, uint256[] memory minAmountsToReceive, + bool[] memory swapAllBalance, uint256[] memory deadline, uint8[] memory v, bytes32[] memory r, bytes32[] memory s - ) = abi.decode(params, (address[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); + ) = abi.decode(params, (address[], uint256[], bool[], uint256[], uint8[], bytes32[], bytes32[])); - return SwapParams(assetToSwapToList, minAmountsToReceive, PermitParams(deadline, v, r, s)); + return SwapParams(assetToSwapToList, minAmountsToReceive, swapAllBalance, PermitParams(deadline, v, r, s)); + } + + /** + * @dev Pull the ATokens from the user and use them to repay the flashloan + * @param reserve address of the asset + * @param reserveAToken address of the aToken of the reserve + * @param user address + * @param amountToPull amount to be pulled from the user + * @param flashLoanDebt need to be repaid + * @param permitSignature struct containing the permit signature + */ + function _pullATokenAndRepayFlashLoan( + address reserve, + address reserveAToken, + address user, + uint256 amountToPull, + uint256 flashLoanDebt, + PermitSignature memory permitSignature + ) internal { + _pullAToken(reserve, reserveAToken, user, amountToPull, permitSignature); + + // Repay flashloan + IERC20(reserve).approve(address(POOL), flashLoanDebt); } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 550241ea..59f9c323 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -119,17 +119,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 premium, PermitSignature memory permitSignature ) internal { - swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); + _swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); // Repay debt IERC20(assetTo).approve(address(POOL), repayAmount); POOL.repay(assetTo, repayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); - pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt, permitSignature); + _pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt, permitSignature); // Take care of reserve leftover from the swap - sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); + _sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); } /** @@ -161,15 +161,35 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { return RepayParams( assetToSwapToList, - leftOverAction, - repayAmounts, - rateModes, - PermitParams( - deadline, - v, - r, - s - ) + leftOverAction, + repayAmounts, + rateModes, + PermitParams( + deadline, + v, + r, + s + ) ); } + + /** + * @dev Pull the ATokens from the user and use them to repay the flashloan + * @param reserve address of the asset + * @param user address + * @param flashLoanDebt need to be repaid + * @param permitSignature struct containing the permit signature + */ + function _pullATokenAndRepayFlashLoan( + address reserve, + address user, + uint256 flashLoanDebt, + PermitSignature memory permitSignature + ) internal { + address reserveAToken = _getAToken(reserve); + _pullAToken(reserve, reserveAToken, user, flashLoanDebt, permitSignature); + + // Repay flashloan + IERC20(reserve).approve(address(POOL), flashLoanDebt); + } } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 7dc7f03c..a86718b0 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -301,12 +301,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], [expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -411,12 +412,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, dai.address], [expectedDaiAmountForEth, expectedDaiAmountForUsdc], [0, 0], [0, 0], + [0, 0], [ '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -572,10 +574,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, dai.address], [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], [deadline, deadline], [aWETHv, aUsdcv], [aWETHr, aUsdcr], @@ -673,8 +676,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [[dai.address], [expectedDaiAmount], [deadline], [v], [r], [s]] + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [[dai.address], [expectedDaiAmount], [0], [deadline], [v], [r], [s]] ); await expect( @@ -738,12 +741,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -764,11 +768,12 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount], [0, 0], + [0, 0], [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -790,10 +795,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params3 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount], + [0, 0], [0], [0, 0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], @@ -816,12 +822,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params4 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount], [0], [0], + [0], [ '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -845,12 +852,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params5 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], [ '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -874,12 +882,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('INCONSISTENT_PARAMS'); const params6 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address, weth.address], [expectedDaiAmount, expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -898,6 +907,33 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params7 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + [expectedDaiAmount], + [0, 0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params7, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -923,12 +959,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], [expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -1003,12 +1040,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const flashloanAmount = new BigNumber(amountUSDCtoSwap.toString()).div(1.0009).toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], [expectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -1068,12 +1106,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], [ [dai.address], [smallExpectedDaiAmount], [0], [0], + [0], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ['0x0000000000000000000000000000000000000000000000000000000000000000'], ] @@ -1093,6 +1132,81 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).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 = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + [ + [dai.address], + [expectedDaiAmount], + [1], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ] + ); + + // Flashloan + premium > aToken balance. Then it will only swap the balance + 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', () => { @@ -1607,6 +1721,72 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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], + [ + { + deadline: 0, + v: 0, + r: '0x0000000000000000000000000000000000000000000000000000000000000000', + s: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + ] + ) + ) + .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); + }); }); }); From 1faffa2c39e86de3ec0e80e1eb84dfa21c2dd696 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 6 Nov 2020 12:12:08 -0300 Subject: [PATCH 14/34] Add permit amount parameter to correctly execute permit call --- contracts/adapters/BaseUniswapAdapter.sol | 4 +- .../adapters/UniswapLiquiditySwapAdapter.sol | 18 +- contracts/adapters/UniswapRepayAdapter.sol | 8 +- test/uniswapAdapters.spec.ts | 1168 ++++++++--------- 4 files changed, 602 insertions(+), 596 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 96ad36ae..c69fa9a0 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -27,6 +27,7 @@ contract BaseUniswapAdapter { enum LeftoverAction {DEPOSIT, TRANSFER} struct PermitParams { + uint256[] amount; uint256[] deadline; uint8[] v; bytes32[] r; @@ -34,6 +35,7 @@ contract BaseUniswapAdapter { } struct PermitSignature { + uint256 amount; uint256 deadline; uint8 v; bytes32 r; @@ -269,7 +271,7 @@ contract BaseUniswapAdapter { IERC20WithPermit(reserveAToken).permit( user, address(this), - amount, + permitSignature.amount, permitSignature.deadline, permitSignature.v, permitSignature.r, diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 7dd365d6..83b334d1 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -43,6 +43,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * 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 @@ -63,6 +64,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { 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 @@ -80,6 +82,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.minAmountsToReceive[i], decodedParams.swapAllBalance[i], PermitSignature( + decodedParams.permitParams.amount[i], decodedParams.permitParams.deadline[i], decodedParams.permitParams.v[i], decodedParams.permitParams.r[i], @@ -101,10 +104,11 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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[] 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 + * 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 */ function swapAndDeposit( address[] calldata assetToSwapFromList, @@ -203,6 +207,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * 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 @@ -214,13 +219,14 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address[] memory assetToSwapToList, uint256[] memory minAmountsToReceive, bool[] memory swapAllBalance, + uint256[] memory permitAmount, uint256[] memory deadline, uint8[] memory v, bytes32[] memory r, bytes32[] memory s - ) = abi.decode(params, (address[], uint256[], bool[], uint256[], uint8[], bytes32[], bytes32[])); + ) = abi.decode(params, (address[], uint256[], bool[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); - return SwapParams(assetToSwapToList, minAmountsToReceive, swapAllBalance, PermitParams(deadline, v, r, s)); + return SwapParams(assetToSwapToList, minAmountsToReceive, swapAllBalance, PermitParams(permitAmount, deadline, v, r, s)); } /** diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 59f9c323..46d9c5b0 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -47,6 +47,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * 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 @@ -67,6 +68,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { assets.length == decodedParams.assetToSwapToList.length && assets.length == decodedParams.repayAmounts.length && assets.length == decodedParams.rateModes.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 @@ -84,6 +86,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.leftOverAction, premiums[i], PermitSignature( + decodedParams.permitParams.amount[i], decodedParams.permitParams.deadline[i], decodedParams.permitParams.v[i], decodedParams.permitParams.r[i], @@ -141,6 +144,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256[] repayAmounts List of amounts of debt to be repaid * uint256[] rateModes List of the rate modes of the debt to be repaid + * 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 @@ -153,11 +157,12 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { LeftoverAction leftOverAction, uint256[] memory repayAmounts, uint256[] memory rateModes, + uint256[] memory permitAmount, uint256[] memory deadline, uint8[] memory v, bytes32[] memory r, bytes32[] memory s - ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); + ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); return RepayParams( assetToSwapToList, @@ -165,6 +170,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { repayAmounts, rateModes, PermitParams( + permitAmount, deadline, v, r, diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index a86718b0..2e23f555 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -4,6 +4,8 @@ import { getContract, buildPermitParams, getSignatureFromTypedData, + buildLiquiditySwapParams, + buildRepayAdapterParams, } from '../helpers/contracts-helpers'; import {getMockUniswapRouter} from '../helpers/contracts-getters'; import { @@ -300,17 +302,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -411,22 +411,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .div(1.0009) .toFixed(0); - const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + const params = buildLiquiditySwapParams( + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], + [0, 0], + [0, 0], + [0, 0], [ - [dai.address, dai.address], - [expectedDaiAmountForEth, expectedDaiAmountForUsdc], - [0, 0], - [0, 0], - [0, 0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ] ); @@ -524,20 +522,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); const userAUsdcBalanceBefore = await aUsdc.balanceOf(userAddress); - // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call - BigNumber.config({ - ROUNDING_MODE: 1, //round down - }); + const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) + .div(1.0009) + .toFixed(0); - const wethFlashloanAmountBN = new BigNumber(amountWETHtoSwap.toString()).div(1.0009); - const wethFlashloanAmount = wethFlashloanAmountBN.toFixed(0); - const wethFlashloanFee = wethFlashloanAmountBN.multipliedBy(9).div(10000); - const wethAmountToPermit = wethFlashloanAmountBN.plus(wethFlashloanFee).toFixed(0); - - const usdcFlashloanAmountBN = new BigNumber(amountUSDCtoSwap.toString()).div(1.0009); - const usdcFlashloanAmount = usdcFlashloanAmountBN.toFixed(0); - const usdcFlashloanFee = usdcFlashloanAmountBN.multipliedBy(9).div(10000); - const usdcAmountToPermit = usdcFlashloanAmountBN.plus(usdcFlashloanFee).toFixed(0); + const usdcFlashloanAmount = new BigNumber(amountUSDCtoSwap.toString()) + .div(1.0009) + .toFixed(0); const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); const aWethMsgParams = buildPermitParams( @@ -549,7 +540,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, aWethNonce, deadline, - wethAmountToPermit.toString() + amountWETHtoSwap.toString() ); const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( ownerPrivateKey, @@ -566,24 +557,21 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, aUsdcNonce, deadline, - usdcAmountToPermit.toString() + amountUSDCtoSwap.toString() ); const {v: aUsdcv, r: aUsdcr, s: aUsdcs} = getSignatureFromTypedData( ownerPrivateKey, aUsdcMsgParams ); - - const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address, dai.address], - [expectedDaiAmountForEth, expectedDaiAmountForUsdc], - [0, 0], - [deadline, deadline], - [aWETHv, aUsdcv], - [aWETHr, aUsdcr], - [aWETHs, aUsdcs], - ] + const params = buildLiquiditySwapParams( + [dai.address, dai.address], + [expectedDaiAmountForEth, expectedDaiAmountForUsdc], + [0, 0], + [amountWETHtoSwap, amountUSDCtoSwap], + [deadline, deadline], + [aWETHv, aUsdcv], + [aWETHr, aUsdcr], + [aWETHs, aUsdcs] ); await pool @@ -616,11 +604,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(amountWETHtoSwap)); expect(userAUsdcBalance).to.be.lt(userAUsdcBalanceBefore); expect(userAUsdcBalance).to.be.gte(userAUsdcBalanceBefore.sub(amountUSDCtoSwap)); - - // Restore round up - BigNumber.config({ - ROUNDING_MODE: 0, //round up - }); }); it('should correctly swap tokens with permit', async () => { @@ -642,16 +625,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = parseEther('10'); const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); - // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call - BigNumber.config({ - ROUNDING_MODE: 1, //round down - }); - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); - const flashloanAmount = flashloanAmountBN.toFixed(0); - const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); - const amountToPermit = flashloanAmountBN.plus(flashloanFee); + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; @@ -665,7 +640,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapLiquiditySwapAdapter.address, nonce, deadline, - amountToPermit.toFixed(0).toString() + liquidityToSwap.toString() ); const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -675,9 +650,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); - const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [[dai.address], [expectedDaiAmount], [0], [deadline], [v], [r], [s]] + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [liquidityToSwap], + [deadline], + [v], + [r], + [s] ); await expect( @@ -711,11 +692,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userADaiBalance).to.be.eq(expectedDaiAmount); expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); - - // Restore round up - BigNumber.config({ - ROUNDING_MODE: 0, //round up - }); }); it('should revert if inconsistent params', async () => { @@ -740,17 +716,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address, weth.address], - [expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -767,17 +741,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params2 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address, weth.address], - [expectedDaiAmount], - [0, 0], - [0, 0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params2 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0, 0], + [0, 0], + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -794,17 +766,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params3 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address, weth.address], - [expectedDaiAmount], - [0, 0], - [0], - [0, 0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params3 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0, 0], + [0], + [0, 0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -821,20 +791,18 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params4 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + const params4 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], [ - [dai.address, weth.address], - [expectedDaiAmount], - [0], - [0], - [0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', + ], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -851,19 +819,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params5 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], + const params5 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], [ - [dai.address, weth.address], - [expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ] ); @@ -881,17 +847,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params6 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address, weth.address], - [expectedDaiAmount, expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params6 = buildLiquiditySwapParams( + [dai.address, weth.address], + [expectedDaiAmount, expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -908,17 +872,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params7 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [expectedDaiAmount], - [0, 0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params7 = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0, 0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -934,6 +896,31 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params8 = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0, 0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapLiquiditySwapAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params8, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -958,17 +945,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -1039,17 +1024,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [expectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -1105,17 +1088,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [smallExpectedDaiAmount], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address], + [smallExpectedDaiAmount], + [0], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -1157,20 +1138,111 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); await aWETH.connect(user).approve(uniswapLiquiditySwapAdapter.address, liquidityToSwap); - const params = ethers.utils.defaultAbiCoder.encode( - ['address[]', 'uint256[]', 'bool[]', 'uint256[]', 'uint8[]', 'bytes32[]', 'bytes32[]'], - [ - [dai.address], - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildLiquiditySwapParams( + [dai.address], + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); - // Flashloan + premium > aToken balance. Then it will only swap the balance + // 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 = BRE.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] + ); + + // Flashloan + premium > aToken balance. Then it will only swap the balance - premium const flashloanFee = liquidityToSwap.mul(9).div(10000); const swappedAmount = liquidityToSwap.sub(flashloanFee); @@ -1253,6 +1325,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1330,6 +1403,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: liquidityToSwap, deadline, v, r, @@ -1377,6 +1451,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1394,6 +1469,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1411,6 +1487,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1440,6 +1517,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount, expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1477,6 +1555,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [smallExpectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1551,12 +1630,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmountForEth, expectedDaiAmountForUsdc], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', s: '0x0000000000000000000000000000000000000000000000000000000000000000', }, { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1688,12 +1769,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmountForEth, expectedDaiAmountForUsdc], [ { + amount: amountWETHtoSwap, deadline, v: aWETHv, r: aWETHr, s: aWETHs, }, { + amount: amountUSDCtoSwap, deadline, v: aUsdcv, r: aUsdcr, @@ -1759,6 +1842,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { [expectedDaiAmount], [ { + amount: 0, deadline: 0, v: 0, r: '0x0000000000000000000000000000000000000000000000000000000000000000', @@ -1787,6 +1871,91 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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 = BRE.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, + }, + ] + ) + ) + .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); + }); }); }); @@ -1881,27 +2050,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -1972,16 +2130,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); - // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call - BigNumber.config({ - ROUNDING_MODE: 1, //round down - }); - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmountBN = new BigNumber(liquidityToSwap.toString()).div(1.0009); - const flashloanAmount = flashloanAmountBN.toFixed(0); - const flashloanFee = flashloanAmountBN.multipliedBy(9).div(10000); - const amountToPermit = flashloanAmountBN.plus(flashloanFee); + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; @@ -1995,7 +2145,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, nonce, deadline, - amountToPermit.toFixed(0).toString() + liquidityToSwap.toString() ); const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -2007,18 +2157,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [[dai.address], 0, [expectedDaiAmount], [1], [deadline], [v], [r], [s]] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [liquidityToSwap], + [deadline], + [v], + [r], + [s] ); await expect( @@ -2048,11 +2196,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); - - // Restore round up - BigNumber.config({ - ROUNDING_MODE: 0, //round up - }); }); it('should correctly swap tokens and repay debt for multiple tokens', async () => { @@ -2143,32 +2286,21 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); const aLendBalanceBefore = await aLend.balanceOf(userAddress); - const params = ethers.utils.defaultAbiCoder.encode( + const params = buildRepayAdapterParams( + [dai.address, usdc.address], + 0, + [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [1, 1], + [0, 0], + [0, 0], + [0, 0], [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ], [ - [dai.address, usdc.address], - 0, - [expectedDaiAmountForEth, expectedUsdcAmountForLend], - [1, 1], - [0, 0], - [0, 0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ] ); @@ -2282,20 +2414,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); const aLendBalanceBefore = await aLend.balanceOf(userAddress); - // IMPORTANT: Round down to work equal to solidity to get the correct value for permit call - BigNumber.config({ - ROUNDING_MODE: 1, //round down - }); + const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) + .div(1.0009) + .toFixed(0); - const wethFlashloanAmountBN = new BigNumber(amountWETHtoSwap.toString()).div(1.0009); - const wethFlashloanAmount = wethFlashloanAmountBN.toFixed(0); - const wethFlashloanFee = wethFlashloanAmountBN.multipliedBy(9).div(10000); - const wethAmountToPermit = wethFlashloanAmountBN.plus(wethFlashloanFee).toFixed(0); - - const lendFlashloanAmountBN = new BigNumber(amountLendToSwap.toString()).div(1.0009); - const lendFlashloanAmount = lendFlashloanAmountBN.toFixed(0); - const lendFlashloanFee = lendFlashloanAmountBN.multipliedBy(9).div(10000); - const lendAmountToPermit = lendFlashloanAmountBN.plus(lendFlashloanFee).toFixed(0); + const lendFlashloanAmount = new BigNumber(amountLendToSwap.toString()) + .div(1.0009) + .toFixed(0); const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); const aWethMsgParams = buildPermitParams( @@ -2307,7 +2432,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, aWethNonce, deadline, - wethAmountToPermit.toString() + amountWETHtoSwap.toString() ); const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( ownerPrivateKey, @@ -2324,7 +2449,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter.address, aLendNonce, deadline, - lendAmountToPermit.toString() + amountLendToSwap.toString() ); const {v: aLendv, r: aLendr, s: aLends} = getSignatureFromTypedData( ownerPrivateKey, @@ -2334,27 +2459,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); await mockUniswapRouter.connect(user).setAmountToSwap(lend.address, lendFlashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address, usdc.address], - 0, - [expectedDaiAmountForEth, expectedUsdcAmountForLend], - [1, 1], - [deadline, deadline], - [aWETHv, aLendv], - [aWETHr, aLendr], - [aWETHs, aLends], - ] + const params = buildRepayAdapterParams( + [dai.address, usdc.address], + 0, + [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [1, 1], + [amountWETHtoSwap, amountLendToSwap], + [deadline, deadline], + [aWETHv, aLendv], + [aWETHr, aLendr], + [aWETHs, aLends] ); await pool @@ -2384,11 +2498,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForLend); expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); expect(aLendBalance).to.be.lt(aLendBalanceBefore); - - // Restore round up - BigNumber.config({ - ROUNDING_MODE: 0, //round up - }); }); it('should revert if inconsistent params', async () => { @@ -2415,27 +2524,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params1 = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address, weth.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params1 = buildRepayAdapterParams( + [dai.address, weth.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2452,27 +2550,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params2 = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount, expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params2 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount, expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2489,27 +2576,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params3 = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1, 1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params3 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1, 1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2526,27 +2602,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params4 = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0, 0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params4 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0, 0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2563,27 +2628,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params5 = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0, 0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params5 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0, 0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2600,30 +2654,19 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params6 = ethers.utils.defaultAbiCoder.encode( + const params6 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2640,29 +2683,18 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); - const params7 = ethers.utils.defaultAbiCoder.encode( + const params7 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000', ] ); @@ -2679,6 +2711,32 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0 ) ).to.be.revertedWith('INCONSISTENT_PARAMS'); + + const params8 = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0, 0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] + ); + + await expect( + pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.toString()], + [0], + userAddress, + params8, + 0 + ) + ).to.be.revertedWith('INCONSISTENT_PARAMS'); }); it('should revert if caller not lending pool', async () => { @@ -2705,27 +2763,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2768,27 +2815,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2830,27 +2866,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2892,27 +2917,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -2981,27 +2995,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 0, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( @@ -3089,27 +3092,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethBalanceBefore = await weth.balanceOf(userAddress); - const params = ethers.utils.defaultAbiCoder.encode( - [ - 'address[]', - 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', - ], - [ - [dai.address], - 1, - [expectedDaiAmount], - [1], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ] + const params = buildRepayAdapterParams( + [dai.address], + 1, + [expectedDaiAmount], + [1], + [0], + [0], + [0], + ['0x0000000000000000000000000000000000000000000000000000000000000000'], + ['0x0000000000000000000000000000000000000000000000000000000000000000'] ); await expect( From 6aeabbe00b05149ab8e9b7d62e857d229e29bf10 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 6 Nov 2020 12:12:40 -0300 Subject: [PATCH 15/34] Add build params unit tests helpers --- helpers/contracts-helpers.ts | 54 +++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index cbe2b69c..4c67b209 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'; @@ -212,3 +212,55 @@ 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)[] +) => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256[]', + 'bool[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [assetToSwapToList, minAmountsToReceive, swapAllBalances, permitAmounts, deadlines, v, r, s] + ); +}; + +export const buildRepayAdapterParams = ( + assetToSwapToList: tEthereumAddress[], + leftoverAction: BigNumberish, + repayAmounts: BigNumberish[], + rateModes: BigNumberish[], + permitAmounts: BigNumberish[], + deadlines: BigNumberish[], + v: BigNumberish[], + r: (string | Buffer)[], + s: (string | Buffer)[] +) => { + return ethers.utils.defaultAbiCoder.encode( + [ + 'address[]', + 'uint256', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint256[]', + 'uint8[]', + 'bytes32[]', + 'bytes32[]', + ], + [assetToSwapToList, leftoverAction, repayAmounts, rateModes, permitAmounts, deadlines, v, r, s] + ); +}; From d3ef60f01b3ff649ecfc0993dd1569030a134aa7 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 6 Nov 2020 15:21:27 -0300 Subject: [PATCH 16/34] Merge fix --- helpers/contracts-getters.ts | 6 +- test/uniswapAdapters.spec.ts | 178 ++++++++++++++++++----------------- 2 files changed, 94 insertions(+), 90 deletions(-) diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 54d61b9d..03b705ba 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -278,7 +278,7 @@ export const getSelfdestructTransferMock = async (address?: tEthereumAddress) => export const getMockUniswapRouter = async (address?: tEthereumAddress) => await MockUniswapV2Router02Factory.connect( address || - (await getDb().get(`${eContractid.MockUniswapV2Router02}.${BRE.network.name}`).value()) + (await getDb().get(`${eContractid.MockUniswapV2Router02}.${DRE.network.name}`).value()) .address, await getFirstSigner() ); @@ -286,7 +286,7 @@ export const getMockUniswapRouter = async (address?: tEthereumAddress) => export const getUniswapLiquiditySwapAdapter = async (address?: tEthereumAddress) => await UniswapLiquiditySwapAdapterFactory.connect( address || - (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${BRE.network.name}`).value()) + (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${DRE.network.name}`).value()) .address, await getFirstSigner() ); @@ -294,6 +294,6 @@ export const getUniswapLiquiditySwapAdapter = async (address?: tEthereumAddress) export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => await UniswapRepayAdapterFactory.connect( address || - (await getDb().get(`${eContractid.UniswapRepayAdapter}.${BRE.network.name}`).value()).address, + (await getDb().get(`${eContractid.UniswapRepayAdapter}.${DRE.network.name}`).value()).address, await getFirstSigner() ); diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 2e23f555..96ece84e 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -15,7 +15,7 @@ import { import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; import {Zero} from '@ethersproject/constants'; import BigNumber from 'bignumber.js'; -import {BRE, evmRevert, evmSnapshot} from '../helpers/misc-utils'; +import {DRE, evmRevert, evmSnapshot} from '../helpers/misc-utils'; import {ethers} from 'ethers'; import {eContractid} from '../helpers/types'; import {AToken} from '../types/AToken'; @@ -94,13 +94,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(result['3']).to.be.eq(daiUsdValue); }); it('should work correctly with different decimals', async () => { - const {lend, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const {aave, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; const amountIn = parseEther('10'); const flashloanPremium = amountIn.mul(9).div(10000); const amountToSwap = amountIn.sub(flashloanPremium); - const lendPrice = await oracle.getAssetPrice(lend.address); + const aavePrice = await oracle.getAssetPrice(aave.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); @@ -114,8 +114,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .mul('1000000') // usdc 6 decimals .div(expectedUSDCAmount.mul(parseEther('1'))); - const lendUsdValue = amountIn - .mul(lendPrice) + const aaveUsdValue = amountIn + .mul(aavePrice) .div(parseEther('1')) .mul(usdPrice) .div(parseEther('1')); @@ -128,20 +128,20 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.setAmountOut( amountToSwap, - lend.address, + aave.address, usdc.address, expectedUSDCAmount ); const result = await uniswapLiquiditySwapAdapter.getAmountsOut( amountIn, - lend.address, + aave.address, usdc.address ); expect(result['0']).to.be.eq(expectedUSDCAmount); expect(result['1']).to.be.eq(outPerInPrice); - expect(result['2']).to.be.eq(lendUsdValue); + expect(result['2']).to.be.eq(aaveUsdValue); expect(result['3']).to.be.eq(usdcUsdValue); }); }); @@ -193,13 +193,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(result['3']).to.be.eq(daiUsdValue); }); it('should work correctly with different decimals', async () => { - const {lend, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const {aave, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; const amountIn = parseEther('10'); const flashloanPremium = amountIn.mul(9).div(10000); const amountToSwap = amountIn.sub(flashloanPremium); - const lendPrice = await oracle.getAssetPrice(lend.address); + const aavePrice = await oracle.getAssetPrice(aave.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); const usdPrice = await oracle.getAssetPrice(USD_ADDRESS); @@ -213,8 +213,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .mul(parseEther('1')) .div(amountToSwap.mul('1000000')); // usdc 6 decimals - const lendUsdValue = amountToSwap - .mul(lendPrice) + const aaveUsdValue = amountToSwap + .mul(aavePrice) .div(parseEther('1')) .mul(usdPrice) .div(parseEther('1')); @@ -225,17 +225,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .mul(usdPrice) .div(parseEther('1')); - await mockUniswapRouter.setAmountIn(amountOut, lend.address, usdc.address, amountIn); + await mockUniswapRouter.setAmountIn(amountOut, aave.address, usdc.address, amountIn); const result = await uniswapLiquiditySwapAdapter.getAmountsIn( amountOut, - lend.address, + aave.address, usdc.address ); expect(result['0']).to.be.eq(amountToSwap); expect(result['1']).to.be.eq(inPerOutPrice); - expect(result['2']).to.be.eq(lendUsdValue); + expect(result['2']).to.be.eq(aaveUsdValue); expect(result['3']).to.be.eq(usdcUsdValue); }); }); @@ -474,7 +474,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { } = testEnv; const user = users[0].signer; const userAddress = users[0].address; - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -628,7 +628,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await aWETH._nonces(userAddress)).toNumber(); const msgParams = buildPermitParams( @@ -1209,7 +1209,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = parseEther('10'); expect(userAEthBalanceBefore).to.be.eq(liquidityToSwap); - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await aWETH._nonces(userAddress)).toNumber(); const msgParams = buildPermitParams( @@ -1373,7 +1373,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = parseEther('10'); const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await aWETH._nonces(userAddress)).toNumber(); const msgParams = buildPermitParams( @@ -1680,7 +1680,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { } = testEnv; const user = users[0].signer; const userAddress = users[0].address; - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -1898,7 +1898,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Only has 10 atokens, so all the balance will be swapped const bigAmountToSwap = parseEther('100'); - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -1974,7 +1974,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('executeOperation', () => { beforeEach(async () => { - const {users, weth, dai, usdc, lend, pool, deployer} = testEnv; + const {users, weth, dai, usdc, aave, pool, deployer} = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -1982,7 +1982,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await dai.approve(pool.address, parseEther('20000')); await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); - const usdcLiquidity = await convertToCurrencyDecimals(usdc.address, '20000'); + 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); @@ -1991,18 +1991,22 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await weth.approve(pool.address, parseEther('100')); await pool.deposit(weth.address, parseEther('100'), deployer.address, 0); - await lend.mint(parseEther('1000000')); - await lend.approve(pool.address, parseEther('1000000')); - await pool.deposit(lend.address, parseEther('1000000'), 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('100')); - await weth.approve(pool.address, parseEther('100')); - await pool.deposit(weth.address, parseEther('100'), userAddress, 0); + await weth.mint(parseEther('1000')); + await weth.approve(pool.address, parseEther('1000')); + await pool.deposit(weth.address, parseEther('1000'), userAddress, 0); - await lend.mint(parseEther('1000000')); - await lend.approve(pool.address, parseEther('1000000')); - await pool.deposit(lend.address, parseEther('1000000'), 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); }); it('should correctly swap tokens and repay debt', async () => { @@ -2133,7 +2137,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // 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 = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const nonce = (await aWETH._nonces(userAddress)).toNumber(); const msgParams = buildPermitParams( @@ -2206,7 +2210,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { oracle, dai, uniswapRepayAdapter, - lend, + aave, usdc, helpersContract, aWETH, @@ -2215,7 +2219,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userAddress = users[0].address; const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); - const amountLendToSwap = await convertToCurrencyDecimals(lend.address, '1'); + const amountAaveToSwap = parseEther('1'); const daiPrice = await oracle.getAssetPrice(dai.address); const expectedDaiAmountForEth = await convertToCurrencyDecimals( @@ -2223,17 +2227,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - const lendPrice = await oracle.getAssetPrice(lend.address); + const aavePrice = await oracle.getAssetPrice(aave.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); - const collateralDecimals = (await lend.decimals()).toString(); + const collateralDecimals = (await aave.decimals()).toString(); const principalDecimals = (await usdc.decimals()).toString(); - const expectedUsdcAmountForLend = await convertToCurrencyDecimals( + const expectedUsdcAmountForAave = await convertToCurrencyDecimals( usdc.address, - new BigNumber(amountLendToSwap.toString()) + new BigNumber(amountAaveToSwap.toString()) .times( - new BigNumber(lendPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + new BigNumber(aavePrice.toString()).times(new BigNumber(10).pow(principalDecimals)) ) .div( new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) @@ -2243,7 +2247,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); - await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForLend, 1, 0, userAddress); + await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForAave, 1, 0, userAddress); const daiStableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) @@ -2270,26 +2274,26 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) .div(1.0009) .toFixed(0); - const lendFlashloanAmount = new BigNumber(amountLendToSwap.toString()) + const aaveFlashloanAmount = new BigNumber(amountAaveToSwap.toString()) .div(1.0009) .toFixed(0); await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); - await mockUniswapRouter.connect(user).setAmountToSwap(lend.address, lendFlashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(aave.address, aaveFlashloanAmount); await aWETH.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); - const lendData = await pool.getReserveData(lend.address); - const aLend = await getContract(eContractid.AToken, lendData.aTokenAddress); - await aLend.connect(user).approve(uniswapRepayAdapter.address, amountLendToSwap); + const aaveData = await pool.getReserveData(aave.address); + const aAave = await getContract(eContractid.AToken, aaveData.aTokenAddress); + await aAave.connect(user).approve(uniswapRepayAdapter.address, amountAaveToSwap); const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); - const aLendBalanceBefore = await aLend.balanceOf(userAddress); + const aAaveBalanceBefore = await aAave.balanceOf(userAddress); const params = buildRepayAdapterParams( [dai.address, usdc.address], 0, - [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [expectedDaiAmountForEth, expectedUsdcAmountForAave], [1, 1], [0, 0], [0, 0], @@ -2308,8 +2312,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address, lend.address], - [wethFlashloanAmount.toString(), lendFlashloanAmount.toString()], + [weth.address, aave.address], + [wethFlashloanAmount.toString(), aaveFlashloanAmount.toString()], [0, 0], userAddress, params, @@ -2321,16 +2325,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); const userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); const aWETHBalance = await aWETH.balanceOf(userAddress); - const aLendBalance = await aLend.balanceOf(userAddress); + const aAaveBalance = await aAave.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); - expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForLend); - expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForLend); + expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForAave); + expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForAave); expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); - expect(aLendBalance).to.be.lt(aLendBalanceBefore); + expect(aAaveBalance).to.be.lt(aAaveBalanceBefore); }); it('should swap tokens and repay debt for multiple tokens using permit', async () => { @@ -2341,14 +2345,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { oracle, dai, uniswapRepayAdapter, - lend, + aave, usdc, helpersContract, aWETH, } = testEnv; const user = users[0].signer; const userAddress = users[0].address; - const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const chainId = DRE.network.config.chainId || BUIDLEREVM_CHAINID; const deadline = MAX_UINT_AMOUNT; const ownerPrivateKey = require('../test-wallets.js').accounts[1].secretKey; @@ -2357,7 +2361,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { } const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); - const amountLendToSwap = await convertToCurrencyDecimals(lend.address, '1'); + const amountAaveToSwap = await convertToCurrencyDecimals(aave.address, '1'); const daiPrice = await oracle.getAssetPrice(dai.address); const expectedDaiAmountForEth = await convertToCurrencyDecimals( @@ -2365,17 +2369,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) ); - const lendPrice = await oracle.getAssetPrice(lend.address); + const aavePrice = await oracle.getAssetPrice(aave.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); - const collateralDecimals = (await lend.decimals()).toString(); + const collateralDecimals = (await aave.decimals()).toString(); const principalDecimals = (await usdc.decimals()).toString(); - const expectedUsdcAmountForLend = await convertToCurrencyDecimals( + const expectedUsdcAmountForAave = await convertToCurrencyDecimals( usdc.address, - new BigNumber(amountLendToSwap.toString()) + new BigNumber(amountAaveToSwap.toString()) .times( - new BigNumber(lendPrice.toString()).times(new BigNumber(10).pow(principalDecimals)) + new BigNumber(aavePrice.toString()).times(new BigNumber(10).pow(principalDecimals)) ) .div( new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) @@ -2385,7 +2389,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); - await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForLend, 1, 0, userAddress); + await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForAave, 1, 0, userAddress); const daiStableDebtTokenAddress = ( await helpersContract.getReserveTokensAddresses(dai.address) @@ -2408,17 +2412,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); const userUsdcStableDebtAmountBefore = await usdcStableDebtContract.balanceOf(userAddress); - const lendData = await pool.getReserveData(lend.address); - const aLend = await getContract(eContractid.AToken, lendData.aTokenAddress); + const aaveData = await pool.getReserveData(aave.address); + const aAave = await getContract(eContractid.AToken, aaveData.aTokenAddress); const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); - const aLendBalanceBefore = await aLend.balanceOf(userAddress); + const aAaveBalanceBefore = await aAave.balanceOf(userAddress); const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) .div(1.0009) .toFixed(0); - const lendFlashloanAmount = new BigNumber(amountLendToSwap.toString()) + const aaveFlashloanAmount = new BigNumber(amountAaveToSwap.toString()) .div(1.0009) .toFixed(0); @@ -2439,44 +2443,44 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { aWethMsgParams ); - const aLendNonce = (await aLend._nonces(userAddress)).toNumber(); - const aLendMsgParams = buildPermitParams( + const aAaveNonce = (await aAave._nonces(userAddress)).toNumber(); + const aAaveMsgParams = buildPermitParams( chainId, - aLend.address, + aAave.address, '1', - await aLend.name(), + await aAave.name(), userAddress, uniswapRepayAdapter.address, - aLendNonce, + aAaveNonce, deadline, - amountLendToSwap.toString() + amountAaveToSwap.toString() ); - const {v: aLendv, r: aLendr, s: aLends} = getSignatureFromTypedData( + const {v: aAavev, r: aAaver, s: aAaves} = getSignatureFromTypedData( ownerPrivateKey, - aLendMsgParams + aAaveMsgParams ); await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); - await mockUniswapRouter.connect(user).setAmountToSwap(lend.address, lendFlashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(aave.address, aaveFlashloanAmount); const params = buildRepayAdapterParams( [dai.address, usdc.address], 0, - [expectedDaiAmountForEth, expectedUsdcAmountForLend], + [expectedDaiAmountForEth, expectedUsdcAmountForAave], [1, 1], - [amountWETHtoSwap, amountLendToSwap], + [amountWETHtoSwap, amountAaveToSwap], [deadline, deadline], - [aWETHv, aLendv], - [aWETHr, aLendr], - [aWETHs, aLends] + [aWETHv, aAavev], + [aWETHr, aAaver], + [aWETHs, aAaves] ); await pool .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address, lend.address], - [wethFlashloanAmount.toString(), lendFlashloanAmount.toString()], + [weth.address, aave.address], + [wethFlashloanAmount.toString(), aaveFlashloanAmount.toString()], [0, 0], userAddress, params, @@ -2488,16 +2492,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmount = await daiStableDebtContract.balanceOf(userAddress); const userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); const aWETHBalance = await aWETH.balanceOf(userAddress); - const aLendBalance = await aLend.balanceOf(userAddress); + const aAaveBalance = await aAave.balanceOf(userAddress); expect(adapterWethBalance).to.be.eq(Zero); expect(adapterDaiBalance).to.be.eq(Zero); expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); - expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForLend); - expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForLend); + expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForAave); + expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForAave); expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); - expect(aLendBalance).to.be.lt(aLendBalanceBefore); + expect(aAaveBalance).to.be.lt(aAaveBalanceBefore); }); it('should revert if inconsistent params', async () => { From 4fb43f7aff489aa170ca380a894157467a69aff2 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Mon, 9 Nov 2020 15:27:18 -0300 Subject: [PATCH 17/34] Remove not used batch capabilities on repay adapter --- contracts/adapters/UniswapRepayAdapter.sol | 99 ++- helpers/contracts-helpers.ts | 34 +- test/uniswapAdapters.spec.ts | 671 ++------------------- 3 files changed, 122 insertions(+), 682 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 46d9c5b0..dba64afd 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -16,11 +16,11 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { struct RepayParams { - address[] assetToSwapToList; + address assetToSwapTo; LeftoverAction leftOverAction; - uint256[] repayAmounts; - uint256[] rateModes; - PermitParams permitParams; + uint256 repayAmount; + uint256 rateMode; + PermitSignature permitSignature; } constructor( @@ -41,17 +41,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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 repay + * address Address of the reserve to be swapped to and repay * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: * (0) Deposit back * (1) Direct transfer to user - * uint256[] repayAmounts List of amounts of debt to be repaid - * uint256[] rateModes List of the rate modes of the debt to be repaid - * 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 + * uint256 repayAmount Amount of debt to be repaid + * 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, @@ -64,36 +64,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { RepayParams memory decodedParams = _decodeParams(params); - require( - assets.length == decodedParams.assetToSwapToList.length - && assets.length == decodedParams.repayAmounts.length - && assets.length == decodedParams.rateModes.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, - 'INCONSISTENT_PARAMS'); - - for (uint256 i = 0; i < assets.length; i++) { _swapAndRepay( - assets[i], - decodedParams.assetToSwapToList[i], - amounts[i], - decodedParams.repayAmounts[i], - decodedParams.rateModes[i], + assets[0], + decodedParams.assetToSwapTo, + amounts[0], + decodedParams.repayAmount, + decodedParams.rateMode, initiator, decodedParams.leftOverAction, - premiums[i], - PermitSignature( - decodedParams.permitParams.amount[i], - decodedParams.permitParams.deadline[i], - decodedParams.permitParams.v[i], - decodedParams.permitParams.r[i], - decodedParams.permitParams.s[i] - ) + premiums[0], + decodedParams.permitSignature ); - } return true; } @@ -138,38 +119,38 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { /** * @dev Decodes debt information encoded in flashloan 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 repay + * address Address of the reserve to be swapped to and repay * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: * (0) Deposit back * (1) Direct transfer to user - * uint256[] repayAmounts List of amounts of debt to be repaid - * uint256[] rateModes List of the rate modes of the debt to be repaid - * 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 + * uint256 repayAmount Amount of debt to be repaid + * 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[] memory assetToSwapToList, + address assetToSwapTo, LeftoverAction leftOverAction, - uint256[] memory repayAmounts, - uint256[] memory rateModes, - uint256[] memory permitAmount, - uint256[] memory deadline, - uint8[] memory v, - bytes32[] memory r, - bytes32[] memory s - ) = abi.decode(params, (address[], LeftoverAction, uint256[], uint256[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); + uint256 repayAmount, + uint256 rateMode, + uint256 permitAmount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) = abi.decode(params, (address, LeftoverAction, uint256, uint256, uint256, uint256, uint8, bytes32, bytes32)); return RepayParams( - assetToSwapToList, + assetToSwapTo, leftOverAction, - repayAmounts, - rateModes, - PermitParams( + repayAmount, + rateMode, + PermitSignature( permitAmount, deadline, v, diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index d3d226af..e2ccf3c8 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -240,28 +240,28 @@ export const buildLiquiditySwapParams = ( }; export const buildRepayAdapterParams = ( - assetToSwapToList: tEthereumAddress[], + assetToSwapTo: tEthereumAddress, leftoverAction: BigNumberish, - repayAmounts: BigNumberish[], - rateModes: BigNumberish[], - permitAmounts: BigNumberish[], - deadlines: BigNumberish[], - v: BigNumberish[], - r: (string | Buffer)[], - s: (string | Buffer)[] + repayAmount: BigNumberish, + rateMode: BigNumberish, + permitAmount: BigNumberish, + deadline: BigNumberish, + v: BigNumberish, + r: string | Buffer, + s: string | Buffer ) => { return ethers.utils.defaultAbiCoder.encode( [ - 'address[]', + 'address', 'uint256', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint256[]', - 'uint8[]', - 'bytes32[]', - 'bytes32[]', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint8', + 'bytes32', + 'bytes32', ], - [assetToSwapToList, leftoverAction, repayAmounts, rateModes, permitAmounts, deadlines, v, r, s] + [assetToSwapTo, leftoverAction, repayAmount, rateMode, permitAmount, deadline, v, r, s] ); }; diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 96ece84e..a3ea4dbe 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1959,7 +1959,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe('UniswapRepayAdapter', () => { + describe.only('UniswapRepayAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; @@ -2055,15 +2055,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -2162,15 +2162,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [liquidityToSwap], - [deadline], - [v], - [r], - [s] + expectedDaiAmount, + 1, + liquidityToSwap, + deadline, + v, + r, + s ); await expect( @@ -2202,547 +2202,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); - it('should correctly swap tokens and repay debt for multiple tokens', async () => { - const { - users, - pool, - weth, - oracle, - dai, - uniswapRepayAdapter, - aave, - usdc, - helpersContract, - aWETH, - } = testEnv; - const user = users[0].signer; - const userAddress = users[0].address; - - const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); - const amountAaveToSwap = parseEther('1'); - - const daiPrice = await oracle.getAssetPrice(dai.address); - const expectedDaiAmountForEth = await convertToCurrencyDecimals( - dai.address, - new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) - ); - - const aavePrice = await oracle.getAssetPrice(aave.address); - const usdcPrice = await oracle.getAssetPrice(usdc.address); - - const collateralDecimals = (await aave.decimals()).toString(); - const principalDecimals = (await usdc.decimals()).toString(); - - const expectedUsdcAmountForAave = await convertToCurrencyDecimals( - usdc.address, - new BigNumber(amountAaveToSwap.toString()) - .times( - new BigNumber(aavePrice.toString()).times(new BigNumber(10).pow(principalDecimals)) - ) - .div( - new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) - ) - .toFixed(0) - ); - - // Open user Debt - await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); - await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForAave, 1, 0, userAddress); - - const daiStableDebtTokenAddress = ( - await helpersContract.getReserveTokensAddresses(dai.address) - ).stableDebtTokenAddress; - - const daiStableDebtContract = await getContract( - eContractid.StableDebtToken, - daiStableDebtTokenAddress - ); - - const usdcStableDebtTokenAddress = ( - await helpersContract.getReserveTokensAddresses(usdc.address) - ).stableDebtTokenAddress; - - const usdcStableDebtContract = await getContract( - eContractid.StableDebtToken, - usdcStableDebtTokenAddress - ); - - const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); - const userUsdcStableDebtAmountBefore = await usdcStableDebtContract.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 aaveFlashloanAmount = new BigNumber(amountAaveToSwap.toString()) - .div(1.0009) - .toFixed(0); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); - await mockUniswapRouter.connect(user).setAmountToSwap(aave.address, aaveFlashloanAmount); - - await aWETH.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); - - const aaveData = await pool.getReserveData(aave.address); - const aAave = await getContract(eContractid.AToken, aaveData.aTokenAddress); - await aAave.connect(user).approve(uniswapRepayAdapter.address, amountAaveToSwap); - - const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); - const aAaveBalanceBefore = await aAave.balanceOf(userAddress); - - const params = buildRepayAdapterParams( - [dai.address, usdc.address], - 0, - [expectedDaiAmountForEth, expectedUsdcAmountForAave], - [1, 1], - [0, 0], - [0, 0], - [0, 0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ] - ); - - await pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address, aave.address], - [wethFlashloanAmount.toString(), aaveFlashloanAmount.toString()], - [0, 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 userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); - const aWETHBalance = await aWETH.balanceOf(userAddress); - const aAaveBalance = await aAave.balanceOf(userAddress); - - expect(adapterWethBalance).to.be.eq(Zero); - expect(adapterDaiBalance).to.be.eq(Zero); - expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); - expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); - expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForAave); - expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForAave); - expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); - expect(aAaveBalance).to.be.lt(aAaveBalanceBefore); - }); - - it('should swap tokens and repay debt for multiple tokens using permit', async () => { - const { - users, - pool, - weth, - oracle, - dai, - uniswapRepayAdapter, - aave, - usdc, - helpersContract, - aWETH, - } = 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 amountAaveToSwap = await convertToCurrencyDecimals(aave.address, '1'); - - const daiPrice = await oracle.getAssetPrice(dai.address); - const expectedDaiAmountForEth = await convertToCurrencyDecimals( - dai.address, - new BigNumber(amountWETHtoSwap.toString()).div(daiPrice.toString()).toFixed(0) - ); - - const aavePrice = await oracle.getAssetPrice(aave.address); - const usdcPrice = await oracle.getAssetPrice(usdc.address); - - const collateralDecimals = (await aave.decimals()).toString(); - const principalDecimals = (await usdc.decimals()).toString(); - - const expectedUsdcAmountForAave = await convertToCurrencyDecimals( - usdc.address, - new BigNumber(amountAaveToSwap.toString()) - .times( - new BigNumber(aavePrice.toString()).times(new BigNumber(10).pow(principalDecimals)) - ) - .div( - new BigNumber(usdcPrice.toString()).times(new BigNumber(10).pow(collateralDecimals)) - ) - .toFixed(0) - ); - - // Open user Debt - await pool.connect(user).borrow(dai.address, expectedDaiAmountForEth, 1, 0, userAddress); - await pool.connect(user).borrow(usdc.address, expectedUsdcAmountForAave, 1, 0, userAddress); - - const daiStableDebtTokenAddress = ( - await helpersContract.getReserveTokensAddresses(dai.address) - ).stableDebtTokenAddress; - - const daiStableDebtContract = await getContract( - eContractid.StableDebtToken, - daiStableDebtTokenAddress - ); - - const usdcStableDebtTokenAddress = ( - await helpersContract.getReserveTokensAddresses(usdc.address) - ).stableDebtTokenAddress; - - const usdcStableDebtContract = await getContract( - eContractid.StableDebtToken, - usdcStableDebtTokenAddress - ); - - const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); - const userUsdcStableDebtAmountBefore = await usdcStableDebtContract.balanceOf(userAddress); - - const aaveData = await pool.getReserveData(aave.address); - const aAave = await getContract(eContractid.AToken, aaveData.aTokenAddress); - - const aWETHBalanceBefore = await aWETH.balanceOf(userAddress); - const aAaveBalanceBefore = await aAave.balanceOf(userAddress); - - const wethFlashloanAmount = new BigNumber(amountWETHtoSwap.toString()) - .div(1.0009) - .toFixed(0); - - const aaveFlashloanAmount = new BigNumber(amountAaveToSwap.toString()) - .div(1.0009) - .toFixed(0); - - const aWethNonce = (await aWETH._nonces(userAddress)).toNumber(); - const aWethMsgParams = buildPermitParams( - chainId, - aWETH.address, - '1', - await aWETH.name(), - userAddress, - uniswapRepayAdapter.address, - aWethNonce, - deadline, - amountWETHtoSwap.toString() - ); - const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( - ownerPrivateKey, - aWethMsgParams - ); - - const aAaveNonce = (await aAave._nonces(userAddress)).toNumber(); - const aAaveMsgParams = buildPermitParams( - chainId, - aAave.address, - '1', - await aAave.name(), - userAddress, - uniswapRepayAdapter.address, - aAaveNonce, - deadline, - amountAaveToSwap.toString() - ); - const {v: aAavev, r: aAaver, s: aAaves} = getSignatureFromTypedData( - ownerPrivateKey, - aAaveMsgParams - ); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, wethFlashloanAmount); - await mockUniswapRouter.connect(user).setAmountToSwap(aave.address, aaveFlashloanAmount); - - const params = buildRepayAdapterParams( - [dai.address, usdc.address], - 0, - [expectedDaiAmountForEth, expectedUsdcAmountForAave], - [1, 1], - [amountWETHtoSwap, amountAaveToSwap], - [deadline, deadline], - [aWETHv, aAavev], - [aWETHr, aAaver], - [aWETHs, aAaves] - ); - - await pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address, aave.address], - [wethFlashloanAmount.toString(), aaveFlashloanAmount.toString()], - [0, 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 userUsdcStableDebtAmount = await usdcStableDebtContract.balanceOf(userAddress); - const aWETHBalance = await aWETH.balanceOf(userAddress); - const aAaveBalance = await aAave.balanceOf(userAddress); - - expect(adapterWethBalance).to.be.eq(Zero); - expect(adapterDaiBalance).to.be.eq(Zero); - expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmountForEth); - expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmountForEth); - expect(userUsdcStableDebtAmountBefore).to.be.gte(expectedUsdcAmountForAave); - expect(userUsdcStableDebtAmount).to.be.lt(expectedUsdcAmountForAave); - expect(aWETHBalance).to.be.lt(aWETHBalanceBefore); - expect(aAaveBalance).to.be.lt(aAaveBalanceBefore); - }); - - it('should revert if inconsistent params', 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); - - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); - - const params1 = buildRepayAdapterParams( - [dai.address, weth.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params1, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params2 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount, expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params2, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params3 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1, 1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params3, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params4 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0, 0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params4, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params5 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0, 0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params5, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params6 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params6, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params7 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - [ - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000', - ] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params7, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - - const params8 = buildRepayAdapterParams( - [dai.address], - 0, - [expectedDaiAmount], - [1], - [0, 0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params8, - 0 - ) - ).to.be.revertedWith('INCONSISTENT_PARAMS'); - }); - it('should revert if caller not lending pool', async () => { const {users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter} = testEnv; const user = users[0].signer; @@ -2768,15 +2227,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -2820,15 +2279,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -2871,15 +2330,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -2922,15 +2381,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -3000,15 +2459,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 0, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( @@ -3097,15 +2556,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const wethBalanceBefore = await weth.balanceOf(userAddress); const params = buildRepayAdapterParams( - [dai.address], + dai.address, 1, - [expectedDaiAmount], - [1], - [0], - [0], - [0], - ['0x0000000000000000000000000000000000000000000000000000000000000000'], - ['0x0000000000000000000000000000000000000000000000000000000000000000'] + expectedDaiAmount, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' ); await expect( From 6e9defe14c111a02abc5056d9c2f1c8fddb6b412 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Mon, 9 Nov 2020 16:53:03 -0300 Subject: [PATCH 18/34] Add repayAllDebt flag to repay the whole debt with collateral --- contracts/adapters/BaseUniswapAdapter.sol | 5 +- .../adapters/UniswapLiquiditySwapAdapter.sol | 4 +- contracts/adapters/UniswapRepayAdapter.sol | 48 ++--- helpers/contracts-helpers.ts | 15 +- test/uniswapAdapters.spec.ts | 186 +++++++++++++++++- 5 files changed, 229 insertions(+), 29 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index c69fa9a0..cb7d32cb 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -224,9 +224,8 @@ contract BaseUniswapAdapter { * @dev Get the aToken associated to the asset * @return address of the aToken */ - function _getAToken(address asset) internal view returns (address) { - ReserveLogic.ReserveData memory reserve = POOL.getReserveData(asset); - return reserve.aTokenAddress; + function _getReserveData(address asset) internal view returns (ReserveLogic.ReserveData memory) { + return POOL.getReserveData(asset); } /** diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 83b334d1..a4150ef1 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -126,7 +126,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ); for (uint256 i = 0; i < assetToSwapFromList.length; i++) { - address aToken = _getAToken(assetToSwapFromList[i]); + address aToken = _getReserveData(assetToSwapFromList[i]).aTokenAddress; uint256 aTokenInitiatorBalance = IERC20(aToken).balanceOf(msg.sender); uint256 amountToSwap = amountToSwapList[i] > aTokenInitiatorBalance ? aTokenInitiatorBalance : amountToSwapList[i]; @@ -172,7 +172,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { bool swapAllBalance, PermitSignature memory permitSignature ) internal { - address aToken = _getAToken(assetFrom); + address aToken = _getReserveData(assetFrom).aTokenAddress; uint256 aTokenInitiatorBalance = IERC20(aToken).balanceOf(initiator); uint256 amountToSwap = swapAllBalance ? aTokenInitiatorBalance.sub(premium) : amount; diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index dba64afd..f616765b 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -7,6 +7,7 @@ import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddresses import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; +import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; /** * @title UniswapRepayAdapter @@ -20,6 +21,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { LeftoverAction leftOverAction; uint256 repayAmount; uint256 rateMode; + bool repayAllDebt; PermitSignature permitSignature; } @@ -47,6 +49,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid + * bool repayAllDebt Flag indicating if all the debt should be repaid * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -72,6 +75,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.rateMode, initiator, decodedParams.leftOverAction, + decodedParams.repayAllDebt, premiums[0], decodedParams.permitSignature ); @@ -100,9 +104,21 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 rateMode, address initiator, LeftoverAction leftOverAction, + bool repayAllDebt, uint256 premium, PermitSignature memory permitSignature ) internal { + + if (repayAllDebt) { + ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); + + address debtToken = ReserveLogic.InterestRateMode(rateMode) == ReserveLogic.InterestRateMode.STABLE + ? reserveDebtData.stableDebtTokenAddress + : reserveDebtData.variableDebtTokenAddress; + + repayAmount = IERC20(debtToken).balanceOf(initiator); + } + _swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); // Repay debt @@ -110,7 +126,12 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.repay(assetTo, repayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); - _pullATokenAndRepayFlashLoan(assetFrom, initiator, flashLoanDebt, permitSignature); + + ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); + _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, flashLoanDebt, permitSignature); + + // Repay flashloan + IERC20(assetFrom).approve(address(POOL), flashLoanDebt); // Take care of reserve leftover from the swap _sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); @@ -125,6 +146,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * (1) Direct transfer to user * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid + * bool repayAllDebt Flag indicating if all the debt should be repaid * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -138,18 +160,20 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { LeftoverAction leftOverAction, uint256 repayAmount, uint256 rateMode, + bool repayAllDebt, uint256 permitAmount, uint256 deadline, uint8 v, bytes32 r, bytes32 s - ) = abi.decode(params, (address, LeftoverAction, uint256, uint256, uint256, uint256, uint8, bytes32, bytes32)); + ) = abi.decode(params, (address, LeftoverAction, uint256, uint256, bool, uint256, uint256, uint8, bytes32, bytes32)); return RepayParams( assetToSwapTo, leftOverAction, repayAmount, rateMode, + repayAllDebt, PermitSignature( permitAmount, deadline, @@ -159,24 +183,4 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) ); } - - /** - * @dev Pull the ATokens from the user and use them to repay the flashloan - * @param reserve address of the asset - * @param user address - * @param flashLoanDebt need to be repaid - * @param permitSignature struct containing the permit signature - */ - function _pullATokenAndRepayFlashLoan( - address reserve, - address user, - uint256 flashLoanDebt, - PermitSignature memory permitSignature - ) internal { - address reserveAToken = _getAToken(reserve); - _pullAToken(reserve, reserveAToken, user, flashLoanDebt, permitSignature); - - // Repay flashloan - IERC20(reserve).approve(address(POOL), flashLoanDebt); - } } diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index e2ccf3c8..6eba519e 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -244,6 +244,7 @@ export const buildRepayAdapterParams = ( leftoverAction: BigNumberish, repayAmount: BigNumberish, rateMode: BigNumberish, + repayAllDebt: BigNumberish, permitAmount: BigNumberish, deadline: BigNumberish, v: BigNumberish, @@ -256,12 +257,24 @@ export const buildRepayAdapterParams = ( 'uint256', 'uint256', 'uint256', + 'bool', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32', ], - [assetToSwapTo, leftoverAction, repayAmount, rateMode, permitAmount, deadline, v, r, s] + [ + assetToSwapTo, + leftoverAction, + repayAmount, + rateMode, + repayAllDebt, + permitAmount, + deadline, + v, + r, + s, + ] ); }; diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index a3ea4dbe..fcb9d81d 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1959,7 +1959,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe.only('UniswapRepayAdapter', () => { + describe('UniswapRepayAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; @@ -2062,6 +2062,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2166,6 +2167,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, expectedDaiAmount, 1, + 0, liquidityToSwap, deadline, v, @@ -2234,6 +2236,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2286,6 +2289,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2337,6 +2341,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2388,6 +2393,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2466,6 +2472,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2563,6 +2570,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 0, 0, 0, + 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2597,6 +2605,182 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(liquidityToSwap)); expect(wethBalance).to.be.eq(wethBalanceBefore.add(leftOverWeth.toString())); }); + + 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); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.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); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + + // Passed amount to repay is smaller than debt, + // but repayAllDebt flag is enabled so the whole debt should be paid + const amountToRepay = expectedDaiAmount.div(2); + + const params = buildRepayAdapterParams( + dai.address, + 0, + amountToRepay, + 1, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.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); + + 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 + ); + + const liquidityToSwap = amountWETHtoSwap; + await aWETH.connect(user).approve(uniswapRepayAdapter.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); + + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + + // Passed amount to repay is smaller than debt, + // but repayAllDebt flag is enabled so the whole debt should be paid + const amountToRepay = expectedDaiAmount.div(2); + + const params = buildRepayAdapterParams( + dai.address, + 0, + amountToRepay, + 2, + 1, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.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); + + 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)); + }); }); }); }); From cf9c8855c33e72ceba2442c22dcf9130061a4616 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 10 Nov 2020 10:38:24 -0300 Subject: [PATCH 19/34] Pull the correct amount of atokens from user to avoid leftovers in repay adapter --- contracts/adapters/BaseUniswapAdapter.sol | 25 ----- contracts/adapters/UniswapRepayAdapter.sol | 25 +---- helpers/contracts-helpers.ts | 27 +---- test/uniswapAdapters.spec.ts | 114 ++------------------- 4 files changed, 14 insertions(+), 177 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index cb7d32cb..dab04acd 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -24,8 +24,6 @@ contract BaseUniswapAdapter { using PercentageMath for uint256; using SafeERC20 for IERC20; - enum LeftoverAction {DEPOSIT, TRANSFER} - struct PermitParams { uint256[] amount; uint256[] deadline; @@ -228,29 +226,6 @@ contract BaseUniswapAdapter { return POOL.getReserveData(asset); } - /** - * @dev Take action with the swap left overs as configured in the parameters - * @param asset address of the asset - * @param reservedAmount Amount reserved to be used by the contract to repay the flash loan - * @param leftOverAction enum indicating what to do with the left over balance from the swap: - * (0) Deposit back - * (1) Direct transfer to user - * @param user address - */ - function _sendLeftovers(address asset, uint256 reservedAmount, LeftoverAction leftOverAction, address user) internal { - uint256 balance = IERC20(asset).balanceOf(address(this)); - uint256 assetLeftOver = balance.sub(reservedAmount); - - if (assetLeftOver > 0) { - if (leftOverAction == LeftoverAction.DEPOSIT) { - IERC20(asset).approve(address(POOL), balance); - POOL.deposit(asset, assetLeftOver, user, 0); - } else { - IERC20(asset).transfer(user, assetLeftOver); - } - } - } - /** * @dev Pull the ATokens from the user * @param reserve address of the asset diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index f616765b..fb821db3 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -18,7 +18,6 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { struct RepayParams { address assetToSwapTo; - LeftoverAction leftOverAction; uint256 repayAmount; uint256 rateMode; bool repayAllDebt; @@ -44,9 +43,6 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param initiator Address of the user * @param params Additional variadic field to include extra params. Expected parameters: * address Address of the reserve to be swapped to and repay - * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: - * (0) Deposit back - * (1) Direct transfer to user * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid * bool repayAllDebt Flag indicating if all the debt should be repaid @@ -74,7 +70,6 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.repayAmount, decodedParams.rateMode, initiator, - decodedParams.leftOverAction, decodedParams.repayAllDebt, premiums[0], decodedParams.permitSignature @@ -84,7 +79,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { } /** - * @dev Perform the swap, the repay of the debt and send back the left overs + * @dev Perform the swap and the repay of the debt * * @param assetFrom Address of token to be swapped * @param assetTo Address of token to be received @@ -92,7 +87,6 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param repayAmount Amount of the debt to be repaid * @param rateMode Rate mode of the debt to be repaid * @param initiator Address of the user - * @param leftOverAction enum indicating what to do with the left over balance from the swap * @param premium Fee of the flash loan * @param permitSignature struct containing the permit signature */ @@ -103,12 +97,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 repayAmount, uint256 rateMode, address initiator, - LeftoverAction leftOverAction, bool repayAllDebt, uint256 premium, PermitSignature memory permitSignature ) internal { - if (repayAllDebt) { ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); @@ -119,31 +111,26 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { repayAmount = IERC20(debtToken).balanceOf(initiator); } - _swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); + uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); // Repay debt IERC20(assetTo).approve(address(POOL), repayAmount); POOL.repay(assetTo, repayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); + uint256 amountToPull = flashLoanDebt.sub(amount.sub(amountSwapped)); ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); - _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, flashLoanDebt, permitSignature); + _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, amountToPull, permitSignature); // Repay flashloan IERC20(assetFrom).approve(address(POOL), flashLoanDebt); - - // Take care of reserve leftover from the swap - _sendLeftovers(assetFrom, flashLoanDebt, leftOverAction, initiator); } /** * @dev Decodes debt information encoded in flashloan params * @param params Additional variadic field to include extra params. Expected parameters: * address Address of the reserve to be swapped to and repay - * uint256 leftOverAction Flag indicating what to do with the left over balance from the swap: - * (0) Deposit back - * (1) Direct transfer to user * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid * bool repayAllDebt Flag indicating if all the debt should be repaid @@ -157,7 +144,6 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { function _decodeParams(bytes memory params) internal pure returns (RepayParams memory) { ( address assetToSwapTo, - LeftoverAction leftOverAction, uint256 repayAmount, uint256 rateMode, bool repayAllDebt, @@ -166,11 +152,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint8 v, bytes32 r, bytes32 s - ) = abi.decode(params, (address, LeftoverAction, uint256, uint256, bool, uint256, uint256, uint8, bytes32, bytes32)); + ) = abi.decode(params, (address, uint256, uint256, bool, uint256, uint256, uint8, bytes32, bytes32)); return RepayParams( assetToSwapTo, - leftOverAction, repayAmount, rateMode, repayAllDebt, diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index 6eba519e..1b968922 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -241,7 +241,6 @@ export const buildLiquiditySwapParams = ( export const buildRepayAdapterParams = ( assetToSwapTo: tEthereumAddress, - leftoverAction: BigNumberish, repayAmount: BigNumberish, rateMode: BigNumberish, repayAllDebt: BigNumberish, @@ -252,29 +251,7 @@ export const buildRepayAdapterParams = ( s: string | Buffer ) => { return ethers.utils.defaultAbiCoder.encode( - [ - 'address', - 'uint256', - 'uint256', - 'uint256', - 'bool', - 'uint256', - 'uint256', - 'uint8', - 'bytes32', - 'bytes32', - ], - [ - assetToSwapTo, - leftoverAction, - repayAmount, - rateMode, - repayAllDebt, - permitAmount, - deadline, - v, - r, - s, - ] + ['address', 'uint256', 'uint256', 'bool', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'], + [assetToSwapTo, repayAmount, rateMode, repayAllDebt, permitAmount, deadline, v, r, s] ); }; diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index fcb9d81d..2a086fb8 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -2056,7 +2056,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2164,7 +2163,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2230,7 +2228,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2283,7 +2280,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2335,7 +2331,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2387,7 +2382,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2413,7 +2407,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ).to.be.revertedWith('maxAmountToSwap exceed max slippage'); }); - it('should swap tokens, repay debt and deposit in pool the left over', async () => { + it('should swap, repay debt and pull the needed ATokens leaving no leftovers', async () => { const { users, pool, @@ -2466,7 +2460,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, expectedDaiAmount, 1, 0, @@ -2497,7 +2490,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); @@ -2509,103 +2504,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ); }); - it('should swap tokens, repay debt and transfer to user the left over', 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); - - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - - const actualWEthSwapped = new BigNumber(flashloanAmount.toString()) - .multipliedBy(0.995) - .toFixed(0); - - const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); - - const wethBalanceBefore = await weth.balanceOf(userAddress); - - const params = buildRepayAdapterParams( - dai.address, - 1, - expectedDaiAmount, - 1, - 0, - 0, - 0, - 0, - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000' - ); - - await expect( - pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], - [0], - userAddress, - params, - 0 - ) - ) - .to.emit(uniswapRepayAdapter, 'Swapped') - .withArgs(weth.address, dai.address, actualWEthSwapped.toString(), expectedDaiAmount); - - 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 wethBalance = await weth.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.gt(userAEthBalanceBefore.sub(liquidityToSwap)); - expect(wethBalance).to.be.eq(wethBalanceBefore.add(leftOverWeth.toString())); - }); - it('should correctly swap tokens and repay the whole stable debt', async () => { const { users, @@ -2657,7 +2555,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, amountToRepay, 1, 1, @@ -2684,7 +2581,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); @@ -2746,7 +2645,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const params = buildRepayAdapterParams( dai.address, - 0, amountToRepay, 2, 1, @@ -2773,7 +2671,9 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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); From b0d9dbe2a7c48c4fd7d31bbd2542e5236ba80f25 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 10 Nov 2020 11:28:19 -0300 Subject: [PATCH 20/34] code improvements --- contracts/adapters/BaseUniswapAdapter.sol | 43 ++++++--------- .../adapters/UniswapLiquiditySwapAdapter.sol | 55 ++++++------------- contracts/adapters/UniswapRepayAdapter.sol | 4 +- 3 files changed, 37 insertions(+), 65 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index dab04acd..e5e779f7 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -24,14 +24,6 @@ contract BaseUniswapAdapter { using PercentageMath for uint256; using SafeERC20 for IERC20; - struct PermitParams { - uint256[] amount; - uint256[] deadline; - uint8[] v; - bytes32[] r; - bytes32[] s; - } - struct PermitSignature { uint256 amount; uint256 deadline; @@ -140,9 +132,9 @@ contract BaseUniswapAdapter { 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)); + .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'); @@ -183,9 +175,9 @@ contract BaseUniswapAdapter { 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)); + .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'); @@ -267,8 +259,7 @@ contract BaseUniswapAdapter { * @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); + return !(uint256(signature.deadline) == uint256(signature.v) && uint256(signature.deadline) == 0); } /** @@ -283,10 +274,10 @@ contract BaseUniswapAdapter { uint256 reservePrice = _getPrice(reserve); return amount - .mul(reservePrice) - .div(10**decimals) - .mul(ethUsdPrice) - .div(10**18); + .mul(reservePrice) + .div(10**decimals) + .mul(ethUsdPrice) + .div(10**18); } /** @@ -314,9 +305,9 @@ contract BaseUniswapAdapter { uint256 reserveOutDecimals = _getDecimals(reserveOut); uint256 outPerInPrice = finalAmountIn - .mul(10**18) - .mul(10**reserveOutDecimals) - .div(amounts[1].mul(10**reserveInDecimals)); + .mul(10**18) + .mul(10**reserveOutDecimals) + .div(amounts[1].mul(10**reserveInDecimals)); return AmountCalc( amounts[1], @@ -351,9 +342,9 @@ contract BaseUniswapAdapter { uint256 reserveOutDecimals = _getDecimals(reserveOut); uint256 inPerOutPrice = amountOut - .mul(10**18) - .mul(10**reserveInDecimals) - .div(finalAmountIn.mul(10**reserveOutDecimals)); + .mul(10**18) + .mul(10**reserveInDecimals) + .div(finalAmountIn.mul(10**reserveOutDecimals)); return AmountCalc( finalAmountIn, diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index a4150ef1..c023b1f5 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -15,6 +15,14 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; **/ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + struct PermitParams { + uint256[] amount; + uint256[] deadline; + uint8[] v; + bytes32[] r; + bytes32[] s; + } + struct SwapParams { address[] assetToSwapToList; uint256[] minAmountsToReceive; @@ -26,8 +34,8 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ILendingPoolAddressesProvider addressesProvider, IUniswapV2Router02 uniswapRouter ) - public - BaseUniswapAdapter(addressesProvider, uniswapRouter) + public + BaseUniswapAdapter(addressesProvider, uniswapRouter) {} /** @@ -191,19 +199,15 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 flashLoanDebt = amount.add(premium); uint256 amountToPull = swapAllBalance ? aTokenInitiatorBalance : flashLoanDebt; - _pullATokenAndRepayFlashLoan( - assetFrom, - aToken, - initiator, - amountToPull, - flashLoanDebt, - permitSignature - ); + _pullAToken(assetFrom, aToken, initiator, amountToPull, permitSignature); + + // Repay flashloan + IERC20(assetFrom).approve(address(POOL), flashLoanDebt); } /** - * @dev Decodes debt information encoded in flashloan params - * @param params Additional variadic field to include extra params. Expected parameters: + * @dev Decodes debt information encoded in flashloan 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 @@ -212,8 +216,8 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * 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 - * @return SwapParams struct containing decoded params - */ + * @return SwapParams struct containing decoded params + */ function _decodeParams(bytes memory params) internal pure returns (SwapParams memory) { ( address[] memory assetToSwapToList, @@ -228,27 +232,4 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { return SwapParams(assetToSwapToList, minAmountsToReceive, swapAllBalance, PermitParams(permitAmount, deadline, v, r, s)); } - - /** - * @dev Pull the ATokens from the user and use them to repay the flashloan - * @param reserve address of the asset - * @param reserveAToken address of the aToken of the reserve - * @param user address - * @param amountToPull amount to be pulled from the user - * @param flashLoanDebt need to be repaid - * @param permitSignature struct containing the permit signature - */ - function _pullATokenAndRepayFlashLoan( - address reserve, - address reserveAToken, - address user, - uint256 amountToPull, - uint256 flashLoanDebt, - PermitSignature memory permitSignature - ) internal { - _pullAToken(reserve, reserveAToken, user, amountToPull, permitSignature); - - // Repay flashloan - IERC20(reserve).approve(address(POOL), flashLoanDebt); - } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index fb821db3..d3db21b0 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -28,8 +28,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ILendingPoolAddressesProvider addressesProvider, IUniswapV2Router02 uniswapRouter ) - public - BaseUniswapAdapter(addressesProvider, uniswapRouter) + public + BaseUniswapAdapter(addressesProvider, uniswapRouter) {} /** From 4c693d1947268bfd9b9bf714948c0c288d117e57 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 13 Nov 2020 12:27:18 -0300 Subject: [PATCH 21/34] Update amountToSwap calc in liquidity swap adapter --- contracts/adapters/UniswapLiquiditySwapAdapter.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index c023b1f5..059170f9 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -183,7 +183,9 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address aToken = _getReserveData(assetFrom).aTokenAddress; uint256 aTokenInitiatorBalance = IERC20(aToken).balanceOf(initiator); - uint256 amountToSwap = swapAllBalance ? aTokenInitiatorBalance.sub(premium) : amount; + uint256 amountToSwap = swapAllBalance && aTokenInitiatorBalance.sub(premium) <= amount + ? aTokenInitiatorBalance.sub(premium) + : amount; uint256 receivedAmount = _swapExactTokensForTokens( assetFrom, @@ -197,7 +199,7 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { POOL.deposit(assetTo, receivedAmount, initiator, 0); uint256 flashLoanDebt = amount.add(premium); - uint256 amountToPull = swapAllBalance ? aTokenInitiatorBalance : flashLoanDebt; + uint256 amountToPull = amountToSwap.add(premium); _pullAToken(assetFrom, aToken, initiator, amountToPull, permitSignature); From 101c77578975825b3241edde4787394c688365a6 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 13 Nov 2020 12:28:57 -0300 Subject: [PATCH 22/34] Avoid param override in repay adapter --- contracts/adapters/UniswapRepayAdapter.sol | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index d3db21b0..2e09f746 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -101,6 +101,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 premium, PermitSignature memory permitSignature ) internal { + uint256 debtRepayAmount; + if (repayAllDebt) { ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); @@ -108,17 +110,19 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ? reserveDebtData.stableDebtTokenAddress : reserveDebtData.variableDebtTokenAddress; - repayAmount = IERC20(debtToken).balanceOf(initiator); + debtRepayAmount = IERC20(debtToken).balanceOf(initiator); + } else { + debtRepayAmount = repayAmount; } - uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, repayAmount); + uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, debtRepayAmount); // Repay debt - IERC20(assetTo).approve(address(POOL), repayAmount); - POOL.repay(assetTo, repayAmount, rateMode, initiator); + IERC20(assetTo).approve(address(POOL), debtRepayAmount); + POOL.repay(assetTo, debtRepayAmount, rateMode, initiator); uint256 flashLoanDebt = amount.add(premium); - uint256 amountToPull = flashLoanDebt.sub(amount.sub(amountSwapped)); + uint256 amountToPull = amountSwapped.add(premium); ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, amountToPull, permitSignature); From d0d0e869d0d734238d7c90023d8a876f9df1e390 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 13 Nov 2020 12:30:14 -0300 Subject: [PATCH 23/34] Fix flash loan fee usage in _getAmountsIn --- contracts/adapters/BaseUniswapAdapter.sol | 4 ++-- test/uniswapAdapters.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index e5e779f7..7a727b7d 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -335,8 +335,8 @@ contract BaseUniswapAdapter { uint256[] memory amounts = UNISWAP_ROUTER.getAmountsIn(amountOut, path); - // Subtract flash loan fee - uint256 finalAmountIn = amounts[0].sub(amounts[0].mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); + // 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); diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 2a086fb8..6f40530f 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -152,7 +152,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const amountIn = parseEther('1'); const flashloanPremium = amountIn.mul(9).div(10000); - const amountToSwap = amountIn.sub(flashloanPremium); + const amountToSwap = amountIn.add(flashloanPremium); const wethPrice = await oracle.getAssetPrice(weth.address); const daiPrice = await oracle.getAssetPrice(dai.address); @@ -197,7 +197,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const amountIn = parseEther('10'); const flashloanPremium = amountIn.mul(9).div(10000); - const amountToSwap = amountIn.sub(flashloanPremium); + const amountToSwap = amountIn.add(flashloanPremium); const aavePrice = await oracle.getAssetPrice(aave.address); const usdcPrice = await oracle.getAssetPrice(usdc.address); @@ -241,7 +241,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe('UniswapLiquiditySwapAdapter', () => { + describe.only('UniswapLiquiditySwapAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; @@ -1959,7 +1959,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe('UniswapRepayAdapter', () => { + describe.only('UniswapRepayAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; From 2e70cc628841c586d63ff2a51c5cfd0a2a6aba07 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 17 Nov 2020 13:31:05 -0300 Subject: [PATCH 24/34] remove .only in tests --- test/uniswapAdapters.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 6f40530f..146e8320 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -241,7 +241,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe.only('UniswapLiquiditySwapAdapter', () => { + describe('UniswapLiquiditySwapAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; @@ -1959,7 +1959,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); }); - describe.only('UniswapRepayAdapter', () => { + describe('UniswapRepayAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { const {addressesProvider} = testEnv; From 8a303c61950130a1a0b9aaae4a767b902aa00840 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Thu, 19 Nov 2020 16:39:00 -0300 Subject: [PATCH 25/34] Refactor repayMode in repayAdapter --- contracts/adapters/UniswapRepayAdapter.sol | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 2e09f746..d8461400 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -16,11 +16,18 @@ import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; **/ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { + /* + * STANDARD: Use the provided amounts parameters + * ALL_DEBT: Repay the whole debt balance + * ALL_COLLATERAL: Use all the collateral balance to repay the max amount of debt + */ + enum RepayMode {STANDARD, ALL_DEBT, ALL_COLLATERAL} + struct RepayParams { address assetToSwapTo; uint256 repayAmount; uint256 rateMode; - bool repayAllDebt; + RepayMode repayMode; PermitSignature permitSignature; } @@ -45,7 +52,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * address Address of the reserve to be swapped to and repay * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid - * bool repayAllDebt Flag indicating if all the debt should be repaid + * RepayMode repayMode Enum indicating the repaid mode * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -70,7 +77,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { decodedParams.repayAmount, decodedParams.rateMode, initiator, - decodedParams.repayAllDebt, + decodedParams.repayMode, premiums[0], decodedParams.permitSignature ); @@ -97,13 +104,13 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 repayAmount, uint256 rateMode, address initiator, - bool repayAllDebt, + RepayMode repayMode, uint256 premium, PermitSignature memory permitSignature ) internal { uint256 debtRepayAmount; - if (repayAllDebt) { + if (repayMode == RepayMode.ALL_DEBT) { ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); address debtToken = ReserveLogic.InterestRateMode(rateMode) == ReserveLogic.InterestRateMode.STABLE @@ -137,7 +144,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * address Address of the reserve to be swapped to and repay * uint256 repayAmount Amount of debt to be repaid * uint256 rateMode Rate modes of the debt to be repaid - * bool repayAllDebt Flag indicating if all the debt should be repaid + * RepayMode repayMode Enum indicating the repaid mode * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -150,19 +157,19 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address assetToSwapTo, uint256 repayAmount, uint256 rateMode, - bool repayAllDebt, + RepayMode repayMode, uint256 permitAmount, uint256 deadline, uint8 v, bytes32 r, bytes32 s - ) = abi.decode(params, (address, uint256, uint256, bool, uint256, uint256, uint8, bytes32, bytes32)); + ) = abi.decode(params, (address, uint256, uint256, RepayMode, uint256, uint256, uint8, bytes32, bytes32)); return RepayParams( assetToSwapTo, repayAmount, rateMode, - repayAllDebt, + repayMode, PermitSignature( permitAmount, deadline, From b48b50208a160ab3839d5ed9117d5335c8e21c39 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 20 Nov 2020 09:06:28 -0300 Subject: [PATCH 26/34] Support using all the collateral for a debt repay --- contracts/adapters/UniswapRepayAdapter.sol | 44 ++- helpers/contracts-helpers.ts | 16 +- test/uniswapAdapters.spec.ts | 318 +++++++++++++++++---- 3 files changed, 317 insertions(+), 61 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index d8461400..616a6ce1 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -109,29 +109,40 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { PermitSignature memory permitSignature ) internal { uint256 debtRepayAmount; + uint256 amountSwapped; - if (repayMode == RepayMode.ALL_DEBT) { - ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); + ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); - address debtToken = ReserveLogic.InterestRateMode(rateMode) == ReserveLogic.InterestRateMode.STABLE - ? reserveDebtData.stableDebtTokenAddress - : reserveDebtData.variableDebtTokenAddress; + if (repayMode == RepayMode.ALL_COLLATERAL) { + uint256 aTokenInitiatorBalance = IERC20(reserveData.aTokenAddress).balanceOf(initiator); + amountSwapped = aTokenInitiatorBalance.sub(premium); - debtRepayAmount = IERC20(debtToken).balanceOf(initiator); + debtRepayAmount = _swapExactTokensForTokens(assetFrom, assetTo, amountSwapped, repayAmount); } else { - debtRepayAmount = repayAmount; - } + if (repayMode == RepayMode.ALL_DEBT) { + ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); - uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, debtRepayAmount); + address debtToken = ReserveLogic.InterestRateMode(rateMode) == ReserveLogic.InterestRateMode.STABLE + ? reserveDebtData.stableDebtTokenAddress + : reserveDebtData.variableDebtTokenAddress; + + debtRepayAmount = IERC20(debtToken).balanceOf(initiator); + } else { + debtRepayAmount = repayAmount; + } + + amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, debtRepayAmount); + } // Repay debt IERC20(assetTo).approve(address(POOL), debtRepayAmount); POOL.repay(assetTo, debtRepayAmount, rateMode, initiator); + // In the case the repay amount provided exceeded the actual debt, send the leftovers to the user + _sendRepayLeftovers(assetTo, initiator); uint256 flashLoanDebt = amount.add(premium); uint256 amountToPull = amountSwapped.add(premium); - ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, amountToPull, permitSignature); // Repay flashloan @@ -179,4 +190,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) ); } + + /** + * @dev Transfers the balance of the adapter to the user, as there shouldn't be any leftover in the adapter + * @param asset address of the asset + * @param user address + */ + function _sendRepayLeftovers(address asset, address user) internal { + uint256 assetLeftover = IERC20(asset).balanceOf(address(this)); + + if (assetLeftover > 0) { + IERC20(asset).transfer(user, assetLeftover); + } + } } diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index d420c00e..a055fb32 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -262,7 +262,7 @@ export const buildRepayAdapterParams = ( assetToSwapTo: tEthereumAddress, repayAmount: BigNumberish, rateMode: BigNumberish, - repayAllDebt: BigNumberish, + repayMode: BigNumberish, permitAmount: BigNumberish, deadline: BigNumberish, v: BigNumberish, @@ -270,7 +270,17 @@ export const buildRepayAdapterParams = ( s: string | Buffer ) => { return ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint256', 'bool', 'uint256', 'uint256', 'uint8', 'bytes32', 'bytes32'], - [assetToSwapTo, repayAmount, rateMode, repayAllDebt, permitAmount, deadline, v, r, s] + [ + 'address', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint8', + 'bytes32', + 'bytes32', + ], + [assetToSwapTo, repayAmount, rateMode, repayMode, permitAmount, deadline, v, r, s] ); }; diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 146e8320..2f0d35c4 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1,4 +1,4 @@ -import {makeSuite, TestEnv} from './helpers/make-suite'; +import { makeSuite, TestEnv } from './helpers/make-suite'; import { convertToCurrencyDecimals, getContract, @@ -7,24 +7,24 @@ import { buildLiquiditySwapParams, buildRepayAdapterParams, } from '../helpers/contracts-helpers'; -import {getMockUniswapRouter} from '../helpers/contracts-getters'; +import { getMockUniswapRouter } from '../helpers/contracts-getters'; import { deployUniswapLiquiditySwapAdapter, deployUniswapRepayAdapter, } from '../helpers/contracts-deployments'; -import {MockUniswapV2Router02} from '../types/MockUniswapV2Router02'; -import {Zero} from '@ethersproject/constants'; +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 {StableDebtToken} from '../types/StableDebtToken'; -import {BUIDLEREVM_CHAINID} from '../helpers/buidler-constants'; -import {MAX_UINT_AMOUNT, USD_ADDRESS} from '../helpers/constants'; -const {parseEther} = ethers.utils; +import { DRE, evmRevert, evmSnapshot } from '../helpers/misc-utils'; +import { ethers } from 'ethers'; +import { eContractid } from '../helpers/types'; +import { AToken } from '../types/AToken'; +import { StableDebtToken } from '../types/StableDebtToken'; +import { BUIDLEREVM_CHAINID } from '../helpers/buidler-constants'; +import { MAX_UINT_AMOUNT, USD_ADDRESS } from '../helpers/constants'; +const { parseEther } = ethers.utils; -const {expect} = require('chai'); +const { expect } = require('chai'); makeSuite('Uniswap adapters', (testEnv: TestEnv) => { let mockUniswapRouter: MockUniswapV2Router02; @@ -45,7 +45,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('BaseUniswapAdapter', () => { describe('getAmountsOut', () => { it('should return the estimated amountOut and prices for the asset swap', async () => { - const {weth, dai, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const { weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; const amountIn = parseEther('1'); const flashloanPremium = amountIn.mul(9).div(10000); @@ -94,7 +94,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(result['3']).to.be.eq(daiUsdValue); }); it('should work correctly with different decimals', async () => { - const {aave, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const { aave, usdc, uniswapLiquiditySwapAdapter, oracle } = testEnv; const amountIn = parseEther('10'); const flashloanPremium = amountIn.mul(9).div(10000); @@ -148,7 +148,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('getAmountsIn', () => { it('should return the estimated required amountIn for the asset swap', async () => { - const {weth, dai, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const { weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; const amountIn = parseEther('1'); const flashloanPremium = amountIn.mul(9).div(10000); @@ -193,7 +193,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(result['3']).to.be.eq(daiUsdValue); }); it('should work correctly with different decimals', async () => { - const {aave, usdc, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const { aave, usdc, uniswapLiquiditySwapAdapter, oracle } = testEnv; const amountIn = parseEther('10'); const flashloanPremium = amountIn.mul(9).div(10000); @@ -244,7 +244,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('UniswapLiquiditySwapAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { - const {addressesProvider} = testEnv; + const { addressesProvider } = testEnv; await deployUniswapLiquiditySwapAdapter([ addressesProvider.address, mockUniswapRouter.address, @@ -260,7 +260,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('executeOperation', () => { beforeEach(async () => { - const {users, weth, dai, usdc, pool, deployer} = testEnv; + const { users, weth, dai, usdc, pool, deployer } = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -280,7 +280,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); 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 { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -542,7 +551,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { deadline, amountWETHtoSwap.toString() ); - const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( + const { v: aWETHv, r: aWETHr, s: aWETHs } = getSignatureFromTypedData( ownerPrivateKey, aWethMsgParams ); @@ -559,7 +568,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { deadline, amountUSDCtoSwap.toString() ); - const {v: aUsdcv, r: aUsdcr, s: aUsdcs} = getSignatureFromTypedData( + const { v: aUsdcv, r: aUsdcr, s: aUsdcs } = getSignatureFromTypedData( ownerPrivateKey, aUsdcMsgParams ); @@ -607,7 +616,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens with permit', async () => { - const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -648,7 +666,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { throw new Error('INVALID_OWNER_PK'); } - const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); const params = buildLiquiditySwapParams( [dai.address], @@ -695,7 +713,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if inconsistent params', async () => { - const {users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -924,7 +942,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if caller not lending pool', async () => { - const {users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1066,7 +1084,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when min amount to receive exceeds the max slippage amount', async () => { - const {users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aWETH, pool, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1115,7 +1133,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens all the balance', async () => { - const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1188,7 +1215,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens all the balance using permit', async () => { - const {users, weth, oracle, dai, aDai, aWETH, pool, uniswapLiquiditySwapAdapter} = testEnv; + const { + users, + weth, + oracle, + dai, + aDai, + aWETH, + pool, + uniswapLiquiditySwapAdapter, + } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1229,7 +1265,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { throw new Error('INVALID_OWNER_PK'); } - const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); const params = buildLiquiditySwapParams( [dai.address], @@ -1283,7 +1319,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('swapAndDeposit', () => { beforeEach(async () => { - const {users, weth, dai, pool, deployer} = testEnv; + const { users, weth, dai, pool, deployer } = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -1298,7 +1334,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens and deposit the out tokens in the pool', async () => { - const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1355,7 +1391,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap tokens using permit', async () => { - const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1393,7 +1429,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { throw new Error('INVALID_OWNER_PK'); } - const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); await expect( uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( @@ -1433,7 +1469,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if inconsistent params', async () => { - const {users, weth, dai, uniswapLiquiditySwapAdapter, oracle} = testEnv; + const { users, weth, dai, uniswapLiquiditySwapAdapter, oracle } = testEnv; const user = users[0].signer; const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); @@ -1529,7 +1565,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert when min amount to receive exceeds the max slippage amount', async () => { - const {users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const amountWETHtoSwap = await convertToCurrencyDecimals(weth.address, '10'); @@ -1740,7 +1776,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { deadline, amountWETHtoSwap.toString() ); - const {v: aWETHv, r: aWETHr, s: aWETHs} = getSignatureFromTypedData( + const { v: aWETHv, r: aWETHr, s: aWETHs } = getSignatureFromTypedData( ownerPrivateKey, aWethMsgParams ); @@ -1757,7 +1793,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { deadline, amountUSDCtoSwap.toString() ); - const {v: aUsdcv, r: aUsdcr, s: aUsdcs} = getSignatureFromTypedData( + const { v: aUsdcv, r: aUsdcr, s: aUsdcs } = getSignatureFromTypedData( ownerPrivateKey, aUsdcMsgParams ); @@ -1806,7 +1842,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap all the balance when using a bigger amount', async () => { - const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1873,7 +1909,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should correctly swap all the balance when using permit', async () => { - const {users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter} = testEnv; + const { users, weth, oracle, dai, aDai, aWETH, uniswapLiquiditySwapAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -1917,7 +1953,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { deadline, bigAmountToSwap.toString() ); - const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, aWethMsgParams); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, aWethMsgParams); await expect( uniswapLiquiditySwapAdapter.connect(user).swapAndDeposit( @@ -1962,7 +1998,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('UniswapRepayAdapter', () => { describe('constructor', () => { it('should deploy with correct parameters', async () => { - const {addressesProvider} = testEnv; + const { addressesProvider } = testEnv; await deployUniswapRepayAdapter([addressesProvider.address, mockUniswapRouter.address]); }); @@ -1974,7 +2010,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { describe('executeOperation', () => { beforeEach(async () => { - const {users, weth, dai, usdc, aave, pool, deployer} = testEnv; + const { users, weth, dai, usdc, aave, pool, deployer } = testEnv; const userAddress = users[0].address; // Provide liquidity @@ -2157,7 +2193,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { throw new Error('INVALID_OWNER_PK'); } - const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); @@ -2203,7 +2239,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if caller not lending pool', async () => { - const {users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter} = testEnv; + const { users, pool, weth, aWETH, oracle, dai, uniswapRepayAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -2252,7 +2288,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); 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 { users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -2306,7 +2342,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); it('should revert if there is not debt to repay', async () => { - const {users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH} = testEnv; + const { users, pool, weth, oracle, dai, uniswapRepayAdapter, aWETH } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -2357,7 +2393,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); 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 { users, pool, weth, oracle, dai, aWETH, uniswapRepayAdapter } = testEnv; const user = users[0].signer; const userAddress = users[0].address; @@ -2681,6 +2717,192 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); + + it('should swap and repay debt using all the collateral for a bigger 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) + ); + + const userDebt = new BigNumber(expectedDaiAmount.toString()).multipliedBy(1.1).toFixed(0); + // Open user Debt + await pool.connect(user).borrow(dai.address, userDebt, 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); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const actualWEthSwapped = new BigNumber(flashloanAmount.toString()) + .multipliedBy(0.995) + .toFixed(0); + + // Remove other balance + await aWETH + .connect(user) + .transfer(users[1].address, userAEthBalanceBefore.sub(actualWEthSwapped)); + + await mockUniswapRouter.connect(user).setAmountToReturn(weth.address, expectedDaiAmount); + + const params = buildRepayAdapterParams( + dai.address, + expectedDaiAmount, + 1, + 2, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.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.lt(expectedDaiAmount); + expect(userAEthBalance).to.be.eq(Zero); + }); + + it('should swap and repay debt using all the collateral for a smaller 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) + ); + + const userDebt = new BigNumber(expectedDaiAmount.toString()).multipliedBy(0.9).toFixed(0); + // Open user Debt + await pool.connect(user).borrow(dai.address, userDebt, 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); + + // Subtract the FL fee from the amount to be swapped 0,09% + const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); + + const actualWEthSwapped = new BigNumber(flashloanAmount.toString()) + .multipliedBy(0.995) + .toFixed(0); + + // Remove other balance + await aWETH + .connect(user) + .transfer(users[1].address, userAEthBalanceBefore.sub(actualWEthSwapped)); + + await mockUniswapRouter.connect(user).setAmountToReturn(weth.address, expectedDaiAmount); + + const params = buildRepayAdapterParams( + dai.address, + expectedDaiAmount, + 1, + 2, + 0, + 0, + 0, + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + await pool + .connect(user) + .flashLoan( + uniswapRepayAdapter.address, + [weth.address], + [flashloanAmount.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); // Validate there are no leftovers + expect(userDaiStableDebtAmountBefore).to.be.gte(userDebt); + expect(userDaiStableDebtAmount).to.be.eq(Zero); + expect(userAEthBalance).to.be.eq(Zero); + }); }); }); }); From 43d05c2bdf846fef7c8f4145988511a36fea3549 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Fri, 20 Nov 2020 15:53:50 -0300 Subject: [PATCH 27/34] Refactor repayAdapter to flash loan the debt asset instead of the collateral --- contracts/adapters/UniswapRepayAdapter.sol | 105 +++--- helpers/contracts-helpers.ts | 8 +- test/uniswapAdapters.spec.ts | 358 +++++---------------- 3 files changed, 114 insertions(+), 357 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 616a6ce1..c42df484 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -16,18 +16,10 @@ import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; **/ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { - /* - * STANDARD: Use the provided amounts parameters - * ALL_DEBT: Repay the whole debt balance - * ALL_COLLATERAL: Use all the collateral balance to repay the max amount of debt - */ - enum RepayMode {STANDARD, ALL_DEBT, ALL_COLLATERAL} - struct RepayParams { - address assetToSwapTo; - uint256 repayAmount; + address collateralAsset; + uint256 collateralAmount; uint256 rateMode; - RepayMode repayMode; PermitSignature permitSignature; } @@ -40,17 +32,17 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { {} /** - * @dev Swaps the received reserve amount into the asset specified in the params. The received funds from the swap are - * then used to repay a debt on 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. + * @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 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. * @param assets Address to be swapped * @param amounts Amount of the reserve 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 Address of the reserve to be swapped to and repay - * uint256 repayAmount Amount of debt to be repaid + * 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 * RepayMode repayMode Enum indicating the repaid mode * uint256 permitAmount Amount for the permit signature @@ -71,13 +63,12 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { RepayParams memory decodedParams = _decodeParams(params); _swapAndRepay( + decodedParams.collateralAsset, assets[0], - decodedParams.assetToSwapTo, amounts[0], - decodedParams.repayAmount, + decodedParams.collateralAmount, decodedParams.rateMode, initiator, - decodedParams.repayMode, premiums[0], decodedParams.permitSignature ); @@ -86,12 +77,12 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { } /** - * @dev Perform the swap and the repay of the debt + * @dev Perform the repay of the debt, pulls the initiator collateral and swaps to repay the flash loan * * @param assetFrom Address of token to be swapped - * @param assetTo Address of token to be received - * @param amount Amount of the reserve to be swapped - * @param repayAmount Amount of the debt to be repaid + * @param assetTo 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 @@ -101,61 +92,39 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { address assetFrom, address assetTo, uint256 amount, - uint256 repayAmount, + uint256 collateralAmount, uint256 rateMode, address initiator, - RepayMode repayMode, uint256 premium, PermitSignature memory permitSignature ) internal { - uint256 debtRepayAmount; - uint256 amountSwapped; - - ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); - - if (repayMode == RepayMode.ALL_COLLATERAL) { - uint256 aTokenInitiatorBalance = IERC20(reserveData.aTokenAddress).balanceOf(initiator); - amountSwapped = aTokenInitiatorBalance.sub(premium); - - debtRepayAmount = _swapExactTokensForTokens(assetFrom, assetTo, amountSwapped, repayAmount); - } else { - if (repayMode == RepayMode.ALL_DEBT) { - ReserveLogic.ReserveData memory reserveDebtData = _getReserveData(assetTo); - - address debtToken = ReserveLogic.InterestRateMode(rateMode) == ReserveLogic.InterestRateMode.STABLE - ? reserveDebtData.stableDebtTokenAddress - : reserveDebtData.variableDebtTokenAddress; - - debtRepayAmount = IERC20(debtToken).balanceOf(initiator); - } else { - debtRepayAmount = repayAmount; - } - - amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, amount, debtRepayAmount); - } - // Repay debt - IERC20(assetTo).approve(address(POOL), debtRepayAmount); - POOL.repay(assetTo, debtRepayAmount, rateMode, initiator); - // In the case the repay amount provided exceeded the actual debt, send the leftovers to the user - _sendRepayLeftovers(assetTo, initiator); + IERC20(assetTo).approve(address(POOL), amount); + POOL.repay(assetTo, amount, rateMode, initiator); + uint256 debtRepayLeftovers = IERC20(assetTo).balanceOf(address(this)); uint256 flashLoanDebt = amount.add(premium); - uint256 amountToPull = amountSwapped.add(premium); + uint256 neededForFlashLoanDebt = flashLoanDebt.sub(debtRepayLeftovers); - _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, amountToPull, permitSignature); + // Pull aTokens from user + ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); + _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, collateralAmount, permitSignature); + + uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, collateralAmount, neededForFlashLoanDebt); + + // Send collateral leftovers from swap to the user + _sendLeftovers(assetFrom, initiator); // Repay flashloan - IERC20(assetFrom).approve(address(POOL), flashLoanDebt); + IERC20(assetTo).approve(address(POOL), flashLoanDebt); } /** * @dev Decodes debt information encoded in flashloan params * @param params Additional variadic field to include extra params. Expected parameters: - * address Address of the reserve to be swapped to and repay - * uint256 repayAmount Amount of debt to be repaid + * 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 - * RepayMode repayMode Enum indicating the repaid mode * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -165,22 +134,20 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { */ function _decodeParams(bytes memory params) internal pure returns (RepayParams memory) { ( - address assetToSwapTo, - uint256 repayAmount, + address collateralAsset, + uint256 collateralAmount, uint256 rateMode, - RepayMode repayMode, uint256 permitAmount, uint256 deadline, uint8 v, bytes32 r, bytes32 s - ) = abi.decode(params, (address, uint256, uint256, RepayMode, uint256, uint256, uint8, bytes32, bytes32)); + ) = abi.decode(params, (address, uint256, uint256, uint256, uint256, uint8, bytes32, bytes32)); return RepayParams( - assetToSwapTo, - repayAmount, + collateralAsset, + collateralAmount, rateMode, - repayMode, PermitSignature( permitAmount, deadline, @@ -196,7 +163,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param asset address of the asset * @param user address */ - function _sendRepayLeftovers(address asset, address user) internal { + function _sendLeftovers(address asset, address user) internal { uint256 assetLeftover = IERC20(asset).balanceOf(address(this)); if (assetLeftover > 0) { diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index a055fb32..832e8ae7 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -259,10 +259,9 @@ export const buildLiquiditySwapParams = ( }; export const buildRepayAdapterParams = ( - assetToSwapTo: tEthereumAddress, - repayAmount: BigNumberish, + collateralAsset: tEthereumAddress, + collateralAmount: BigNumberish, rateMode: BigNumberish, - repayMode: BigNumberish, permitAmount: BigNumberish, deadline: BigNumberish, v: BigNumberish, @@ -276,11 +275,10 @@ export const buildRepayAdapterParams = ( 'uint256', 'uint256', 'uint256', - 'uint256', 'uint8', 'bytes32', 'bytes32', ], - [assetToSwapTo, repayAmount, rateMode, repayMode, permitAmount, deadline, v, r, s] + [collateralAsset, collateralAmount, rateMode, permitAmount, deadline, v, r, s] ); }; diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index 2f0d35c4..b821f238 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -2085,19 +2085,19 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await aWETH.connect(user).approve(uniswapRepayAdapter.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); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2107,8 +2107,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2116,7 +2116,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ) .to.emit(uniswapRepayAdapter, 'Swapped') - .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + .withArgs(weth.address, dai.address, liquidityToSwap.toString(), flashLoanDebt); const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); @@ -2170,9 +2170,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; 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(); @@ -2195,13 +2192,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const { v, r, s } = getSignatureFromTypedData(ownerPrivateKey, msgParams); - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, - 0, liquidityToSwap, deadline, v, @@ -2214,8 +2214,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2223,7 +2223,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ) .to.emit(uniswapRepayAdapter, 'Swapped') - .withArgs(weth.address, dai.address, flashloanAmount.toString(), expectedDaiAmount); + .withArgs(weth.address, dai.address, liquidityToSwap.toString(), flashLoanDebt); const adapterWethBalance = await weth.balanceOf(uniswapRepayAdapter.address); const adapterDaiBalance = await dai.balanceOf(uniswapRepayAdapter.address); @@ -2257,19 +2257,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aWETH.connect(user).approve(uniswapRepayAdapter.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); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2278,8 +2274,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { uniswapRepayAdapter .connect(user) .executeOperation( - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params @@ -2309,19 +2305,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aWETH.connect(user).approve(uniswapRepayAdapter.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); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2331,8 +2323,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2360,19 +2352,15 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aWETH.connect(user).approve(uniswapRepayAdapter.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); - - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2382,8 +2370,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2408,22 +2396,18 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { // Open user Debt await pool.connect(user).borrow(dai.address, expectedDaiAmount, 1, 0, userAddress); - await aWETH.connect(user).approve(uniswapRepayAdapter.address, amountWETHtoSwap); - - // Subtract the FL fee from the amount to be swapped 0,09% const bigMaxAmountToSwap = amountWETHtoSwap.mul(2); - const flashloanAmount = new BigNumber(bigMaxAmountToSwap.toString()).div(1.0009).toFixed(0); + await aWETH.connect(user).approve(uniswapRepayAdapter.address, bigMaxAmountToSwap); - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, flashloanAmount); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, bigMaxAmountToSwap); const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + bigMaxAmountToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2433,8 +2417,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2482,26 +2466,27 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const liquidityToSwap = amountWETHtoSwap; await aWETH.connect(user).approve(uniswapRepayAdapter.address, liquidityToSwap); const userAEthBalanceBefore = await aWETH.balanceOf(userAddress); + const userWethBalanceBefore = await weth.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 actualWEthSwapped = new BigNumber(flashloanAmount.toString()) + const actualWEthSwapped = new BigNumber(liquidityToSwap.toString()) .multipliedBy(0.995) .toFixed(0); - const leftOverWeth = new BigNumber(flashloanAmount).minus(actualWEthSwapped); + const leftOverWeth = new BigNumber(liquidityToSwap.toString()).minus(actualWEthSwapped); await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, actualWEthSwapped); + const flashLoanDebt = new BigNumber(expectedDaiAmount.toString()) + .multipliedBy(1.0009) + .toFixed(0); + const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, + weth.address, + liquidityToSwap, 1, 0, 0, 0, - 0, '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000000' ); @@ -2511,8 +2496,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [expectedDaiAmount.toString()], [0], userAddress, params, @@ -2520,13 +2505,14 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { ) ) .to.emit(uniswapRepayAdapter, 'Swapped') - .withArgs(weth.address, dai.address, actualWEthSwapped.toString(), expectedDaiAmount); + .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); @@ -2534,13 +2520,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userDaiStableDebtAmountBefore).to.be.gte(expectedDaiAmount); expect(userDaiStableDebtAmount).to.be.lt(expectedDaiAmount); expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); - expect(userAEthBalance).to.be.gt(userAEthBalanceBefore.sub(liquidityToSwap)); - expect(userAEthBalance).to.be.gte( - userAEthBalanceBefore.sub(liquidityToSwap).add(leftOverWeth.toString()) - ); + expect(userAEthBalance).to.be.eq(userAEthBalanceBefore.sub(liquidityToSwap)); + expect(userWethBalance).to.be.gte(userWethBalanceBefore.add(leftOverWeth.toString())); }); - it('should correctly swap tokens and repay the whole stable debt', async () => { + it('should correctly swap tokens and repay the whole stable debt with no leftovers', async () => { const { users, pool, @@ -2580,19 +2564,16 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await aWETH.connect(user).approve(uniswapRepayAdapter.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); + // 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, flashloanAmount); - - // Passed amount to repay is smaller than debt, - // but repayAllDebt flag is enabled so the whole debt should be paid - const amountToRepay = expectedDaiAmount.div(2); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); const params = buildRepayAdapterParams( - dai.address, - amountToRepay, - 1, + weth.address, + liquidityToSwap, 1, 0, 0, @@ -2605,8 +2586,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [amountToRepay.toString()], [0], userAddress, params, @@ -2628,7 +2609,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); - it('should correctly swap tokens and repay the whole variable debt', async () => { + it('should correctly swap tokens and repay the whole variable debt with no leftovers', async () => { const { users, pool, @@ -2670,20 +2651,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { await aWETH.connect(user).approve(uniswapRepayAdapter.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); + // 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, flashloanAmount); - - // Passed amount to repay is smaller than debt, - // but repayAllDebt flag is enabled so the whole debt should be paid - const amountToRepay = expectedDaiAmount.div(2); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); const params = buildRepayAdapterParams( - dai.address, - amountToRepay, + weth.address, + liquidityToSwap, 2, - 1, 0, 0, 0, @@ -2695,8 +2673,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .connect(user) .flashLoan( uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.toString()], + [dai.address], + [amountToRepay.toString()], [0], userAddress, params, @@ -2717,192 +2695,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.lt(userAEthBalanceBefore); expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); - - it('should swap and repay debt using all the collateral for a bigger 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) - ); - - const userDebt = new BigNumber(expectedDaiAmount.toString()).multipliedBy(1.1).toFixed(0); - // Open user Debt - await pool.connect(user).borrow(dai.address, userDebt, 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); - - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - - const actualWEthSwapped = new BigNumber(flashloanAmount.toString()) - .multipliedBy(0.995) - .toFixed(0); - - // Remove other balance - await aWETH - .connect(user) - .transfer(users[1].address, userAEthBalanceBefore.sub(actualWEthSwapped)); - - await mockUniswapRouter.connect(user).setAmountToReturn(weth.address, expectedDaiAmount); - - const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, - 1, - 2, - 0, - 0, - 0, - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000' - ); - - await pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.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.lt(expectedDaiAmount); - expect(userAEthBalance).to.be.eq(Zero); - }); - - it('should swap and repay debt using all the collateral for a smaller 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) - ); - - const userDebt = new BigNumber(expectedDaiAmount.toString()).multipliedBy(0.9).toFixed(0); - // Open user Debt - await pool.connect(user).borrow(dai.address, userDebt, 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); - - // Subtract the FL fee from the amount to be swapped 0,09% - const flashloanAmount = new BigNumber(liquidityToSwap.toString()).div(1.0009).toFixed(0); - - const actualWEthSwapped = new BigNumber(flashloanAmount.toString()) - .multipliedBy(0.995) - .toFixed(0); - - // Remove other balance - await aWETH - .connect(user) - .transfer(users[1].address, userAEthBalanceBefore.sub(actualWEthSwapped)); - - await mockUniswapRouter.connect(user).setAmountToReturn(weth.address, expectedDaiAmount); - - const params = buildRepayAdapterParams( - dai.address, - expectedDaiAmount, - 1, - 2, - 0, - 0, - 0, - '0x0000000000000000000000000000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000000000000000000000000000' - ); - - await pool - .connect(user) - .flashLoan( - uniswapRepayAdapter.address, - [weth.address], - [flashloanAmount.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); // Validate there are no leftovers - expect(userDaiStableDebtAmountBefore).to.be.gte(userDebt); - expect(userDaiStableDebtAmount).to.be.eq(Zero); - expect(userAEthBalance).to.be.eq(Zero); - }); }); }); }); From 50e5ea907b9d3977b399ef78ef6ef06011ff1ca2 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 24 Nov 2020 12:11:13 -0300 Subject: [PATCH 28/34] Add first draft of swapAndRepay without using flash loans --- contracts/adapters/BaseUniswapAdapter.sol | 29 ++++++++++++------ contracts/adapters/UniswapRepayAdapter.sol | 34 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 7a727b7d..0e4ca193 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -73,7 +73,7 @@ contract BaseUniswapAdapter { view returns (uint256, uint256, uint256, uint256) { - AmountCalc memory results = _getAmountsOut(reserveIn, reserveOut, amountIn); + AmountCalc memory results = _getAmountsOutData(reserveIn, reserveOut, amountIn); return ( results.calculatedAmount, @@ -98,7 +98,7 @@ contract BaseUniswapAdapter { view returns (uint256, uint256, uint256, uint256) { - AmountCalc memory results = _getAmountsIn(reserveIn, reserveOut, amountOut); + AmountCalc memory results = _getAmountsInData(reserveIn, reserveOut, amountOut); return ( results.calculatedAmount, @@ -291,7 +291,7 @@ contract BaseUniswapAdapter { * uint256 In amount of reserveIn value denominated in USD (8 decimals) * uint256 Out amount of reserveOut value denominated in USD (8 decimals) */ - function _getAmountsOut(address reserveIn, address reserveOut, uint256 amountIn) internal view returns (AmountCalc memory) { + 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)); @@ -328,12 +328,8 @@ contract BaseUniswapAdapter { * uint256 In amount of reserveIn value denominated in USD (8 decimals) * uint256 Out amount of reserveOut value denominated in USD (8 decimals) */ - function _getAmountsIn(address reserveIn, address reserveOut, uint256 amountOut) internal view returns (AmountCalc memory) { - address[] memory path = new address[](2); - path[0] = reserveIn; - path[1] = reserveOut; - - uint256[] memory amounts = UNISWAP_ROUTER.getAmountsIn(amountOut, path); + function _getAmountsInData(address reserveIn, address reserveOut, uint256 amountOut) internal view returns (AmountCalc memory) { + uint256[] memory amounts = _getAmountsIn(reserveIn, reserveOut, amountOut); // Add flash loan fee uint256 finalAmountIn = amounts[0].add(amounts[0].mul(FLASHLOAN_PREMIUM_TOTAL).div(10000)); @@ -353,4 +349,19 @@ contract BaseUniswapAdapter { _calcUsdValue(reserveOut, amountOut, reserveOutDecimals) ); } + + /** + * @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) internal view returns (uint256[] memory) { + address[] memory path = new address[](2); + path[0] = reserveIn; + path[1] = reserveOut; + + return UNISWAP_ROUTER.getAmountsIn(amountOut, path); + } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index c42df484..9c741745 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -76,6 +76,40 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { return true; } + function swapAndRepay( + address collateralAsset, + address debtAsset, + uint256 collateralAmount, + uint256 debtRepayAmount, + uint256 debtRateMode, + PermitSignature calldata permitSignature + ) external { + ReserveLogic.ReserveData memory reserveData = _getReserveData(collateralAsset); + + if (collateralAsset != debtAsset) { + // Get exact collateral needed for the swap to avoid leftovers + uint256[] memory amounts = _getAmountsIn(collateralAsset, debtAsset, debtRepayAmount); + require(amounts[0] <= collateralAmount, 'slippage too high'); + + // Pull aTokens from user + _pullAToken(collateralAsset, reserveData.aTokenAddress, msg.sender, amounts[0], permitSignature); + + // Swap collateral for debt asset + _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], debtRepayAmount); + } else { + // Pull aTokens from user + _pullAToken(collateralAsset, reserveData.aTokenAddress, msg.sender, debtRepayAmount, permitSignature); + } + + // Repay debt + IERC20(debtAsset).approve(address(POOL), debtRepayAmount); + POOL.repay(debtAsset, debtRepayAmount, debtRateMode, msg.sender); + + // In the case the repay amount provided exceeded the actual debt, send the leftovers to the user + _sendLeftovers(debtAsset, msg.sender); + } + + /** * @dev Perform the repay of the debt, pulls the initiator collateral and swaps to repay the flash loan * From 4d2d9e8459ca21b435d54a11ce88ca168ec1b92a Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Tue, 24 Nov 2020 14:06:58 -0300 Subject: [PATCH 29/34] Avoid leftovers on swapAndRepay --- contracts/adapters/UniswapRepayAdapter.sol | 32 ++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 9c741745..e8d319bb 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -84,29 +84,39 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 debtRateMode, PermitSignature calldata permitSignature ) external { - ReserveLogic.ReserveData memory reserveData = _getReserveData(collateralAsset); + ReserveLogic.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + ReserveLogic.ReserveData memory debtReserveData = _getReserveData(debtAsset); + + address debtToken = ReserveLogic.InterestRateMode(debtRateMode) == ReserveLogic.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, debtRepayAmount); - require(amounts[0] <= collateralAmount, 'slippage too high'); + uint256[] memory amounts = _getAmountsIn(collateralAsset, debtAsset, amountToRepay); + require(amounts[0] <= maxCollateralToSwap, 'slippage too high'); // Pull aTokens from user - _pullAToken(collateralAsset, reserveData.aTokenAddress, msg.sender, amounts[0], permitSignature); + _pullAToken(collateralAsset, collateralReserveData.aTokenAddress, msg.sender, amounts[0], permitSignature); // Swap collateral for debt asset - _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], debtRepayAmount); + _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], amountToRepay); } else { // Pull aTokens from user - _pullAToken(collateralAsset, reserveData.aTokenAddress, msg.sender, debtRepayAmount, permitSignature); + _pullAToken(collateralAsset, collateralReserveData.aTokenAddress, msg.sender, amountToRepay, permitSignature); } // Repay debt - IERC20(debtAsset).approve(address(POOL), debtRepayAmount); - POOL.repay(debtAsset, debtRepayAmount, debtRateMode, msg.sender); - - // In the case the repay amount provided exceeded the actual debt, send the leftovers to the user - _sendLeftovers(debtAsset, msg.sender); + IERC20(debtAsset).approve(address(POOL), amountToRepay); + POOL.repay(debtAsset, amountToRepay, debtRateMode, msg.sender); } From a496be8833cb8413b8cffed391f26f19a898d0e3 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Wed, 25 Nov 2020 10:44:50 -0300 Subject: [PATCH 30/34] Refactor to avoid leftovers on _swapAndRepay with flash loan --- contracts/adapters/UniswapRepayAdapter.sol | 68 +++++++++++-------- .../mocks/swap/MockUniswapV2Router02.sol | 9 ++- test/uniswapAdapters.spec.ts | 56 ++++++++++++--- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index e8d319bb..005228b9 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -123,8 +123,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { /** * @dev Perform the repay of the debt, pulls the initiator collateral and swaps to repay the flash loan * - * @param assetFrom Address of token to be swapped - * @param assetTo Address of debt token to be received from the swap + * @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 @@ -133,8 +133,8 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @param permitSignature struct containing the permit signature */ function _swapAndRepay( - address assetFrom, - address assetTo, + address collateralAsset, + address debtAsset, uint256 amount, uint256 collateralAmount, uint256 rateMode, @@ -142,25 +142,48 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 premium, PermitSignature memory permitSignature ) internal { + ReserveLogic.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + // Repay debt - IERC20(assetTo).approve(address(POOL), amount); - POOL.repay(assetTo, amount, rateMode, initiator); - uint256 debtRepayLeftovers = IERC20(assetTo).balanceOf(address(this)); + IERC20(debtAsset).approve(address(POOL), amount); + uint256 repaidAmount = IERC20(debtAsset).balanceOf(address(this)); + POOL.repay(debtAsset, amount, rateMode, initiator); + repaidAmount = repaidAmount.sub(IERC20(debtAsset).balanceOf(address(this))); - uint256 flashLoanDebt = amount.add(premium); - uint256 neededForFlashLoanDebt = flashLoanDebt.sub(debtRepayLeftovers); + if (collateralAsset != debtAsset) { + uint256 maxCollateralToSwap = collateralAmount; + if (repaidAmount < amount) { + maxCollateralToSwap = maxCollateralToSwap.mul(repaidAmount).div(amount); + } - // Pull aTokens from user - ReserveLogic.ReserveData memory reserveData = _getReserveData(assetFrom); - _pullAToken(assetFrom, reserveData.aTokenAddress, initiator, collateralAmount, permitSignature); + uint256 neededForFlashLoanDebt = repaidAmount.add(premium); + uint256[] memory amounts = _getAmountsIn(collateralAsset, debtAsset, neededForFlashLoanDebt); + require(amounts[0] <= maxCollateralToSwap, 'slippage too high'); - uint256 amountSwapped = _swapTokensForExactTokens(assetFrom, assetTo, collateralAmount, neededForFlashLoanDebt); + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + initiator, + amounts[0], + permitSignature + ); - // Send collateral leftovers from swap to the user - _sendLeftovers(assetFrom, initiator); + // Swap collateral asset to the debt asset + _swapTokensForExactTokens(collateralAsset, debtAsset, amounts[0], neededForFlashLoanDebt); + } else { + // Pull aTokens from user + _pullAToken( + collateralAsset, + collateralReserveData.aTokenAddress, + initiator, + repaidAmount.add(premium), + permitSignature + ); + } // Repay flashloan - IERC20(assetTo).approve(address(POOL), flashLoanDebt); + IERC20(debtAsset).approve(address(POOL), amount.add(premium)); } /** @@ -201,17 +224,4 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ) ); } - - /** - * @dev Transfers the balance of the adapter to the user, as there shouldn't be any leftover in the adapter - * @param asset address of the asset - * @param user address - */ - function _sendLeftovers(address asset, address user) internal { - uint256 assetLeftover = IERC20(asset).balanceOf(address(this)); - - if (assetLeftover > 0) { - IERC20(asset).transfer(user, assetLeftover); - } - } } diff --git a/contracts/mocks/swap/MockUniswapV2Router02.sol b/contracts/mocks/swap/MockUniswapV2Router02.sol index 6d771cda..2c94050c 100644 --- a/contracts/mocks/swap/MockUniswapV2Router02.sol +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -10,6 +10,7 @@ contract MockUniswapV2Router02 is IUniswapV2Router02 { 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; @@ -61,16 +62,20 @@ contract MockUniswapV2Router02 is IUniswapV2Router02 { _amountsIn[reserveIn][reserveOut][amountOut] = amountIn; } + function setDefaultMockValue(uint value) public { + defaultMockValue = value; + } + function getAmountsOut(uint amountIn, address[] calldata path) external view override returns (uint[] memory) { uint256[] memory amounts = new uint256[](2); amounts[0] = amountIn; - amounts[1] = _amountsOut[path[0]][path[1]][amountIn]; + amounts[1] = _amountsOut[path[0]][path[1]][amountIn] > 0 ? _amountsOut[path[0]][path[1]][amountIn] : defaultMockValue; return amounts; } function getAmountsIn(uint amountOut, address[] calldata path) external view override returns (uint[] memory) { uint256[] memory amounts = new uint256[](2); - amounts[0] = _amountsIn[path[0]][path[1]][amountOut]; + amounts[0] = _amountsIn[path[0]][path[1]][amountOut] > 0 ? _amountsIn[path[0]][path[1]][amountOut] : defaultMockValue; amounts[1] = amountOut; return amounts; } diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index b821f238..eb3d6cc5 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -2091,6 +2091,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .multipliedBy(1.0009) .toFixed(0); + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + liquidityToSwap + ); + const params = buildRepayAdapterParams( weth.address, liquidityToSwap, @@ -2198,6 +2205,13 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .multipliedBy(1.0009) .toFixed(0); + await mockUniswapRouter.setAmountIn( + flashLoanDebt, + weth.address, + dai.address, + liquidityToSwap + ); + const params = buildRepayAdapterParams( weth.address, liquidityToSwap, @@ -2401,6 +2415,17 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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, @@ -2472,14 +2497,19 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .multipliedBy(0.995) .toFixed(0); - const leftOverWeth = new BigNumber(liquidityToSwap.toString()).minus(actualWEthSwapped); - 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, @@ -2520,8 +2550,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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(liquidityToSwap)); - expect(userWethBalance).to.be.gte(userWethBalanceBefore.add(leftOverWeth.toString())); + 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 with no leftovers', async () => { @@ -2560,7 +2590,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { const userDaiStableDebtAmountBefore = await daiStableDebtContract.balanceOf(userAddress); - const liquidityToSwap = amountWETHtoSwap; + // 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); @@ -2569,7 +2603,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .multipliedBy(1.1) .toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); const params = buildRepayAdapterParams( weth.address, @@ -2647,7 +2682,11 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { userAddress ); - const liquidityToSwap = amountWETHtoSwap; + // 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); @@ -2656,7 +2695,8 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { .multipliedBy(1.1) .toFixed(0); - await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, liquidityToSwap); + await mockUniswapRouter.connect(user).setAmountToSwap(weth.address, amountWETHtoSwap); + await mockUniswapRouter.setDefaultMockValue(amountWETHtoSwap); const params = buildRepayAdapterParams( weth.address, From cdd922c9084858e7862f37bd4de62bca0e05f3af Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Wed, 25 Nov 2020 11:07:33 -0300 Subject: [PATCH 31/34] Merge fixes --- contracts/adapters/BaseUniswapAdapter.sol | 8 ++++---- contracts/adapters/UniswapLiquiditySwapAdapter.sol | 2 +- contracts/adapters/UniswapRepayAdapter.sol | 12 ++++++------ contracts/interfaces/IERC20WithPermit.sol | 2 +- contracts/interfaces/IUniswapV2Router02.sol | 2 +- contracts/mocks/swap/MockUniswapV2Router02.sol | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/adapters/BaseUniswapAdapter.sol b/contracts/adapters/BaseUniswapAdapter.sol index 0e4ca193..718b8f24 100644 --- a/contracts/adapters/BaseUniswapAdapter.sol +++ b/contracts/adapters/BaseUniswapAdapter.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; pragma experimental ABIEncoderV2; -import {PercentageMath} from '../libraries/math/PercentageMath.sol'; +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 {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol'; import {ILendingPool} from '../interfaces/ILendingPool.sol'; -import {ReserveLogic} from '../libraries/logic/ReserveLogic.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'; @@ -214,7 +214,7 @@ contract BaseUniswapAdapter { * @dev Get the aToken associated to the asset * @return address of the aToken */ - function _getReserveData(address asset) internal view returns (ReserveLogic.ReserveData memory) { + function _getReserveData(address asset) internal view returns (DataTypes.ReserveData memory) { return POOL.getReserveData(asset); } diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 059170f9..088fd411 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 005228b9..59aff05b 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; pragma experimental ABIEncoderV2; import {BaseUniswapAdapter} from './BaseUniswapAdapter.sol'; @@ -7,7 +7,7 @@ import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddresses import {IUniswapV2Router02} from '../interfaces/IUniswapV2Router02.sol'; import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; -import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol'; +import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; /** * @title UniswapRepayAdapter @@ -84,10 +84,10 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 debtRateMode, PermitSignature calldata permitSignature ) external { - ReserveLogic.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); - ReserveLogic.ReserveData memory debtReserveData = _getReserveData(debtAsset); + DataTypes.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + DataTypes.ReserveData memory debtReserveData = _getReserveData(debtAsset); - address debtToken = ReserveLogic.InterestRateMode(debtRateMode) == ReserveLogic.InterestRateMode.STABLE + address debtToken = DataTypes.InterestRateMode(debtRateMode) == DataTypes.InterestRateMode.STABLE ? debtReserveData.stableDebtTokenAddress : debtReserveData.variableDebtTokenAddress; @@ -142,7 +142,7 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { uint256 premium, PermitSignature memory permitSignature ) internal { - ReserveLogic.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); + DataTypes.ReserveData memory collateralReserveData = _getReserveData(collateralAsset); // Repay debt IERC20(debtAsset).approve(address(POOL), amount); diff --git a/contracts/interfaces/IERC20WithPermit.sol b/contracts/interfaces/IERC20WithPermit.sol index 448b383b..46466b90 100644 --- a/contracts/interfaces/IERC20WithPermit.sol +++ b/contracts/interfaces/IERC20WithPermit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; diff --git a/contracts/interfaces/IUniswapV2Router02.sol b/contracts/interfaces/IUniswapV2Router02.sol index 6453e74a..af0f8280 100644 --- a/contracts/interfaces/IUniswapV2Router02.sol +++ b/contracts/interfaces/IUniswapV2Router02.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; interface IUniswapV2Router02 { function swapExactTokensForTokens( diff --git a/contracts/mocks/swap/MockUniswapV2Router02.sol b/contracts/mocks/swap/MockUniswapV2Router02.sol index 2c94050c..7f38bf92 100644 --- a/contracts/mocks/swap/MockUniswapV2Router02.sol +++ b/contracts/mocks/swap/MockUniswapV2Router02.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: agpl-3.0 -pragma solidity ^0.6.8; +pragma solidity 0.6.12; import {IUniswapV2Router02} from "../../interfaces/IUniswapV2Router02.sol"; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; From bc969d926e27c6568d1967635529a482b0e5657d Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Wed, 25 Nov 2020 14:55:46 -0300 Subject: [PATCH 32/34] Add swapAndRepay unit tests --- test/uniswapAdapters.spec.ts | 533 ++++++++++++++++++++++++++++++++--- 1 file changed, 495 insertions(+), 38 deletions(-) diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index eb3d6cc5..f65f2402 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -1996,6 +1996,42 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); 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 } = testEnv; @@ -2009,42 +2045,6 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { }); describe('executeOperation', () => { - 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); - }); - it('should correctly swap tokens and repay debt', async () => { const { users, @@ -2554,7 +2554,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userWethBalance).to.be.eq(userWethBalanceBefore); }); - it('should correctly swap tokens and repay the whole stable debt with no leftovers', async () => { + it('should correctly swap tokens and repay the whole stable debt', async () => { const { users, pool, @@ -2644,7 +2644,7 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); - it('should correctly swap tokens and repay the whole variable debt with no leftovers', async () => { + it('should correctly swap tokens and repay the whole variable debt', async () => { const { users, pool, @@ -2736,5 +2736,462 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { expect(userAEthBalance).to.be.gte(userAEthBalanceBefore.sub(liquidityToSwap)); }); }); + + 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', + }); + + 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, + }); + + 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', + }) + ).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', + }) + ).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', + }); + + 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', + }); + + 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', + }); + + 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)); + }); + }); }); }); From 3415204216dca7a5a3b0965f9f7d47a25a3db942 Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Wed, 25 Nov 2020 15:49:11 -0300 Subject: [PATCH 33/34] Update adapter docs --- .../adapters/UniswapLiquiditySwapAdapter.sol | 34 +++++++++++++------ contracts/adapters/UniswapRepayAdapter.sol | 26 ++++++++++---- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/contracts/adapters/UniswapLiquiditySwapAdapter.sol b/contracts/adapters/UniswapLiquiditySwapAdapter.sol index 088fd411..8b325499 100644 --- a/contracts/adapters/UniswapLiquiditySwapAdapter.sol +++ b/contracts/adapters/UniswapLiquiditySwapAdapter.sol @@ -10,7 +10,7 @@ import {IERC20} from '../dependencies/openzeppelin/contracts/IERC20.sol'; /** * @title UniswapLiquiditySwapAdapter - * @notice Uniswap V2 Adapter to swap liquidity using a flash loan. + * @notice Uniswap V2 Adapter to swap liquidity. * @author Aave **/ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { @@ -39,12 +39,12 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { {} /** - * @dev Swaps the received reserve amount from the flashloan into the asset specified in the params. + * @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 to be swapped - * @param amounts Amount of the reserve to be swapped + * @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: @@ -103,8 +103,9 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { } /** - * @dev Swaps an `amountToSwap` of an asset to another and deposits the funds on behalf of the user without using a flashloan. - * This method can be used when the user has no debts. + * @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 @@ -164,8 +165,8 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { * @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 flashloan - * @param premium Premium of the flashloan + * @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 @@ -203,12 +204,12 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { _pullAToken(assetFrom, aToken, initiator, amountToPull, permitSignature); - // Repay flashloan + // Repay flash loan IERC20(assetFrom).approve(address(POOL), flashLoanDebt); } /** - * @dev Decodes debt information encoded in flashloan params + * @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 @@ -232,6 +233,17 @@ contract UniswapLiquiditySwapAdapter is BaseUniswapAdapter, IFlashLoanReceiver { bytes32[] memory s ) = abi.decode(params, (address[], uint256[], bool[], uint256[], uint256[], uint8[], bytes32[], bytes32[])); - return SwapParams(assetToSwapToList, minAmountsToReceive, swapAllBalance, PermitParams(permitAmount, deadline, v, r, s)); + return SwapParams( + assetToSwapToList, + minAmountsToReceive, + swapAllBalance, + PermitParams( + permitAmount, + deadline, + v, + r, + s + ) + ); } } diff --git a/contracts/adapters/UniswapRepayAdapter.sol b/contracts/adapters/UniswapRepayAdapter.sol index 59aff05b..4945edf0 100644 --- a/contracts/adapters/UniswapRepayAdapter.sol +++ b/contracts/adapters/UniswapRepayAdapter.sol @@ -11,7 +11,7 @@ import {DataTypes} from '../protocol/libraries/types/DataTypes.sol'; /** * @title UniswapRepayAdapter - * @notice Uniswap V2 Adapter to perform a repay of a debt using a flash loan. + * @notice Uniswap V2 Adapter to perform a repay of a debt with collateral. * @author Aave **/ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { @@ -33,18 +33,18 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { /** * @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 repay the flash loan. + * 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. - * @param assets Address to be swapped - * @param amounts Amount of the reserve to be swapped + * 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 - * RepayMode repayMode Enum indicating the repaid mode * uint256 permitAmount Amount for the permit signature * uint256 deadline Deadline for the permit signature * uint8 v V param for the permit signature @@ -76,6 +76,18 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { 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 + */ function swapAndRepay( address collateralAsset, address debtAsset, @@ -182,12 +194,12 @@ contract UniswapRepayAdapter is BaseUniswapAdapter, IFlashLoanReceiver { ); } - // Repay flashloan + // Repay flash loan IERC20(debtAsset).approve(address(POOL), amount.add(premium)); } /** - * @dev Decodes debt information encoded in flashloan params + * @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 From b41ccf744182f3219be31e8a3981dd7187e19dde Mon Sep 17 00:00:00 2001 From: Gerardo Nardelli Date: Thu, 26 Nov 2020 12:42:42 -0300 Subject: [PATCH 34/34] Add repay with same asset unit test --- test/uniswapAdapters.spec.ts | 137 +++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/test/uniswapAdapters.spec.ts b/test/uniswapAdapters.spec.ts index f65f2402..e5dcb7f5 100644 --- a/test/uniswapAdapters.spec.ts +++ b/test/uniswapAdapters.spec.ts @@ -2735,6 +2735,79 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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' + ); + + 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', () => { @@ -3192,6 +3265,70 @@ makeSuite('Uniswap adapters', (testEnv: TestEnv) => { 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('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); + + 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', + } + ); + + 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); + }); }); }); });