From 5c28bf5a490d81a084364cbe80da8e6d64f2914e Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 12:46:11 +0300 Subject: [PATCH 01/15] initial implementation of collateral swap method --- contracts/interfaces/ILendingPool.sol | 8 +++ contracts/lendingpool/LendingPool.sol | 74 ++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index a7a5e1ca..1522c8f9 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -237,6 +237,14 @@ interface ILendingPool { uint16 referralCode ) external; + function collateralSwap( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external; + /** * @dev accessory functions to fetch data from the core contract **/ diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 9e4e6707..b0100335 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -450,15 +450,13 @@ contract LendingPool is VersionedInitializable, ILendingPool { vars.amountPlusPremium = amount.add(vars.premium); if (debtMode == ReserveLogic.InterestRateMode.NONE) { - IERC20(asset).transferFrom(receiverAddress, vars.aTokenAddress, vars.amountPlusPremium); - + reserve.updateCumulativeIndexesAndTimestamp(); reserve.cumulateToLiquidityIndex(IERC20(vars.aTokenAddress).totalSupply(), vars.premium); reserve.updateInterestRates(asset, vars.aTokenAddress, vars.premium, 0); - - emit FlashLoan(receiverAddress, asset, amount, vars.premium, referralCode); + emit FlashLoan(receiverAddress, asset, amount, vars.premium, referralCode); } else { // If the transfer didn't succeed, the receiver either didn't return the funds, or didn't approve the transfer. _executeBorrow( @@ -475,6 +473,56 @@ contract LendingPool is VersionedInitializable, ILendingPool { } } + function collateralSwap( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external override { + ReserveLogic.ReserveData storage fromReserve = _reserves[fromAsset]; + ReserveLogic.ReserveData storage toReserve = _reserves[toAsset]; + IAToken fromReserveAToken = IAToken(fromReserve.aTokenAddress); + IAToken toReserveAToken = IAToken(toReserve.aTokenAddress); + + fromReserve.updateCumulativeIndexesAndTimestamp(); + toReserve.updateCumulativeIndexesAndTimestamp(); + + // get user position + uint256 userBalance = fromReserveAToken.balanceOf(msg.sender); + require(userBalance >= amountToSwap, 'not enough collateral'); + if (userBalance == amountToSwap) { + _usersConfig[msg.sender].setUsingAsCollateral(fromReserve.index, false); + } + + fromReserve.updateInterestRates(fromAsset, address(fromReserveAToken), 0, amountToSwap); + + fromReserveAToken.burn(msg.sender, receiverAddress, amountToSwap); + // Notifies the receiver to proceed, sending as param the underlying already transferred + IFlashLoanReceiver(receiverAddress).executeOperation( + fromAsset, + // toAsset, + amountToSwap, + 0, + params + ); + + uint256 amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); + IERC20(toAsset).transferFrom(receiverAddress, address(toReserveAToken), amountToReceive); + toReserveAToken.mint(msg.sender, amountToReceive); + toReserve.updateInterestRates(toAsset, address(toReserveAToken), amountToReceive, 0); + + (, , , , uint256 healthFactor) = GenericLogic.calculateUserAccountData( + msg.sender, + _reserves, + _usersConfig[msg.sender], + _reservesList, + _addressesProvider.getPriceOracle() + ); + + require(healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, 'low hf'); + } + /** * @dev accessory functions to fetch data from the core contract **/ @@ -728,13 +776,11 @@ contract LendingPool is VersionedInitializable, ILendingPool { oracle ); - uint256 reserveIndex = reserve.index; if (!userConfig.isBorrowing(reserveIndex)) { userConfig.setBorrowing(reserveIndex, true); } - reserve.updateCumulativeIndexesAndTimestamp(); //caching the current stable borrow rate @@ -754,13 +800,17 @@ contract LendingPool is VersionedInitializable, ILendingPool { IVariableDebtToken(reserve.variableDebtTokenAddress).mint(vars.user, vars.amount); } - reserve.updateInterestRates(vars.asset, vars.aTokenAddress, 0, vars.releaseUnderlying ? vars.amount : 0); - - if(vars.releaseUnderlying){ - IAToken(vars.aTokenAddress).transferUnderlyingTo(msg.sender, vars.amount); + reserve.updateInterestRates( + vars.asset, + vars.aTokenAddress, + 0, + vars.releaseUnderlying ? vars.amount : 0 + ); + + if (vars.releaseUnderlying) { + IAToken(vars.aTokenAddress).transferUnderlyingTo(msg.sender, vars.amount); } - - + emit Borrow( vars.asset, msg.sender, From f1d3b8c9d64738816d8a13c6b65dbb7eb0563817 Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 12:54:38 +0300 Subject: [PATCH 02/15] add ISwapAdapter interface --- contracts/interfaces/ISwapAdapter.sol | 20 ++++++++++++++++++++ contracts/lendingpool/LendingPool.sol | 7 ++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 contracts/interfaces/ISwapAdapter.sol diff --git a/contracts/interfaces/ISwapAdapter.sol b/contracts/interfaces/ISwapAdapter.sol new file mode 100644 index 00000000..ed91f95f --- /dev/null +++ b/contracts/interfaces/ISwapAdapter.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: agpl-3.0 +pragma solidity ^0.6.8; + +interface ISwapAdapter { + /** + * @dev Swaps an `amountToSwap` of an asset to another, approving a `fundsDestination` to pull the funds + * @param assetToSwapFrom Origin asset + * @param assetToSwapTo Destination asset + * @param amountToSwap How much `assetToSwapFrom` needs to be swapped + * @param fundsDestination Address that will be pulling the swapped funds + * @param params Additional variadic field to include extra params + */ + function executeOperation( + address assetToSwapFrom, + address assetToSwapTo, + uint256 amountToSwap, + address fundsDestination, + bytes calldata params + ) external; +} diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index b0100335..cde02e10 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -20,6 +20,7 @@ import {UserConfiguration} from '../libraries/configuration/UserConfiguration.so import {IStableDebtToken} from '../tokenization/interfaces/IStableDebtToken.sol'; import {IVariableDebtToken} from '../tokenization/interfaces/IVariableDebtToken.sol'; import {IFlashLoanReceiver} from '../flashloan/interfaces/IFlashLoanReceiver.sol'; +import {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; import {LendingPoolLiquidationManager} from './LendingPoolLiquidationManager.sol'; import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; @@ -499,11 +500,11 @@ contract LendingPool is VersionedInitializable, ILendingPool { fromReserveAToken.burn(msg.sender, receiverAddress, amountToSwap); // Notifies the receiver to proceed, sending as param the underlying already transferred - IFlashLoanReceiver(receiverAddress).executeOperation( + ISwapAdapter(receiverAddress).executeOperation( fromAsset, - // toAsset, + toAsset, amountToSwap, - 0, + address(this), params ); From 5e3b6869b57e01091414a5300174f0a605cf4196 Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 13:00:28 +0300 Subject: [PATCH 03/15] add check of 0 amountToReceive --- contracts/lendingpool/LendingPool.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index cde02e10..cdf5d05f 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -509,9 +509,11 @@ contract LendingPool is VersionedInitializable, ILendingPool { ); uint256 amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); - IERC20(toAsset).transferFrom(receiverAddress, address(toReserveAToken), amountToReceive); - toReserveAToken.mint(msg.sender, amountToReceive); - toReserve.updateInterestRates(toAsset, address(toReserveAToken), amountToReceive, 0); + if (amountToReceive != 0) { + IERC20(toAsset).transferFrom(receiverAddress, address(toReserveAToken), amountToReceive); + toReserveAToken.mint(msg.sender, amountToReceive); + toReserve.updateInterestRates(toAsset, address(toReserveAToken), amountToReceive, 0); + } (, , , , uint256 healthFactor) = GenericLogic.calculateUserAccountData( msg.sender, From 721e73c36e6a5459e41bbca6ed6e545aa977a618 Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 14:41:27 +0300 Subject: [PATCH 04/15] add error from lib --- contracts/lendingpool/LendingPool.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index cdf5d05f..4dcd7aa3 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -491,7 +491,6 @@ contract LendingPool is VersionedInitializable, ILendingPool { // get user position uint256 userBalance = fromReserveAToken.balanceOf(msg.sender); - require(userBalance >= amountToSwap, 'not enough collateral'); if (userBalance == amountToSwap) { _usersConfig[msg.sender].setUsingAsCollateral(fromReserve.index, false); } @@ -523,7 +522,10 @@ contract LendingPool is VersionedInitializable, ILendingPool { _addressesProvider.getPriceOracle() ); - require(healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, 'low hf'); + require( + healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); } /** From 212cf76f2356175a689e84d034d2447ccb4aef02 Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 16:48:46 +0300 Subject: [PATCH 05/15] add base tests --- contracts/mocks/flashloan/MockSwapAdapter.sol | 43 +++++++ deployed-contracts.json | 9 +- helpers/contracts-helpers.ts | 12 ++ helpers/types.ts | 3 +- test/__setup.spec.ts | 4 + test/collateral-swap.spec.ts | 111 ++++++++++++++++++ test/flashloan.spec.ts | 12 +- 7 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 contracts/mocks/flashloan/MockSwapAdapter.sol create mode 100644 test/collateral-swap.spec.ts 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'); - }); }); From aaab81bc150f98eaca1d00dbf697e6244fa077fd Mon Sep 17 00:00:00 2001 From: andyk Date: Mon, 7 Sep 2020 16:59:41 +0300 Subject: [PATCH 06/15] add hf related test --- test/collateral-swap.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index dd3d879f..feb78b63 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -108,4 +108,18 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { 'was received incorrect amount if reserve funds' ); }); + + it('User tries to drop HF below one', async () => { + const {pool, weth, dai} = testEnv; + const amountToSwap = ethers.utils.parseEther('0.3'); + + const amountToReturn = ethers.utils.parseEther('0.5'); + await _mockSwapAdapter.setAmountToReturn(amountToReturn); + + await pool.borrow(weth.address, ethers.utils.parseEther('0.4'), 1, 0); + + await expect( + pool.collateralSwap(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') + ).to.be.revertedWith(HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); + }); }); From 4912f3bb73296214380c07a6dd3230974a2edc68 Mon Sep 17 00:00:00 2001 From: andyk Date: Tue, 8 Sep 2020 13:08:16 +0300 Subject: [PATCH 07/15] update tests calculation logic on swap --- test/collateral-swap.spec.ts | 49 ++++++++++++++++++------------ test/helpers/actions.ts | 4 +-- test/helpers/utils/calculations.ts | 2 +- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index feb78b63..867875a0 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -4,6 +4,9 @@ import {getMockSwapAdapter} from '../helpers/contracts-helpers'; import {ProtocolErrors} from '../helpers/types'; import {ethers} from 'ethers'; import {APPROVAL_AMOUNT_LENDING_POOL} from '../helpers/constants'; +import {getContractsData, getTxCostAndTimestamp} from './helpers/actions'; +import {calcExpectedATokenBalance} from './helpers/utils/calculations'; +import {waitForTx} from './__setup.spec'; const {expect} = require('chai'); @@ -54,13 +57,6 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { '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 () => { @@ -71,19 +67,30 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { const amountToReturn = ethers.utils.parseEther('0.5'); await _mockSwapAdapter.setAmountToReturn(amountToReturn); + const { + reserveData: wethReserveDataBefore, + userData: wethUserDataBefore, + } = await getContractsData(weth.address, userAddress, testEnv); + + const {reserveData: daiReserveDataBefore, userData: daiUserDataBefore} = await getContractsData( + dai.address, + userAddress, + testEnv + ); + 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 txReceipt = await waitForTx( + await pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + dai.address, + amountToSwap, + '0x10' + ) ); + const {txTimestamp} = await getTxCostAndTimestamp(txReceipt); const userATokenBalanceWETHAfter = await aEth.balanceOf(userAddress); const userATokenBalanceDAIAfter = await aDai.balanceOf(userAddress); @@ -91,11 +98,15 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { const reserveBalanceDAIAfter = await dai.balanceOf(aDai.address); expect(userATokenBalanceWETHAfter.toString()).to.be.equal( - userATokenBalanceWETHBefore.sub(amountToSwap).toString(), + calcExpectedATokenBalance(wethReserveDataBefore, wethUserDataBefore, txTimestamp) + .minus(amountToSwap.toString()) + .toString(), 'was burned incorrect amount of user funds' ); expect(userATokenBalanceDAIAfter.toString()).to.be.equal( - userATokenBalanceDAIBefore.add(amountToReturn).toString(), + calcExpectedATokenBalance(daiReserveDataBefore, daiUserDataBefore, txTimestamp) + .plus(amountToReturn.toString()) + .toString(), 'was minted incorrect amount of user funds' ); @@ -116,7 +127,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { const amountToReturn = ethers.utils.parseEther('0.5'); await _mockSwapAdapter.setAmountToReturn(amountToReturn); - await pool.borrow(weth.address, ethers.utils.parseEther('0.4'), 1, 0); + await pool.borrow(weth.address, ethers.utils.parseEther('0.3'), 1, 0); await expect( pool.collateralSwap(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index be0daac5..b9a181e8 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -831,7 +831,7 @@ const getDataBeforeAction = async ( }; }; -const getTxCostAndTimestamp = async (tx: ContractReceipt) => { +export const getTxCostAndTimestamp = async (tx: ContractReceipt) => { if (!tx.blockNumber || !tx.transactionHash || !tx.cumulativeGasUsed) { throw new Error('No tx blocknumber'); } @@ -845,7 +845,7 @@ const getTxCostAndTimestamp = async (tx: ContractReceipt) => { return {txCost, txTimestamp}; }; -const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { +export const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { const {pool} = testEnv; const reserveData = await getReserveData(pool, reserve); const userData = await getUserData(pool, reserve, user); diff --git a/test/helpers/utils/calculations.ts b/test/helpers/utils/calculations.ts index 4ff03b19..d16943d9 100644 --- a/test/helpers/utils/calculations.ts +++ b/test/helpers/utils/calculations.ts @@ -1076,7 +1076,7 @@ const calcExpectedATokenUserIndex = ( return calcExpectedReserveNormalizedIncome(reserveDataBeforeAction, currentTimestamp); }; -const calcExpectedATokenBalance = ( +export const calcExpectedATokenBalance = ( reserveDataBeforeAction: ReserveData, userDataBeforeAction: UserReserveData, currentTimestamp: BigNumber From 77ca4b47f820c531bafada7a5837e431cf7bc70d Mon Sep 17 00:00:00 2001 From: andyk Date: Wed, 9 Sep 2020 12:36:13 +0300 Subject: [PATCH 08/15] add failing test for setUsingAsCollateral --- test/collateral-swap.spec.ts | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index 867875a0..a67cd55e 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -7,6 +7,7 @@ import {APPROVAL_AMOUNT_LENDING_POOL} from '../helpers/constants'; import {getContractsData, getTxCostAndTimestamp} from './helpers/actions'; import {calcExpectedATokenBalance} from './helpers/utils/calculations'; import {waitForTx} from './__setup.spec'; +import {advanceBlock, timeLatest} from '../helpers/misc-utils'; const {expect} = require('chai'); @@ -133,4 +134,46 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { pool.collateralSwap(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') ).to.be.revertedWith(HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); }); + + it('Should set usage as collateral to false if no leftovers after swap', async () => { + const {pool, weth, dai, aEth} = testEnv; + const userAddress = await pool.signer.getAddress(); + + // cleanup borrowings, to be abe to swap whole weth + const amountToRepay = ethers.utils.parseEther('0.2'); + await weth.mint(amountToRepay); + await pool.repay(weth.address, amountToRepay, '1', userAddress); + const txTimestamp = (await timeLatest()).plus(100); + + const { + reserveData: wethReserveDataBefore, + userData: wethUserDataBefore, + } = await getContractsData(weth.address, userAddress, testEnv); + const amountToSwap = calcExpectedATokenBalance( + wethReserveDataBefore, + wethUserDataBefore, + txTimestamp + ); + + await advanceBlock(txTimestamp.toNumber()); + + console.log('before', amountToSwap.toString()); + await pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + dai.address, + '1'.toString(), + '0x10' + ); + console.log('after', (await aEth.balanceOf(userAddress)).toString()); + const {userData: wethUserDataAfter} = await getContractsData( + weth.address, + userAddress, + testEnv + ); + expect(wethUserDataAfter.usageAsCollateralEnabled).to.be.equal( + false, + 'usageAsCollateralEnabled are not set to false' + ); + }); }); From 1d2617a4ceec62451f40c020f28f992bae2ddff0 Mon Sep 17 00:00:00 2001 From: andyk Date: Thu, 10 Sep 2020 13:31:43 +0300 Subject: [PATCH 09/15] fix usageAsCollateral test --- test/collateral-swap.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index a67cd55e..c6d804c1 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -136,11 +136,15 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { }); it('Should set usage as collateral to false if no leftovers after swap', async () => { - const {pool, weth, dai, aEth} = testEnv; + const {pool, weth, dai, aEth, users} = testEnv; const userAddress = await pool.signer.getAddress(); + // add more liquidity to allow user 0 to swap everything he has + await weth.connect(users[2].signer).mint(ethers.utils.parseEther('1')); + await pool.connect(users[2].signer).deposit(weth.address, ethers.utils.parseEther('1'), '0'); + // cleanup borrowings, to be abe to swap whole weth - const amountToRepay = ethers.utils.parseEther('0.2'); + const amountToRepay = ethers.utils.parseEther('0.5'); await weth.mint(amountToRepay); await pool.repay(weth.address, amountToRepay, '1', userAddress); const txTimestamp = (await timeLatest()).plus(100); @@ -152,20 +156,18 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { const amountToSwap = calcExpectedATokenBalance( wethReserveDataBefore, wethUserDataBefore, - txTimestamp + txTimestamp.plus('1') ); await advanceBlock(txTimestamp.toNumber()); - console.log('before', amountToSwap.toString()); await pool.collateralSwap( _mockSwapAdapter.address, weth.address, dai.address, - '1'.toString(), + amountToSwap.toString(), '0x10' ); - console.log('after', (await aEth.balanceOf(userAddress)).toString()); const {userData: wethUserDataAfter} = await getContractsData( weth.address, userAddress, From 0911f907a801d72c0624f58788ba683fbd392ad6 Mon Sep 17 00:00:00 2001 From: eboado Date: Sun, 13 Sep 2020 10:08:14 +0200 Subject: [PATCH 10/15] Fixes #35 --- contracts/lendingpool/LendingPool.sol | 4 ++ contracts/libraries/logic/ValidationLogic.sol | 16 ++++++++ test/liquidation-underlying.spec.ts | 21 ++++++++++ test/repay-with-collateral.spec.ts | 38 +++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 30a69084..5dfe544f 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -385,6 +385,8 @@ contract LendingPool is VersionedInitializable, ILendingPool { uint256 purchaseAmount, bool receiveAToken ) external override { + ValidationLogic.validateLiquidation(_reserves[collateral], _reserves[asset]); + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line @@ -444,6 +446,8 @@ contract LendingPool is VersionedInitializable, ILendingPool { require(!_flashLiquidationLocked, Errors.REENTRANCY_NOT_ALLOWED); _flashLiquidationLocked = true; + ValidationLogic.validateLiquidation(_reserves[collateral], _reserves[principal]); + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 9be3f828..60d7645c 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -329,4 +329,20 @@ library ValidationLogic { require(premium > 0, Errors.REQUESTED_AMOUNT_TOO_SMALL); require(mode <= uint256(ReserveLogic.InterestRateMode.VARIABLE), Errors.INVALID_FLASHLOAN_MODE); } + + /** + * @dev Validates configurations for liquidation actions, both liquidationCall() and repayWithCollateral() + * @param collateralReserve The reserve data of the collateral + * @param principalReserve The reserve data of the principal + **/ + function validateLiquidation( + ReserveLogic.ReserveData storage collateralReserve, + ReserveLogic.ReserveData storage principalReserve + ) internal view { + require( + collateralReserve.configuration.getActive() && + principalReserve.configuration.getActive(), + Errors.NO_ACTIVE_RESERVE + ); + } } diff --git a/test/liquidation-underlying.spec.ts b/test/liquidation-underlying.spec.ts index 3f2aecbc..076abf88 100644 --- a/test/liquidation-underlying.spec.ts +++ b/test/liquidation-underlying.spec.ts @@ -7,6 +7,7 @@ import {makeSuite} from './helpers/make-suite'; import {ProtocolErrors, RateMode} from '../helpers/types'; import {calcExpectedStableDebtTokenBalance} from './helpers/utils/calculations'; import {getUserData} from './helpers/utils/helpers'; +import {parseEther} from 'ethers/lib/utils'; const chai = require('chai'); @@ -23,6 +24,26 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', BigNumber.config({DECIMAL_PLACES: 20, ROUNDING_MODE: BigNumber.ROUND_HALF_UP}); }); + it("It's not possible to liquidate on a non-active collateral or a non active principal", async () => { + const {configurator, weth, pool, users, dai} = testEnv; + const user = users[1]; + await configurator.deactivateReserve(weth.address); + + await expect( + pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(weth.address); + + await configurator.deactivateReserve(dai.address); + + await expect( + pool.liquidationCall(weth.address, dai.address, user.address, parseEther('1000'), false) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(dai.address); + }); + it('LIQUIDATION - Deposits WETH, borrows DAI', async () => { const {dai, weth, users, pool, oracle} = testEnv; const depositor = users[0]; diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index faefa403..81d42cbe 100644 --- a/test/repay-with-collateral.spec.ts +++ b/test/repay-with-collateral.spec.ts @@ -39,6 +39,44 @@ export const expectRepayWithCollateralEvent = ( }; makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { + it("It's not possible to repayWithCollateral() on a non-active collateral or a non active principal", async () => { + const {configurator, weth, pool, users, dai, mockSwapAdapter} = testEnv; + const user = users[1]; + await configurator.deactivateReserve(weth.address); + + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + parseEther('100'), + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(weth.address); + + await configurator.deactivateReserve(dai.address); + + await expect( + pool + .connect(user.signer) + .repayWithCollateral( + weth.address, + dai.address, + user.address, + parseEther('100'), + mockSwapAdapter.address, + '0x' + ) + ).to.be.revertedWith('2'); + + await configurator.activateReserve(dai.address); + }); + it('User 1 provides some liquidity for others to borrow', async () => { const {pool, weth, dai, usdc, deployer} = testEnv; From e2500d153201b2bbe06a0ca8c16a0984f5f04af1 Mon Sep 17 00:00:00 2001 From: eboado Date: Mon, 14 Sep 2020 10:52:31 +0200 Subject: [PATCH 11/15] - Refactored validation logic of liquidationCall() and repayWithCollateral() to ValidationLogic. --- contracts/lendingpool/LendingPool.sol | 3 - .../LendingPoolLiquidationManager.sol | 114 ++++++----------- contracts/libraries/helpers/Errors.sol | 10 ++ contracts/libraries/logic/ValidationLogic.sol | 115 ++++++++++++++++-- 4 files changed, 155 insertions(+), 87 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 5dfe544f..e7ee1d5c 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -385,7 +385,6 @@ contract LendingPool is VersionedInitializable, ILendingPool { uint256 purchaseAmount, bool receiveAToken ) external override { - ValidationLogic.validateLiquidation(_reserves[collateral], _reserves[asset]); address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); @@ -446,8 +445,6 @@ contract LendingPool is VersionedInitializable, ILendingPool { require(!_flashLiquidationLocked, Errors.REENTRANCY_NOT_ALLOWED); _flashLiquidationLocked = true; - ValidationLogic.validateLiquidation(_reserves[collateral], _reserves[principal]); - address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 8640761b..13e7a4b0 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -21,6 +21,7 @@ import {PercentageMath} from '../libraries/math/PercentageMath.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol'; import {ISwapAdapter} from '../interfaces/ISwapAdapter.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; +import {ValidationLogic} from '../libraries/logic/ValidationLogic.sol'; /** * @title LendingPoolLiquidationManager contract @@ -88,15 +89,6 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 swappedCollateralAmount ); - enum LiquidationErrors { - NO_ERROR, - NO_COLLATERAL_AVAILABLE, - COLLATERAL_CANNOT_BE_LIQUIDATED, - CURRRENCY_NOT_BORROWED, - HEALTH_FACTOR_ABOVE_THRESHOLD, - NOT_ENOUGH_LIQUIDITY - } - struct LiquidationCallLocalVars { uint256 userCollateralBalance; uint256 userStableDebt; @@ -112,6 +104,9 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 healthFactor; IAToken collateralAtoken; bool isCollateralEnabled; + address principalAToken; + uint256 errorCode; + string errorMsg; } /** @@ -138,8 +133,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 purchaseAmount, bool receiveAToken ) external returns (uint256, string memory) { - ReserveLogic.ReserveData storage principalReserve = reserves[principal]; ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; + ReserveLogic.ReserveData storage principalReserve = reserves[principal]; UserConfiguration.Map storage userConfig = usersConfig[user]; LiquidationCallLocalVars memory vars; @@ -152,43 +147,29 @@ contract LendingPoolLiquidationManager is VersionedInitializable { addressesProvider.getPriceOracle() ); - if (vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { - return ( - uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), - Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD - ); - } - - vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); - - vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); - - vars.isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && - userConfig.isUsingAsCollateral(collateralReserve.index); - - //if collateral isn't enabled as collateral by user, it cannot be liquidated - if (!vars.isCollateralEnabled) { - return ( - uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), - Errors.COLLATERAL_CANNOT_BE_LIQUIDATED - ); - } - //if the user hasn't borrowed the specific currency defined by asset, it cannot be liquidated (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt( user, principalReserve ); - if (vars.userStableDebt == 0 && vars.userVariableDebt == 0) { - return ( - uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), - Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER - ); + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateLiquidationCall( + collateralReserve, + principalReserve, + userConfig, + vars.healthFactor, + vars.userStableDebt, + vars.userVariableDebt + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); } - //all clear - calculate the max principal amount that can be liquidated + vars.collateralAtoken = IAToken(collateralReserve.aTokenAddress); + + vars.userCollateralBalance = vars.collateralAtoken.balanceOf(user); + vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt).percentMul( LIQUIDATION_CLOSE_FACTOR_PERCENT ); @@ -224,7 +205,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { ); if (currentAvailableCollateral < vars.maxCollateralToLiquidate) { return ( - uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY), + uint256(Errors.LiquidationErrors.NOT_ENOUGH_LIQUIDITY), Errors.NOT_ENOUGH_LIQUIDITY_TO_LIQUIDATE ); } @@ -291,7 +272,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { receiveAToken ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** @@ -314,9 +295,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { address receiver, bytes calldata params ) external returns (uint256, string memory) { - ReserveLogic.ReserveData storage debtReserve = reserves[principal]; ReserveLogic.ReserveData storage collateralReserve = reserves[collateral]; - + ReserveLogic.ReserveData storage debtReserve = reserves[principal]; UserConfiguration.Map storage userConfig = usersConfig[user]; LiquidationCallLocalVars memory vars; @@ -329,36 +309,20 @@ contract LendingPoolLiquidationManager is VersionedInitializable { addressesProvider.getPriceOracle() ); - if ( - msg.sender != user && vars.healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD - ) { - return ( - uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), - Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD - ); - } - - if (msg.sender != user) { - vars.isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && - userConfig.isUsingAsCollateral(collateralReserve.index); - - //if collateral isn't enabled as collateral by user, it cannot be liquidated - if (!vars.isCollateralEnabled) { - return ( - uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), - Errors.COLLATERAL_CANNOT_BE_LIQUIDATED - ); - } - } - (vars.userStableDebt, vars.userVariableDebt) = Helpers.getUserCurrentDebt(user, debtReserve); - if (vars.userStableDebt == 0 && vars.userVariableDebt == 0) { - return ( - uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED), - Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER - ); + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateRepayWithCollateral( + collateralReserve, + debtReserve, + userConfig, + user, + vars.healthFactor, + vars.userStableDebt, + vars.userVariableDebt + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); } vars.maxPrincipalAmountToLiquidate = vars.userStableDebt.add(vars.userVariableDebt); @@ -397,7 +361,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { usersConfig[user].setUsingAsCollateral(collateralReserve.index, false); } - address principalAToken = debtReserve.aTokenAddress; + vars.principalAToken = debtReserve.aTokenAddress; // Notifies the receiver to proceed, sending as param the underlying already transferred ISwapAdapter(receiver).executeOperation( @@ -410,8 +374,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { //updating debt reserve debtReserve.updateCumulativeIndexesAndTimestamp(); - debtReserve.updateInterestRates(principal, principalAToken, vars.actualAmountToLiquidate, 0); - IERC20(principal).transferFrom(receiver, principalAToken, vars.actualAmountToLiquidate); + debtReserve.updateInterestRates(principal, vars.principalAToken, vars.actualAmountToLiquidate, 0); + IERC20(principal).transferFrom(receiver, vars.principalAToken, vars.actualAmountToLiquidate); if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { IVariableDebtToken(debtReserve.variableDebtTokenAddress).burn( @@ -443,7 +407,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { vars.maxCollateralToLiquidate ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } struct AvailableCollateralToLiquidateLocalVars { diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 60fd2da3..3cdb0303 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -76,4 +76,14 @@ library Errors { string public constant MULTIPLICATION_OVERFLOW = '44'; string public constant ADDITION_OVERFLOW = '45'; string public constant DIVISION_BY_ZERO = '46'; + + enum LiquidationErrors { + NO_ERROR, + NO_COLLATERAL_AVAILABLE, + COLLATERAL_CANNOT_BE_LIQUIDATED, + CURRRENCY_NOT_BORROWED, + HEALTH_FACTOR_ABOVE_THRESHOLD, + NOT_ENOUGH_LIQUIDITY, + NO_ACTIVE_RESERVE + } } diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 60d7645c..1cb29b8d 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -13,6 +13,7 @@ import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol'; import {UserConfiguration} from '../configuration/UserConfiguration.sol'; import {IPriceOracleGetter} from '../../interfaces/IPriceOracleGetter.sol'; import {Errors} from '../helpers/Errors.sol'; +import {Helpers} from '../helpers/Helpers.sol'; /** * @title ReserveLogic library @@ -331,18 +332,114 @@ library ValidationLogic { } /** - * @dev Validates configurations for liquidation actions, both liquidationCall() and repayWithCollateral() + * @dev Validates the liquidationCall() action * @param collateralReserve The reserve data of the collateral * @param principalReserve The reserve data of the principal + * @param userConfig The user configuration + * @param userHealthFactor The user's health factor + * @param userStableDebt Total stable debt balance of the user + * @param userVariableDebt Total variable debt balance of the user **/ - function validateLiquidation( + function validateLiquidationCall( ReserveLogic.ReserveData storage collateralReserve, - ReserveLogic.ReserveData storage principalReserve - ) internal view { - require( - collateralReserve.configuration.getActive() && - principalReserve.configuration.getActive(), - Errors.NO_ACTIVE_RESERVE - ); + ReserveLogic.ReserveData storage principalReserve, + UserConfiguration.Map storage userConfig, + uint256 userHealthFactor, + uint256 userStableDebt, + uint256 userVariableDebt + ) internal view returns(uint256, string memory) { + if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { + return ( + uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), + Errors.NO_ACTIVE_RESERVE + ); + } + + if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD + ); + } + + bool isCollateralEnabled = + collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.index); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!isCollateralEnabled) { + return ( + uint256(Errors.LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + Errors.COLLATERAL_CANNOT_BE_LIQUIDATED + ); + } + + if (userStableDebt == 0 && userVariableDebt == 0) { + return ( + uint256(Errors.LiquidationErrors.CURRRENCY_NOT_BORROWED), + Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + } + + /** + * @dev Validates the repayWithCollateral() action + * @param collateralReserve The reserve data of the collateral + * @param principalReserve The reserve data of the principal + * @param userConfig The user configuration + * @param user The address of the user + * @param userHealthFactor The user's health factor + * @param userStableDebt Total stable debt balance of the user + * @param userVariableDebt Total variable debt balance of the user + **/ + function validateRepayWithCollateral( + ReserveLogic.ReserveData storage collateralReserve, + ReserveLogic.ReserveData storage principalReserve, + UserConfiguration.Map storage userConfig, + address user, + uint256 userHealthFactor, + uint256 userStableDebt, + uint256 userVariableDebt + ) internal view returns(uint256, string memory) { + if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { + return ( + uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), + Errors.NO_ACTIVE_RESERVE + ); + } + + if ( + msg.sender != user && userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD), + Errors.HEALTH_FACTOR_NOT_BELOW_THRESHOLD + ); + } + + if (msg.sender != user) { + bool isCollateralEnabled = + collateralReserve.configuration.getLiquidationThreshold() > 0 && + userConfig.isUsingAsCollateral(collateralReserve.index); + + //if collateral isn't enabled as collateral by user, it cannot be liquidated + if (!isCollateralEnabled) { + return ( + uint256(Errors.LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED), + Errors.COLLATERAL_CANNOT_BE_LIQUIDATED + ); + } + } + + if (userStableDebt == 0 && userVariableDebt == 0) { + return ( + uint256(Errors.LiquidationErrors.CURRRENCY_NOT_BORROWED), + Errors.SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } } From 65775ca3bf8ac4fb72aa297e737d16ba0b1dd444 Mon Sep 17 00:00:00 2001 From: andyk Date: Tue, 15 Sep 2020 09:46:24 +0300 Subject: [PATCH 12/15] update collateralSwap signature in the delegate call --- contracts/lendingpool/LendingPool.sol | 2 +- test/collateral-swap.spec.ts | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 69dee3a3..592e604b 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -592,7 +592,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { //solium-disable-next-line (bool success, bytes memory result) = liquidationManager.delegatecall( abi.encodeWithSignature( - 'collateralSwap(address,address,address,uint256,address,bytes)', + 'collateralSwap(address,address,address,uint256,bytes)', receiverAddress, fromAsset, toAsset, diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index a267162b..04e223fc 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -27,10 +27,12 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { 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, await signer.getAddress(), '0',); + await pool + .connect(signer) + .deposit(weth.address, amountToDeposit, await signer.getAddress(), '0'); } }); - it('User tries to swap more then he can', async () => { + it('User tries to swap more then he can, revert expected', async () => { const {pool, weth, dai} = testEnv; await expect( pool.collateralSwap( @@ -43,6 +45,19 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { ).to.be.revertedWith('55'); }); + it('User tries to swap asset on equal asset, revert expected', async () => { + const {pool, weth} = testEnv; + await expect( + pool.collateralSwap( + _mockSwapAdapter.address, + weth.address, + weth.address, + ethers.utils.parseEther('0.1'), + '0x10' + ) + ).to.be.revertedWith('56'); + }); + it('User tries to swap more then available on the reserve', async () => { const {pool, weth, dai, users, aEth, deployer} = testEnv; @@ -61,7 +76,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { }); it('User tries to swap correct amount', async () => { - const {pool, weth, dai, aEth, aDai} = testEnv; + const {pool, weth, dai, aEth, aDai} = testEnv; const userAddress = await pool.signer.getAddress(); const amountToSwap = ethers.utils.parseEther('0.25'); @@ -141,7 +156,9 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { // add more liquidity to allow user 0 to swap everything he has await weth.connect(users[2].signer).mint(ethers.utils.parseEther('1')); - await pool.connect(users[2].signer).deposit(weth.address, ethers.utils.parseEther('1'), users[2].address, '0'); + await pool + .connect(users[2].signer) + .deposit(weth.address, ethers.utils.parseEther('1'), users[2].address, '0'); // cleanup borrowings, to be abe to swap whole weth const amountToRepay = ethers.utils.parseEther('0.5'); From 172cb05b640f0a1ba50b8cca2a1876624193cc85 Mon Sep 17 00:00:00 2001 From: eboado Date: Tue, 15 Sep 2020 09:51:23 +0200 Subject: [PATCH 13/15] - Renamed collateralSwap() to swapDeposit(). - Added docs to sw --- contracts/interfaces/ILendingPool.sol | 10 +++++++++- contracts/lendingpool/LendingPool.sol | 12 ++++++++++-- .../lendingpool/LendingPoolLiquidationManager.sol | 10 +++++++++- test/collateral-swap.spec.ts | 14 +++++++------- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 3a6805da..e206d15c 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -290,7 +290,15 @@ interface ILendingPool { uint16 referralCode ) external; - function collateralSwap( + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( address receiverAddress, address fromAsset, address toAsset, diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 592e604b..b24eb0e8 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -580,7 +580,15 @@ contract LendingPool is VersionedInitializable, ILendingPool { } } - function collateralSwap( + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( address receiverAddress, address fromAsset, address toAsset, @@ -592,7 +600,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { //solium-disable-next-line (bool success, bytes memory result) = liquidationManager.delegatecall( abi.encodeWithSignature( - 'collateralSwap(address,address,address,uint256,bytes)', + 'swapLiquidity(address,address,address,uint256,bytes)', receiverAddress, fromAsset, toAsset, diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index e8f1e611..dcc3f268 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -518,7 +518,15 @@ contract LendingPoolLiquidationManager is VersionedInitializable { return (collateralAmount, principalAmountNeeded); } - function collateralSwap( + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( address receiverAddress, address fromAsset, address toAsset, diff --git a/test/collateral-swap.spec.ts b/test/collateral-swap.spec.ts index 04e223fc..cbae27f5 100644 --- a/test/collateral-swap.spec.ts +++ b/test/collateral-swap.spec.ts @@ -11,7 +11,7 @@ import {advanceBlock, timeLatest} from '../helpers/misc-utils'; const {expect} = require('chai'); -makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { +makeSuite('LendingPool SwapDeposit function', (testEnv: TestEnv) => { let _mockSwapAdapter = {} as MockSwapAdapter; const {HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD} = ProtocolErrors; @@ -35,7 +35,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { it('User tries to swap more then he can, revert expected', async () => { const {pool, weth, dai} = testEnv; await expect( - pool.collateralSwap( + pool.swapLiquidity( _mockSwapAdapter.address, weth.address, dai.address, @@ -48,7 +48,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { it('User tries to swap asset on equal asset, revert expected', async () => { const {pool, weth} = testEnv; await expect( - pool.collateralSwap( + pool.swapLiquidity( _mockSwapAdapter.address, weth.address, weth.address, @@ -65,7 +65,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { await pool.connect(users[2].signer).withdraw(weth.address, ethers.utils.parseEther('1')); await expect( - pool.collateralSwap( + pool.swapLiquidity( _mockSwapAdapter.address, weth.address, dai.address, @@ -98,7 +98,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { const reserveBalanceDAIBefore = await dai.balanceOf(aDai.address); const txReceipt = await waitForTx( - await pool.collateralSwap( + await pool.swapLiquidity( _mockSwapAdapter.address, weth.address, dai.address, @@ -146,7 +146,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { await pool.borrow(weth.address, ethers.utils.parseEther('0.3'), 1, 0, deployer.address); await expect( - pool.collateralSwap(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') + pool.swapLiquidity(_mockSwapAdapter.address, weth.address, dai.address, amountToSwap, '0x10') ).to.be.revertedWith(HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD); }); @@ -178,7 +178,7 @@ makeSuite('LendingPool CollateralSwap function', (testEnv: TestEnv) => { await advanceBlock(txTimestamp.toNumber()); - await pool.collateralSwap( + await pool.swapLiquidity( _mockSwapAdapter.address, weth.address, dai.address, From 59996e1ece6aafc4e0ff438e38eae5ac9dc01b05 Mon Sep 17 00:00:00 2001 From: eboado Date: Tue, 15 Sep 2020 10:28:39 +0200 Subject: [PATCH 14/15] - Refactor validation of swapLiquidity() to ValidationLogic. - Added extra check on active reserves on swapLiquidity(). --- .../LendingPoolLiquidationManager.sol | 195 +++++++++++------- contracts/libraries/logic/ValidationLogic.sol | 55 +++-- 2 files changed, 154 insertions(+), 96 deletions(-) diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 643ad42f..944c681e 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -110,6 +110,26 @@ contract LendingPoolLiquidationManager is VersionedInitializable { string errorMsg; } + struct SwapLiquidityLocalVars { + uint256 healthFactor; + uint256 amountToReceive; + uint256 userBalanceBefore; + IAToken fromReserveAToken; + IAToken toReserveAToken; + uint256 errorCode; + string errorMsg; + } + + struct AvailableCollateralToLiquidateLocalVars { + uint256 userCompoundedBorrowBalance; + uint256 liquidationBonus; + uint256 collateralPrice; + uint256 principalCurrencyPrice; + uint256 maxAmountCollateralToLiquidate; + uint256 principalDecimals; + uint256 collateralDecimals; + } + /** * @dev as the contract extends the VersionedInitializable contract to match the state * of the LendingPool contract, the getRevision() function is needed. @@ -253,7 +273,12 @@ contract LendingPoolLiquidationManager is VersionedInitializable { ); //burn the equivalent amount of atoken - vars.collateralAtoken.burn(user, msg.sender, vars.maxCollateralToLiquidate, collateralReserve.liquidityIndex); + vars.collateralAtoken.burn( + user, + msg.sender, + vars.maxCollateralToLiquidate, + collateralReserve.liquidityIndex + ); } //transfers the principal currency to the aToken @@ -356,7 +381,12 @@ contract LendingPoolLiquidationManager is VersionedInitializable { //updating collateral reserve indexes collateralReserve.updateCumulativeIndexesAndTimestamp(); - vars.collateralAtoken.burn(user, receiver, vars.maxCollateralToLiquidate, collateralReserve.liquidityIndex); + vars.collateralAtoken.burn( + user, + receiver, + vars.maxCollateralToLiquidate, + collateralReserve.liquidityIndex + ); if (vars.userCollateralBalance == vars.maxCollateralToLiquidate) { usersConfig[user].setUsingAsCollateral(collateralReserve.id, false); @@ -375,7 +405,12 @@ contract LendingPoolLiquidationManager is VersionedInitializable { //updating debt reserve debtReserve.updateCumulativeIndexesAndTimestamp(); - debtReserve.updateInterestRates(principal, vars.principalAToken, vars.actualAmountToLiquidate, 0); + debtReserve.updateInterestRates( + principal, + vars.principalAToken, + vars.actualAmountToLiquidate, + 0 + ); IERC20(principal).transferFrom(receiver, vars.principalAToken, vars.actualAmountToLiquidate); if (vars.userVariableDebt >= vars.actualAmountToLiquidate) { @@ -411,14 +446,83 @@ contract LendingPoolLiquidationManager is VersionedInitializable { return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } - struct AvailableCollateralToLiquidateLocalVars { - uint256 userCompoundedBorrowBalance; - uint256 liquidationBonus; - uint256 collateralPrice; - uint256 principalCurrencyPrice; - uint256 maxAmountCollateralToLiquidate; - uint256 principalDecimals; - uint256 collateralDecimals; + /** + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * - It's not possible to release one asset to swap for the same + * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface + * @param fromAsset Asset to swap from + * @param toAsset Asset to swap to + * @param params a bytes array to be sent (if needed) to the receiver contract with extra data + **/ + function swapLiquidity( + address receiverAddress, + address fromAsset, + address toAsset, + uint256 amountToSwap, + bytes calldata params + ) external returns (uint256, string memory) { + ReserveLogic.ReserveData storage fromReserve = reserves[fromAsset]; + ReserveLogic.ReserveData storage toReserve = reserves[toAsset]; + + // Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables + SwapLiquidityLocalVars memory vars; + + (vars.errorCode, vars.errorMsg) = ValidationLogic.validateSwapLiquidity( + fromReserve, + toReserve, + fromAsset, + toAsset + ); + + if (Errors.LiquidationErrors(vars.errorCode) != Errors.LiquidationErrors.NO_ERROR) { + return (vars.errorCode, vars.errorMsg); + } + + vars.fromReserveAToken = IAToken(fromReserve.aTokenAddress); + vars.toReserveAToken = IAToken(toReserve.aTokenAddress); + + fromReserve.updateCumulativeIndexesAndTimestamp(); + toReserve.updateCumulativeIndexesAndTimestamp(); + + if (vars.fromReserveAToken.balanceOf(msg.sender) == amountToSwap) { + usersConfig[msg.sender].setUsingAsCollateral(fromReserve.id, false); + } + + fromReserve.updateInterestRates(fromAsset, address(vars.fromReserveAToken), 0, amountToSwap); + + vars.fromReserveAToken.burn(msg.sender, receiverAddress, amountToSwap, fromReserve.liquidityIndex); + // Notifies the receiver to proceed, sending as param the underlying already transferred + ISwapAdapter(receiverAddress).executeOperation( + fromAsset, + toAsset, + amountToSwap, + address(this), + params + ); + + vars.amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); + if (vars.amountToReceive != 0) { + IERC20(toAsset).transferFrom(receiverAddress, address(vars.toReserveAToken), vars.amountToReceive); + vars.toReserveAToken.mint(msg.sender, vars.amountToReceive, toReserve.liquidityIndex); + toReserve.updateInterestRates(toAsset, address(vars.toReserveAToken), vars.amountToReceive, 0); + } + + (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( + msg.sender, + reserves, + usersConfig[msg.sender], + reservesList, + addressesProvider.getPriceOracle() + ); + + if (vars.healthFactor < GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { + return ( + uint256(Errors.LiquidationErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD), + Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** @@ -479,73 +583,4 @@ contract LendingPoolLiquidationManager is VersionedInitializable { return (collateralAmount, principalAmountNeeded); } - /** - * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. - * - It's not possible to release one asset to swap for the same - * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface - * @param fromAsset Asset to swap from - * @param toAsset Asset to swap to - * @param params a bytes array to be sent (if needed) to the receiver contract with extra data - **/ - function swapLiquidity( - address receiverAddress, - address fromAsset, - address toAsset, - uint256 amountToSwap, - bytes calldata params - ) external returns (uint256, string memory) { - if (fromAsset == toAsset) { - return (uint256(Errors.LiquidationErrors.INVALID_EQUAL_ASSETS_TO_SWAP), Errors.INVALID_EQUAL_ASSETS_TO_SWAP); - } - - ReserveLogic.ReserveData storage fromReserve = reserves[fromAsset]; - ReserveLogic.ReserveData storage toReserve = reserves[toAsset]; - IAToken fromReserveAToken = IAToken(fromReserve.aTokenAddress); - IAToken toReserveAToken = IAToken(toReserve.aTokenAddress); - - fromReserve.updateCumulativeIndexesAndTimestamp(); - toReserve.updateCumulativeIndexesAndTimestamp(); - - // get user position - uint256 userBalance = fromReserveAToken.balanceOf(msg.sender); - if (userBalance == amountToSwap) { - usersConfig[msg.sender].setUsingAsCollateral(fromReserve.id, false); - } - - fromReserve.updateInterestRates(fromAsset, address(fromReserveAToken), 0, amountToSwap); - - fromReserveAToken.burn(msg.sender, receiverAddress, amountToSwap, fromReserve.liquidityIndex); - // Notifies the receiver to proceed, sending as param the underlying already transferred - ISwapAdapter(receiverAddress).executeOperation( - fromAsset, - toAsset, - amountToSwap, - address(this), - params - ); - - uint256 amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); - if (amountToReceive != 0) { - IERC20(toAsset).transferFrom(receiverAddress, address(toReserveAToken), amountToReceive); - toReserveAToken.mint(msg.sender, amountToReceive, toReserve.liquidityIndex); - toReserve.updateInterestRates(toAsset, address(toReserveAToken), amountToReceive, 0); - } - - (, , , , uint256 healthFactor) = GenericLogic.calculateUserAccountData( - msg.sender, - reserves, - usersConfig[msg.sender], - reservesList, - addressesProvider.getPriceOracle() - ); - - if (healthFactor < GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { - return ( - uint256(Errors.LiquidationErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD), - Errors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD - ); - } - - return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); - } } diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 84c1d861..336e18ad 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -347,12 +347,11 @@ library ValidationLogic { uint256 userHealthFactor, uint256 userStableDebt, uint256 userVariableDebt - ) internal view returns(uint256, string memory) { - if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { - return ( - uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), - Errors.NO_ACTIVE_RESERVE - ); + ) internal view returns (uint256, string memory) { + if ( + !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive() + ) { + return (uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), Errors.NO_ACTIVE_RESERVE); } if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) { @@ -362,8 +361,7 @@ library ValidationLogic { ); } - bool isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && + bool isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && userConfig.isUsingAsCollateral(collateralReserve.id); //if collateral isn't enabled as collateral by user, it cannot be liquidated @@ -402,12 +400,11 @@ library ValidationLogic { uint256 userHealthFactor, uint256 userStableDebt, uint256 userVariableDebt - ) internal view returns(uint256, string memory) { - if ( !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()) { - return ( - uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), - Errors.NO_ACTIVE_RESERVE - ); + ) internal view returns (uint256, string memory) { + if ( + !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive() + ) { + return (uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), Errors.NO_ACTIVE_RESERVE); } if ( @@ -420,8 +417,7 @@ library ValidationLogic { } if (msg.sender != user) { - bool isCollateralEnabled = - collateralReserve.configuration.getLiquidationThreshold() > 0 && + bool isCollateralEnabled = collateralReserve.configuration.getLiquidationThreshold() > 0 && userConfig.isUsingAsCollateral(collateralReserve.id); //if collateral isn't enabled as collateral by user, it cannot be liquidated @@ -442,4 +438,31 @@ library ValidationLogic { return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } + + /** + * @dev Validates the swapLiquidity() action + * @param fromReserve The reserve data of the asset to swap from + * @param toReserve The reserve data of the asset to swap to + * @param fromAsset Address of the asset to swap from + * @param toAsset Address of the asset to swap to + **/ + function validateSwapLiquidity( + ReserveLogic.ReserveData storage fromReserve, + ReserveLogic.ReserveData storage toReserve, + address fromAsset, + address toAsset + ) internal view returns (uint256, string memory) { + if (!fromReserve.configuration.getActive() || !toReserve.configuration.getActive()) { + return (uint256(Errors.LiquidationErrors.NO_ACTIVE_RESERVE), Errors.NO_ACTIVE_RESERVE); + } + + if (fromAsset == toAsset) { + return ( + uint256(Errors.LiquidationErrors.INVALID_EQUAL_ASSETS_TO_SWAP), + Errors.INVALID_EQUAL_ASSETS_TO_SWAP + ); + } + + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + } } From 0c94b24700c21f3326bf305141c7b8c511c5903b Mon Sep 17 00:00:00 2001 From: eboado Date: Tue, 15 Sep 2020 10:43:22 +0200 Subject: [PATCH 15/15] - Fixed description on swapLiquidity(). --- contracts/interfaces/ILendingPool.sol | 2 +- contracts/lendingpool/LendingPool.sol | 2 +- .../LendingPoolLiquidationManager.sol | 23 +++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index e206d15c..41b13f07 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -291,7 +291,7 @@ interface ILendingPool { ) external; /** - * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. * - It's not possible to release one asset to swap for the same * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface * @param fromAsset Asset to swap from diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 153c45d0..ba5e7894 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -582,7 +582,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { } /** - * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. * - It's not possible to release one asset to swap for the same * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface * @param fromAsset Asset to swap from diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 944c681e..664e32f4 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -447,7 +447,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { } /** - * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral. + * @dev Allows an user to release one of his assets deposited in the protocol, even if it is used as collateral, to swap for another. * - It's not possible to release one asset to swap for the same * @param receiverAddress The address of the contract receiving the funds. The receiver should implement the ISwapAdapter interface * @param fromAsset Asset to swap from @@ -490,7 +490,12 @@ contract LendingPoolLiquidationManager is VersionedInitializable { fromReserve.updateInterestRates(fromAsset, address(vars.fromReserveAToken), 0, amountToSwap); - vars.fromReserveAToken.burn(msg.sender, receiverAddress, amountToSwap, fromReserve.liquidityIndex); + vars.fromReserveAToken.burn( + msg.sender, + receiverAddress, + amountToSwap, + fromReserve.liquidityIndex + ); // Notifies the receiver to proceed, sending as param the underlying already transferred ISwapAdapter(receiverAddress).executeOperation( fromAsset, @@ -502,9 +507,18 @@ contract LendingPoolLiquidationManager is VersionedInitializable { vars.amountToReceive = IERC20(toAsset).balanceOf(receiverAddress); if (vars.amountToReceive != 0) { - IERC20(toAsset).transferFrom(receiverAddress, address(vars.toReserveAToken), vars.amountToReceive); + IERC20(toAsset).transferFrom( + receiverAddress, + address(vars.toReserveAToken), + vars.amountToReceive + ); vars.toReserveAToken.mint(msg.sender, vars.amountToReceive, toReserve.liquidityIndex); - toReserve.updateInterestRates(toAsset, address(vars.toReserveAToken), vars.amountToReceive, 0); + toReserve.updateInterestRates( + toAsset, + address(vars.toReserveAToken), + vars.amountToReceive, + 0 + ); } (, , , , vars.healthFactor) = GenericLogic.calculateUserAccountData( @@ -582,5 +596,4 @@ contract LendingPoolLiquidationManager is VersionedInitializable { } return (collateralAmount, principalAmountNeeded); } - }