diff --git a/contracts/mocks/flashloan/MockSwapAdapter.sol b/contracts/mocks/flashloan/MockSwapAdapter.sol new file mode 100644 index 00000000..75a7ff5d --- /dev/null +++ b/contracts/mocks/flashloan/MockSwapAdapter.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {MintableERC20} from '../tokens/MintableERC20.sol'; +import {ILendingPoolAddressesProvider} from '../../interfaces/ILendingPoolAddressesProvider.sol'; +import {ISwapAdapter} from '../../interfaces/ISwapAdapter.sol'; + +contract MockSwapAdapter is ISwapAdapter { + uint256 amountToReturn; + ILendingPoolAddressesProvider public addressesProvider; + + event Swapped(address fromAsset, address toAsset, uint256 fromAmount, uint256 receivedAmount); + + constructor(ILendingPoolAddressesProvider provider) public { + addressesProvider = provider; + } + + function setAmountToReturn(uint256 amount) public { + amountToReturn = amount; + } + + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external override { + params; + IERC20(assetToSwapFrom).transfer(address(1), amountToSwap); // We don't want to keep funds here + MintableERC20(assetToSwapTo).mint(amountToReturn); + IERC20(assetToSwapTo).approve(fundsDestination, amountToReturn); + + emit Swapped(assetToSwapFrom, assetToSwapTo, amountToSwap, amountToReturn); + } + + function burnAsset(IERC20 asset, uint256 amount) public { + uint256 amountToBurn = (amount == type(uint256).max) ? asset.balanceOf(address(this)) : amount; + asset.transfer(address(0), amountToBurn); + } +} diff --git a/deployed-contracts.json b/deployed-contracts.json index d04816b9..4af861a4 100644 --- a/deployed-contracts.json +++ b/deployed-contracts.json @@ -174,7 +174,7 @@ }, "WalletBalanceProvider": { "buidlerevm": { - "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2", + "address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -414,7 +414,7 @@ }, "AaveProtocolTestHelpers": { "buidlerevm": { - "address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10" + "address": "0x2cfcA5785261fbC88EFFDd46fCFc04c22525F9e4" }, "localhost": { "address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10" @@ -489,5 +489,10 @@ "address": "0xA8083d78B6ABC328b4d3B714F76F384eCC7147e1", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } + }, + "MockSwapAdapter": { + "buidlerevm": { + "address": "0xBEF0d4b9c089a5883741fC14cbA352055f35DDA2" + } } } \ No newline at end of file diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index b5b483d3..0d51f491 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -31,6 +31,7 @@ import BigNumber from 'bignumber.js'; import {Ierc20Detailed} from '../types/Ierc20Detailed'; import {StableDebtToken} from '../types/StableDebtToken'; import {VariableDebtToken} from '../types/VariableDebtToken'; +import {MockSwapAdapter} from '../types/MockSwapAdapter'; export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => { const currentNetwork = BRE.network.name; @@ -212,6 +213,9 @@ export const deployMockFlashLoanReceiver = async (addressesProvider: tEthereumAd addressesProvider, ]); +export const deployMockSwapAdapter = async (addressesProvider: tEthereumAddress) => + await deployContract(eContractid.MockSwapAdapter, [addressesProvider]); + export const deployWalletBalancerProvider = async (addressesProvider: tEthereumAddress) => await deployContract(eContractid.WalletBalanceProvider, [ addressesProvider, @@ -387,6 +391,14 @@ export const getMockFlashLoanReceiver = async (address?: tEthereumAddress) => { ); }; +export const getMockSwapAdapter = async (address?: tEthereumAddress) => { + return await getContract( + eContractid.MockSwapAdapter, + address || + (await getDb().get(`${eContractid.MockSwapAdapter}.${BRE.network.name}`).value()).address + ); +}; + export const getLendingRateOracle = async (address?: tEthereumAddress) => { return await getContract( eContractid.LendingRateOracle, diff --git a/helpers/types.ts b/helpers/types.ts index e18ca909..eb8fe198 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -41,6 +41,7 @@ export enum eContractid { IERC20Detailed = 'IERC20Detailed', StableDebtToken = 'StableDebtToken', VariableDebtToken = 'VariableDebtToken', + MockSwapAdapter = 'MockSwapAdapter', } export enum ProtocolErrors { @@ -111,7 +112,7 @@ export enum ProtocolErrors { INVALID_REDIRECTION_ADDRESS = 'Invalid redirection address', INVALID_HF = 'Invalid health factor', TRANSFER_AMOUNT_EXCEEDS_BALANCE = 'ERC20: transfer amount exceeds balance', - SAFEERC20_LOWLEVEL_CALL = 'SafeERC20: low-level call failed' + SAFEERC20_LOWLEVEL_CALL = 'SafeERC20: low-level call failed', } export type tEthereumAddress = string; diff --git a/test/__setup.spec.ts b/test/__setup.spec.ts index 342b7b64..2d0dc07c 100644 --- a/test/__setup.spec.ts +++ b/test/__setup.spec.ts @@ -23,6 +23,7 @@ import { deployStableDebtToken, deployVariableDebtToken, deployGenericAToken, + deployMockSwapAdapter, } from '../helpers/contracts-helpers'; import {LendingPoolAddressesProvider} from '../types/LendingPoolAddressesProvider'; import {ContractTransaction, Signer} from 'ethers'; @@ -503,6 +504,9 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => { const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address); await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address); + const mockMockSwapAdapter = await deployMockSwapAdapter(addressesProvider.address); + await insertContractAddressInDb(eContractid.MockSwapAdapter, mockMockSwapAdapter.address); + await deployWalletBalancerProvider(addressesProvider.address); const testHelpers = await deployAaveProtocolTestHelpers(addressesProvider.address); diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts new file mode 100644 index 00000000..dd3d879f --- /dev/null +++ b/test/collateral-swap.spec.ts @@ -0,0 +1,111 @@ +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {MockSwapAdapter} from '../types/MockSwapAdapter'; +import {getMockSwapAdapter} from '../helpers/contracts-helpers'; +import {ProtocolErrors} from '../helpers/types'; +import {ethers} from 'ethers'; +import {APPROVAL_AMOUNT_LENDING_POOL} from '../helpers/constants'; + +const {expect} = require('chai'); + +makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { + let _mockSwapAdapter = {} as MockSwapAdapter; + const {HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD} = ProtocolErrors; + + before(async () => { + _mockSwapAdapter = await getMockSwapAdapter(); + }); + + it('Deposits WETH into the reserve', async () => { + const {pool, weth, users} = testEnv; + const amountToDeposit = ethers.utils.parseEther('1'); + + for (const signer of [weth.signer, users[2].signer]) { + const connectedWETH = weth.connect(signer); + await connectedWETH.mint(amountToDeposit); + await connectedWETH.approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); + await pool.connect(signer).deposit(weth.address, amountToDeposit, '0'); + } + }); + it('User tries to swap more then he can', async () => { + const {pool, weth, dai} = testEnv; + await expect( + pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + dai.address, + ethers.utils.parseEther('1.1'), + '0x10' + ) + ).to.be.revertedWith('ERC20: burn amount exceeds balance'); + }); + + it('User tries to swap more then available on the reserve', async () => { + const {pool, weth, dai, users, aEth} = testEnv; + + await pool.borrow(weth.address, ethers.utils.parseEther('0.1'), 1, 0); + await pool.connect(users[2].signer).withdraw(weth.address, ethers.utils.parseEther('1')); + + await expect( + pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + dai.address, + ethers.utils.parseEther('1'), + '0x10' + ) + ).to.be.revertedWith('SafeMath: subtraction overflow'); + await weth.mint(ethers.utils.parseEther('0.1')); + await pool.repay( + weth.address, + ethers.utils.parseEther('0.2'), + 1, + await pool.signer.getAddress() + ); + }); + + it('User tries to swap correct amount', async () => { + const {pool, weth, dai, aEth, aDai} = testEnv; + const userAddress = await pool.signer.getAddress(); + const amountToSwap = ethers.utils.parseEther('0.25'); + + const amountToReturn = ethers.utils.parseEther('0.5'); + await _mockSwapAdapter.setAmountToReturn(amountToReturn); + + const reserveBalanceWETHBefore = await weth.balanceOf(aEth.address); + const reserveBalanceDAIBefore = await dai.balanceOf(aDai.address); + + const userATokenBalanceWETHBefore = await aEth.balanceOf(userAddress); + const userATokenBalanceDAIBefore = await aDai.balanceOf(userAddress); + + await pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + dai.address, + amountToSwap, + '0x10' + ); + const userATokenBalanceWETHAfter = await aEth.balanceOf(userAddress); + const userATokenBalanceDAIAfter = await aDai.balanceOf(userAddress); + + const reserveBalanceWETHAfter = await weth.balanceOf(aEth.address); + const reserveBalanceDAIAfter = await dai.balanceOf(aDai.address); + + expect(userATokenBalanceWETHAfter.toString()).to.be.equal( + userATokenBalanceWETHBefore.sub(amountToSwap).toString(), + 'was burned incorrect amount of user funds' + ); + expect(userATokenBalanceDAIAfter.toString()).to.be.equal( + userATokenBalanceDAIBefore.add(amountToReturn).toString(), + 'was minted incorrect amount of user funds' + ); + + expect(reserveBalanceWETHAfter.toString()).to.be.equal( + reserveBalanceWETHBefore.sub(amountToSwap).toString(), + 'was sent incorrect amount if reserve funds' + ); + expect(reserveBalanceDAIAfter.toString()).to.be.equal( + reserveBalanceDAIBefore.add(amountToReturn).toString(), + 'was received incorrect amount if reserve funds' + ); + }); +}); diff --git a/test/flashloan.spec.ts b/test/flashloan.spec.ts index 79db4bcb..329eccda 100644 --- a/test/flashloan.spec.ts +++ b/test/flashloan.spec.ts @@ -1,3 +1,5 @@ +import BigNumber from 'bignumber.js'; + import {TestEnv, makeSuite} from './helpers/make-suite'; import {APPROVAL_AMOUNT_LENDING_POOL, oneRay} from '../helpers/constants'; import { @@ -8,7 +10,6 @@ import { import {ethers} from 'ethers'; import {MockFlashLoanReceiver} from '../types/MockFlashLoanReceiver'; import {ProtocolErrors, eContractid} from '../helpers/types'; -import BigNumber from 'bignumber.js'; import {VariableDebtToken} from '../types/VariableDebtToken'; import {StableDebtToken} from '../types/StableDebtToken'; @@ -21,14 +22,14 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { REQUESTED_AMOUNT_TOO_SMALL, TRANSFER_AMOUNT_EXCEEDS_BALANCE, INVALID_FLASHLOAN_MODE, - SAFEERC20_LOWLEVEL_CALL + SAFEERC20_LOWLEVEL_CALL, } = ProtocolErrors; before(async () => { _mockFlashLoanReceiver = await getMockFlashLoanReceiver(); }); - it('Deposits ETH into the reserve', async () => { + it('Deposits WETH into the reserve', async () => { const {pool, weth} = testEnv; const amountToDeposit = ethers.utils.parseEther('1'); @@ -340,8 +341,8 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { await _mockFlashLoanReceiver.setFailExecutionTransfer(true); await pool - .connect(caller.signer) - .flashLoan(_mockFlashLoanReceiver.address, weth.address, flashAmount, 1, '0x10', '0'); + .connect(caller.signer) + .flashLoan(_mockFlashLoanReceiver.address, weth.address, flashAmount, 1, '0x10', '0'); const {stableDebtTokenAddress} = await pool.getReserveTokensAddresses(weth.address); @@ -353,6 +354,5 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => { const callerDebt = await wethDebtToken.balanceOf(caller.address); expect(callerDebt.toString()).to.be.equal('800720000000000000', 'Invalid user debt'); - }); });