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<MockUniswapV2Router02>(eContractid.MockUniswapV2Router02, []); + +export const deployUniswapLiquiditySwapAdapter = async ( + addressesProvider: tEthereumAddress, + uniswapRouter: tEthereumAddress +) => + await deployContract<UniswapLiquiditySwapAdapter>(eContractid.UniswapLiquiditySwapAdapter, [ + addressesProvider, + uniswapRouter, + ]); + +export const deployUniswapRepayAdapter = async ( + addressesProvider: tEthereumAddress, + uniswapRouter: tEthereumAddress +) => + await deployContract<UniswapRepayAdapter>(eContractid.UniswapRepayAdapter, [ + addressesProvider, + uniswapRouter, + ]); + export const getChainlingProxyPriceProvider = async (address?: tEthereumAddress) => await getContract<MockAggregator>( eContractid.ChainlinkProxyPriceProvider, @@ -321,8 +344,6 @@ export const deployWalletBalancerProvider = async ( } return instance; }; -export const deployMockSwapAdapter = async (addressesProvider: tEthereumAddress) => - await deployContract<MockSwapAdapter>(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<MockSwapAdapter>( - eContractid.MockSwapAdapter, +export const getMockUniswapRouter = async (address?: tEthereumAddress) => { + return await getContract<MockUniswapV2Router02>( + 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<UniswapLiquiditySwapAdapter>( + eContractid.UniswapLiquiditySwapAdapter, + address || + (await getDb().get(`${eContractid.UniswapLiquiditySwapAdapter}.${BRE.network.name}`).value()) + .address + ); +}; + +export const getUniswapRepayAdapter = async (address?: tEthereumAddress) => { + return await getContract<UniswapLiquiditySwapAdapter>( + 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<AToken>(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<StableDebtToken>( + 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<StableDebtToken>( + 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<StableDebtToken>( + 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())); + }); + }); + }); +});