diff --git a/buidler.config.ts b/buidler.config.ts index be2d98f4..dd736359 100644 --- a/buidler.config.ts +++ b/buidler.config.ts @@ -2,6 +2,7 @@ import {usePlugin, BuidlerConfig} from '@nomiclabs/buidler/config'; // @ts-ignore import {accounts} from './test-wallets.js'; import {eEthereumNetwork} from './helpers/types'; +import { BUIDLEREVM_CHAINID, COVERAGE_CHAINID } from './helpers/constants'; usePlugin('@nomiclabs/buidler-ethers'); usePlugin('buidler-typechain'); @@ -59,6 +60,7 @@ const config: any = { networks: { coverage: { url: 'http://localhost:8555', + chainId: COVERAGE_CHAINID, }, kovan: getCommonNetworkConfig(eEthereumNetwork.kovan, 42), ropsten: getCommonNetworkConfig(eEthereumNetwork.ropsten, 3), @@ -68,7 +70,7 @@ const config: any = { blockGasLimit: DEFAULT_BLOCK_GAS_LIMIT, gas: DEFAULT_BLOCK_GAS_LIMIT, gasPrice: 8000000000, - chainId: 31337, + chainId: BUIDLEREVM_CHAINID, throwOnTransactionFailures: true, throwOnCallFailures: true, accounts: accounts.map(({secretKey, balance}: {secretKey: string; balance: string}) => ({ diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index 4daf85e0..3c8f4cf6 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -29,6 +29,13 @@ interface ILendingPool { **/ event Withdraw(address indexed reserve, address indexed user, uint256 amount); + event BorrowAllowanceDelegated( + address indexed asset, + address indexed fromUser, + address indexed toUser, + uint256 interestRateMode, + uint256 amount + ); /** * @dev emitted on borrow * @param reserve the address of the reserve @@ -40,7 +47,8 @@ interface ILendingPool { **/ event Borrow( address indexed reserve, - address indexed user, + address user, + address indexed onBehalfOf, uint256 amount, uint256 borrowRateMode, uint256 borrowRate, @@ -151,6 +159,27 @@ interface ILendingPool { **/ function withdraw(address reserve, uint256 amount) external; + /** + * @dev Sets allowance to borrow on a certain type of debt asset for a certain user address + * @param asset The underlying asset of the debt token + * @param user The user to give allowance to + * @param interestRateMode Type of debt: 1 for stable, 2 for variable + * @param amount Allowance amount to borrow + **/ + function delegateBorrowAllowance( + address asset, + address user, + uint256 interestRateMode, + uint256 amount + ) external; + + function getBorrowAllowance( + address fromUser, + address toUser, + address asset, + uint256 interestRateMode + ) external view returns (uint256); + /** * @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower * already deposited enough collateral. @@ -162,7 +191,8 @@ interface ILendingPool { address reserve, uint256 amount, uint256 interestRateMode, - uint16 referralCode + uint16 referralCode, + address onBehalfOf ) external; /** diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index 1ccfc467..5f8a552b 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -52,6 +52,8 @@ contract LendingPool is VersionedInitializable, ILendingPool { mapping(address => ReserveLogic.ReserveData) internal _reserves; mapping(address => UserConfiguration.Map) internal _usersConfig; ILendingPoolAddressesProvider internal _addressesProvider; + // debt token address => user who gives allowance => user who receives allowance => amount + mapping(address => mapping(address => mapping(address => uint256))) internal _borrowAllowance; address[] internal _reservesList; @@ -159,6 +161,35 @@ contract LendingPool is VersionedInitializable, ILendingPool { emit Withdraw(asset, msg.sender, amount); } + function getBorrowAllowance( + address fromUser, + address toUser, + address asset, + uint256 interestRateMode + ) external override view returns (uint256) { + return + _borrowAllowance[_reserves[asset].getDebtTokenAddress(interestRateMode)][fromUser][toUser]; + } + + /** + * @dev Sets allowance to borrow on a certain type of debt asset for a certain user address + * @param asset The underlying asset of the debt token + * @param user The user to give allowance to + * @param interestRateMode Type of debt: 1 for stable, 2 for variable + * @param amount Allowance amount to borrow + **/ + function delegateBorrowAllowance( + address asset, + address user, + uint256 interestRateMode, + uint256 amount + ) external override { + address debtToken = _reserves[asset].getDebtTokenAddress(interestRateMode); + + _borrowAllowance[debtToken][msg.sender][user] = amount; + emit BorrowAllowanceDelegated(asset, msg.sender, user, interestRateMode, amount); + } + /** * @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower * already deposited enough collateral. @@ -166,20 +197,34 @@ contract LendingPool is VersionedInitializable, ILendingPool { * @param amount the amount to be borrowed * @param interestRateMode the interest rate mode at which the user wants to borrow. Can be 0 (STABLE) or 1 (VARIABLE) * @param referralCode a referral code for integrators + * @param onBehalfOf address of the user who will receive the debt **/ function borrow( address asset, uint256 amount, uint256 interestRateMode, - uint16 referralCode + uint16 referralCode, + address onBehalfOf ) external override { + ReserveLogic.ReserveData storage reserve = _reserves[asset]; + + if (onBehalfOf != msg.sender) { + address debtToken = reserve.getDebtTokenAddress(interestRateMode); + + _borrowAllowance[debtToken][onBehalfOf][msg + .sender] = _borrowAllowance[debtToken][onBehalfOf][msg.sender].sub( + amount, + Errors.BORROW_ALLOWANCE_ARE_NOT_ENOUGH + ); + } _executeBorrow( ExecuteBorrowParams( asset, msg.sender, + onBehalfOf, amount, interestRateMode, - _reserves[asset].aTokenAddress, + reserve.aTokenAddress, referralCode, true ) @@ -375,6 +420,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { uint256 purchaseAmount, bool receiveAToken ) external override { + address liquidationManager = _addressesProvider.getLendingPoolLiquidationManager(); //solium-disable-next-line @@ -513,6 +559,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { ExecuteBorrowParams( asset, msg.sender, + msg.sender, vars.amountPlusPremium.sub(vars.availableBalance), mode, vars.aTokenAddress, @@ -804,6 +851,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { struct ExecuteBorrowParams { address asset; address user; + address onBehalfOf; uint256 amount; uint256 interestRateMode; address aTokenAddress; @@ -817,7 +865,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { **/ function _executeBorrow(ExecuteBorrowParams memory vars) internal { ReserveLogic.ReserveData storage reserve = _reserves[vars.asset]; - UserConfiguration.Map storage userConfig = _usersConfig[msg.sender]; + UserConfiguration.Map storage userConfig = _usersConfig[vars.onBehalfOf]; address oracle = _addressesProvider.getPriceOracle(); @@ -827,7 +875,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { ValidationLogic.validateBorrow( reserve, - vars.asset, + vars.onBehalfOf, vars.amount, amountInETH, vars.interestRateMode, @@ -866,12 +914,13 @@ contract LendingPool is VersionedInitializable, ILendingPool { ); if (vars.releaseUnderlying) { - IAToken(vars.aTokenAddress).transferUnderlyingTo(msg.sender, vars.amount); + IAToken(vars.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } emit Borrow( vars.asset, - msg.sender, + vars.user, + vars.onBehalfOf, vars.amount, vars.interestRateMode, ReserveLogic.InterestRateMode(vars.interestRateMode) == ReserveLogic.InterestRateMode.STABLE diff --git a/contracts/lendingpool/LendingPoolLiquidationManager.sol b/contracts/lendingpool/LendingPoolLiquidationManager.sol index 8a018489..02e34ad6 100644 --- a/contracts/lendingpool/LendingPoolLiquidationManager.sol +++ b/contracts/lendingpool/LendingPoolLiquidationManager.sol @@ -22,6 +22,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 @@ -44,6 +45,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { mapping(address => ReserveLogic.ReserveData) internal reserves; mapping(address => UserConfiguration.Map) internal usersConfig; + mapping(address => mapping(address => mapping(address => uint256))) internal _borrowAllowance; address[] internal reservesList; @@ -89,15 +91,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; @@ -113,6 +106,9 @@ contract LendingPoolLiquidationManager is VersionedInitializable { uint256 healthFactor; IAToken collateralAtoken; bool isCollateralEnabled; + address principalAToken; + uint256 errorCode; + string errorMsg; } /** @@ -139,8 +135,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; @@ -153,43 +149,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.id); - - //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 ); @@ -225,7 +207,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 ); } @@ -301,7 +283,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { receiveAToken ); - return (uint256(LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); + return (uint256(Errors.LiquidationErrors.NO_ERROR), Errors.NO_ERRORS); } /** @@ -324,9 +306,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; @@ -339,36 +320,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.id); - - //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); @@ -412,7 +377,7 @@ contract LendingPoolLiquidationManager is VersionedInitializable { usersConfig[user].setUsingAsCollateral(collateralReserve.id, false); } - address principalAToken = debtReserve.aTokenAddress; + vars.principalAToken = debtReserve.aTokenAddress; // Notifies the receiver to proceed, sending as param the underlying already transferred ISwapAdapter(receiver).executeOperation( @@ -425,8 +390,8 @@ contract LendingPoolLiquidationManager is VersionedInitializable { //updating debt reserve debtReserve.updateState(); - 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( @@ -463,7 +428,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 ee9feac9..6a681b14 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -38,6 +38,7 @@ library Errors { string public constant INCONSISTENT_PROTOCOL_ACTUAL_BALANCE = '26'; // 'The actual balance of the protocol is inconsistent' string public constant CALLER_NOT_LENDING_POOL_CONFIGURATOR = '27'; // 'The actual balance of the protocol is inconsistent' string public constant INVALID_FLASHLOAN_MODE = '43'; //Invalid flashloan mode selected + string public constant BORROW_ALLOWANCE_ARE_NOT_ENOUGH = '54'; // User borrows on behalf, but allowance are too small string public constant REENTRANCY_NOT_ALLOWED = '52'; string public constant FAILED_REPAY_WITH_COLLATERAL = '53'; @@ -73,4 +74,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/ReserveLogic.sol b/contracts/libraries/logic/ReserveLogic.sol index 73b68f17..e91a45c4 100644 --- a/contracts/libraries/logic/ReserveLogic.sol +++ b/contracts/libraries/logic/ReserveLogic.sol @@ -121,8 +121,30 @@ library ReserveLogic { } /** - * @dev Updates the state of the reserve by minting to the reserve treasury and calculate the new - * reserve indexes + * @dev returns an address of the debt token used for particular interest rate mode on asset. + * @param reserve the reserve object + * @param interestRateMode - STABLE or VARIABLE from ReserveLogic.InterestRateMode enum + * @return an address of the corresponding debt token from reserve configuration + **/ + function getDebtTokenAddress(ReserveLogic.ReserveData storage reserve, uint256 interestRateMode) + internal + view + returns (address) + { + require( + ReserveLogic.InterestRateMode.STABLE == ReserveLogic.InterestRateMode(interestRateMode) || + ReserveLogic.InterestRateMode.VARIABLE == ReserveLogic.InterestRateMode(interestRateMode), + Errors.INVALID_INTEREST_RATE_MODE_SELECTED + ); + return + ReserveLogic.InterestRateMode.STABLE == ReserveLogic.InterestRateMode(interestRateMode) + ? reserve.stableDebtTokenAddress + : reserve.variableDebtTokenAddress; + } + + /** + * @dev Updates the liquidity cumulative index Ci and variable borrow cumulative index Bvc. Refer to the whitepaper for + * a formal specification. * @param reserve the reserve object **/ function updateState(ReserveData storage reserve) internal { diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index a5f3f331..84c1d861 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 @@ -100,7 +101,7 @@ library ValidationLogic { /** * @dev validates a borrow. * @param reserve the reserve state from which the user is borrowing - * @param reserveAddress the address of the reserve + * @param userAddress the address of the user * @param amount the amount to be borrowed * @param amountInETH the amount to be borrowed, in ETH * @param interestRateMode the interest rate mode at which the user is borrowing @@ -113,7 +114,7 @@ library ValidationLogic { function validateBorrow( ReserveLogic.ReserveData storage reserve, - address reserveAddress, + address userAddress, uint256 amount, uint256 amountInETH, uint256 interestRateMode, @@ -151,7 +152,7 @@ library ValidationLogic { vars.currentLiquidationThreshold, vars.healthFactor ) = GenericLogic.calculateUserAccountData( - msg.sender, + userAddress, reservesData, userConfig, reserves, @@ -192,7 +193,7 @@ library ValidationLogic { require( !userConfig.isUsingAsCollateral(reserve.id) || reserve.configuration.getLtv() == 0 || - amount > IERC20(reserve.aTokenAddress).balanceOf(msg.sender), + amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress), Errors.CALLATERAL_SAME_AS_BORROWING_CURRENCY ); @@ -329,4 +330,116 @@ library ValidationLogic { require(premium > 0, Errors.REQUESTED_AMOUNT_TOO_SMALL); require(mode <= uint256(ReserveLogic.InterestRateMode.VARIABLE), Errors.INVALID_FLASHLOAN_MODE); } + + /** + * @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 validateLiquidationCall( + ReserveLogic.ReserveData storage collateralReserve, + 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.id); + + //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.id); + + //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); + } } diff --git a/contracts/tokenization/AToken.sol b/contracts/tokenization/AToken.sol index cd270ccb..5285b804 100644 --- a/contracts/tokenization/AToken.sol +++ b/contracts/tokenization/AToken.sol @@ -23,12 +23,19 @@ contract AToken is VersionedInitializable, ERC20, IAToken { using SafeERC20 for ERC20; uint256 public constant UINT_MAX_VALUE = uint256(-1); - uint256 public constant ATOKEN_REVISION = 0x1; address public immutable UNDERLYING_ASSET_ADDRESS; address public immutable RESERVE_TREASURY_ADDRESS; LendingPool public immutable POOL; + /// @dev owner => next valid nonce to submit with permit() + mapping (address => uint256) public _nonces; + uint256 public constant ATOKEN_REVISION = 0x1; + + bytes32 public DOMAIN_SEPARATOR; + bytes public constant EIP712_REVISION = bytes("1"); + bytes32 internal constant EIP712_DOMAIN = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); modifier onlyLendingPool { require(msg.sender == address(POOL), Errors.CALLER_MUST_BE_LENDING_POOL); @@ -56,6 +63,21 @@ contract AToken is VersionedInitializable, ERC20, IAToken { string calldata tokenName, string calldata tokenSymbol ) external virtual initializer { + uint256 chainId; + + //solium-disable-next-line + assembly { + chainId := chainid() + } + + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712_DOMAIN, + keccak256(bytes(tokenName)), + keccak256(EIP712_REVISION), + chainId, + address(this) + )); + _setName(tokenName); _setSymbol(tokenSymbol); _setDecimals(underlyingAssetDecimals); @@ -191,6 +213,42 @@ contract AToken is VersionedInitializable, ERC20, IAToken { return amount; } + /** + * @dev implements the permit function as for https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md + * @param owner the owner of the funds + * @param spender the spender + * @param value the amount + * @param deadline the deadline timestamp, type(uint256).max for max deadline + * @param v signature param + * @param s signature param + * @param r signature param + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + require(owner != address(0), "INVALID_OWNER"); + //solium-disable-next-line + require(block.timestamp <= deadline, "INVALID_EXPIRATION"); + uint256 currentValidNonce = _nonces[owner]; + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256( + abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline)) + ) + ); + require(owner == ecrecover(digest, v, r, s), "INVALID_SIGNATURE"); + _nonces[owner] = currentValidNonce.add(1); + _approve(owner, spender, value); + } + function _transfer( address from, address to, diff --git a/helpers/constants.ts b/helpers/constants.ts index 412d2256..54a10de5 100644 --- a/helpers/constants.ts +++ b/helpers/constants.ts @@ -8,12 +8,16 @@ import { IReserveParams, tEthereumAddress, iBasicDistributionParams, + eEthereumNetwork, } from './types'; import BigNumber from 'bignumber.js'; -import {getParamPerPool} from './contracts-helpers'; +import {getParamPerPool, getParamPerNetwork} from './contracts-helpers'; export const TEST_SNAPSHOT_ID = '0x1'; +export const BUIDLEREVM_CHAINID = 31337; +export const COVERAGE_CHAINID = 1337; + // ---------------- // MATH // ---------------- @@ -531,3 +535,18 @@ export const getFeeDistributionParamsCommon = ( percentages, }; }; + +export const getATokenDomainSeparatorPerNetwork = ( + network: eEthereumNetwork +): tEthereumAddress => + getParamPerNetwork( + { + [eEthereumNetwork.coverage]: "0x95b73a72c6ecf4ccbbba5178800023260bad8e75cdccdb8e4827a2977a37c820", + [eEthereumNetwork.buidlerevm]: + "0x76cbbf8aa4b11a7c207dd79ccf8c394f59475301598c9a083f8258b4fafcfa86", + [eEthereumNetwork.kovan]: "", + [eEthereumNetwork.ropsten]: "", + [eEthereumNetwork.main]: "", + }, + network + ); \ No newline at end of file diff --git a/helpers/contracts-helpers.ts b/helpers/contracts-helpers.ts index c5edc9da..8767fe3d 100644 --- a/helpers/contracts-helpers.ts +++ b/helpers/contracts-helpers.ts @@ -33,6 +33,8 @@ import {StableDebtToken} from '../types/StableDebtToken'; import {VariableDebtToken} from '../types/VariableDebtToken'; import { ZERO_ADDRESS } from './constants'; import {MockSwapAdapter} from '../types/MockSwapAdapter'; +import { signTypedData_v4, TypedData } from "eth-sig-util"; +import { fromRpcSig, ECDSASignature } from "ethereumjs-util"; export const registerContractInJsonDb = async (contractId: string, contractInstance: Contract) => { const currentNetwork = BRE.network.name; @@ -449,10 +451,14 @@ const linkBytecode = (artifact: Artifact, libraries: any) => { }; export const getParamPerNetwork = ( - {kovan, ropsten, main}: iParamsPerNetwork, + {kovan, ropsten, main, buidlerevm, coverage}: iParamsPerNetwork, network: eEthereumNetwork ) => { switch (network) { + case eEthereumNetwork.coverage: + return coverage; + case eEthereumNetwork.buidlerevm: + return buidlerevm; case eEthereumNetwork.kovan: return kovan; case eEthereumNetwork.ropsten: @@ -489,3 +495,59 @@ export const convertToCurrencyUnits = async (tokenAddress: string, amount: strin const amountInCurrencyUnits = new BigNumber(amount).div(currencyUnit); return amountInCurrencyUnits.toFixed(); }; + +export const buildPermitParams = ( + chainId: number, + token: tEthereumAddress, + revision: string, + tokenName: string, + owner: tEthereumAddress, + spender: tEthereumAddress, + nonce: number, + deadline: string, + value: tStringTokenSmallUnits +) => ({ + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + primaryType: "Permit" as const, + domain: { + name: tokenName, + version: revision, + chainId: chainId, + verifyingContract: token, + }, + message: { + owner, + spender, + value, + nonce, + deadline, + }, +}); + + +export const getSignatureFromTypedData = ( + privateKey: string, + typedData: any // TODO: should be TypedData, from eth-sig-utils, but TS doesn't accept it +): ECDSASignature => { + const signature = signTypedData_v4( + Buffer.from(privateKey.substring(2, 66), "hex"), + { + data: typedData, + } + ); + return fromRpcSig(signature); +}; \ No newline at end of file diff --git a/helpers/types.ts b/helpers/types.ts index 083d043e..c965d658 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -5,6 +5,7 @@ export enum eEthereumNetwork { kovan = 'kovan', ropsten = 'ropsten', main = 'main', + coverage = 'coverage' } export enum AavePools { @@ -245,6 +246,8 @@ export interface IMarketRates { } export interface iParamsPerNetwork { + [eEthereumNetwork.coverage]: T; + [eEthereumNetwork.buidlerevm]: T; [eEthereumNetwork.kovan]: T; [eEthereumNetwork.ropsten]: T; [eEthereumNetwork.main]: T; diff --git a/package.json b/package.json index 3da86f23..bfb663df 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "test-repay-with-collateral": "buidler test test/__setup.spec.ts test/repay-with-collateral.spec.ts", "test-liquidate-with-collateral": "buidler test test/__setup.spec.ts test/flash-liquidation-with-collateral.spec.ts", "test-flash": "buidler test test/__setup.spec.ts test/flashloan.spec.ts", + "test-permit": "buidler test test/__setup.spec.ts test/atoken-permit.spec.ts", "dev:coverage": "buidler coverage --network coverage", "dev:deployment": "buidler dev-deployment", "dev:deployExample": "buidler deploy-Example", @@ -58,7 +59,9 @@ "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.3.0", "typechain": "2.0.0", - "typescript": "3.9.3" + "typescript": "3.9.3", + "eth-sig-util": "2.5.3", + "ethereumjs-util": "7.0.2" }, "husky": { "hooks": { diff --git a/test/atoken-permit.spec.ts b/test/atoken-permit.spec.ts new file mode 100644 index 00000000..ef5a39e0 --- /dev/null +++ b/test/atoken-permit.spec.ts @@ -0,0 +1,312 @@ +import { + MAX_UINT_AMOUNT, + ZERO_ADDRESS, + getATokenDomainSeparatorPerNetwork, + BUIDLEREVM_CHAINID, +} from '../helpers/constants'; +import {buildPermitParams, getSignatureFromTypedData} from '../helpers/contracts-helpers'; +import {expect} from 'chai'; +import {ethers} from 'ethers'; +import {eEthereumNetwork} from '../helpers/types'; +import {makeSuite, TestEnv} from './helpers/make-suite'; +import {BRE} from '../helpers/misc-utils'; +import {waitForTx} from './__setup.spec'; + +const {parseEther} = ethers.utils; + +makeSuite('AToken: Permit', (testEnv: TestEnv) => { + it('Checks the domain separator', async () => { + const DOMAIN_SEPARATOR_ENCODED = getATokenDomainSeparatorPerNetwork( + eEthereumNetwork.buidlerevm + ); + + const {aDai} = testEnv; + + const separator = await aDai.DOMAIN_SEPARATOR(); + + expect(separator).to.be.equal(DOMAIN_SEPARATOR_ENCODED, 'Invalid domain separator'); + }); + + it('Get aDAI for tests', async () => { + const {dai, deployer, pool} = testEnv; + + await dai.mint(parseEther('20000')); + await dai.approve(pool.address, parseEther('20000')); + await pool.deposit(dai.address, parseEther('20000'), deployer.address, 0); + }); + + it('Reverts submitting a permit with 0 expiration', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const tokenName = await aDai.name(); + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const expiration = 0; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = ethers.utils.parseEther('2').toString(); + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + tokenName, + owner.address, + spender.address, + nonce, + permitAmount, + expiration.toFixed() + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_BEFORE_PERMIT' + ); + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + aDai + .connect(spender.signer) + .permit(owner.address, spender.address, permitAmount, expiration, v, r, s) + ).to.be.revertedWith('INVALID_EXPIRATION'); + + expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_AFTER_PERMIT' + ); + }); + + it('Submits a permit with maximum expiration length', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = parseEther('2').toString(); + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + deadline, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal( + '0', + 'INVALID_ALLOWANCE_BEFORE_PERMIT' + ); + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await waitForTx( + await aDai + .connect(spender.signer) + .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) + ); + + expect((await aDai._nonces(owner.address)).toNumber()).to.be.equal(1); + }); + + it('Cancels the previous permit', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = '0'; + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + deadline, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal( + ethers.utils.parseEther('2'), + 'INVALID_ALLOWANCE_BEFORE_PERMIT' + ); + + await waitForTx( + await aDai + .connect(spender.signer) + .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) + ); + expect((await aDai.allowance(owner.address, spender.address)).toString()).to.be.equal( + permitAmount, + 'INVALID_ALLOWANCE_AFTER_PERMIT' + ); + + expect((await aDai._nonces(owner.address)).toNumber()).to.be.equal(2); + }); + + it('Tries to submit a permit with invalid nonce', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = 1000; + const permitAmount = '0'; + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + deadline, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + aDai + .connect(spender.signer) + .permit(owner.address, spender.address, permitAmount, deadline, v, r, s) + ).to.be.revertedWith('INVALID_SIGNATURE'); + }); + + it('Tries to submit a permit with invalid expiration (previous to the current block)', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const expiration = '1'; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = '0'; + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + expiration, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + aDai + .connect(spender.signer) + .permit(owner.address, spender.address, expiration, permitAmount, v, r, s) + ).to.be.revertedWith('INVALID_EXPIRATION'); + }); + + it('Tries to submit a permit with invalid signature', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const deadline = MAX_UINT_AMOUNT; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = '0'; + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + deadline, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + aDai + .connect(spender.signer) + .permit(owner.address, ZERO_ADDRESS, permitAmount, deadline, v, r, s) + ).to.be.revertedWith('INVALID_SIGNATURE'); + }); + + it('Tries to submit a permit with invalid owner', async () => { + const {aDai, deployer, users} = testEnv; + const owner = deployer; + const spender = users[1]; + + const chainId = BRE.network.config.chainId || BUIDLEREVM_CHAINID; + const expiration = MAX_UINT_AMOUNT; + const nonce = (await aDai._nonces(owner.address)).toNumber(); + const permitAmount = '0'; + const msgParams = buildPermitParams( + chainId, + aDai.address, + '1', + await aDai.name(), + owner.address, + spender.address, + nonce, + expiration, + permitAmount + ); + + const ownerPrivateKey = require('../test-wallets.js').accounts[0].secretKey; + if (!ownerPrivateKey) { + throw new Error('INVALID_OWNER_PK'); + } + + const {v, r, s} = getSignatureFromTypedData(ownerPrivateKey, msgParams); + + await expect( + aDai + .connect(spender.signer) + .permit(ZERO_ADDRESS, spender.address, expiration, permitAmount, v, r, s) + ).to.be.revertedWith('INVALID_OWNER'); + }); +}); diff --git a/test/atoken-transfer.spec.ts b/test/atoken-transfer.spec.ts index 73c299ad..a5c13989 100644 --- a/test/atoken-transfer.spec.ts +++ b/test/atoken-transfer.spec.ts @@ -60,7 +60,13 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { await expect( pool .connect(users[1].signer) - .borrow(weth.address, ethers.utils.parseEther('0.1'), RateMode.Stable, AAVE_REFERRAL), + .borrow( + weth.address, + ethers.utils.parseEther('0.1'), + RateMode.Stable, + AAVE_REFERRAL, + users[1].address + ), COLLATERAL_BALANCE_IS_0 ).to.be.revertedWith(COLLATERAL_BALANCE_IS_0); }); @@ -73,7 +79,13 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { await pool .connect(users[1].signer) - .borrow(weth.address, ethers.utils.parseEther('0.1'), RateMode.Stable, AAVE_REFERRAL); + .borrow( + weth.address, + ethers.utils.parseEther('0.1'), + RateMode.Stable, + AAVE_REFERRAL, + users[1].address + ); await expect( aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer), diff --git a/test/flash-liquidation-with-collateral.spec.ts b/test/flash-liquidation-with-collateral.spec.ts index be1db606..87ad829d 100644 --- a/test/flash-liquidation-with-collateral.spec.ts +++ b/test/flash-liquidation-with-collateral.spec.ts @@ -47,9 +47,9 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn const usdcPrice = await oracle.getAssetPrice(usdc.address); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0, user.address); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 1, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 1, 0, user.address); const {userData: wethUserDataBefore} = await getContractsData( weth.address, @@ -203,7 +203,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn .toFixed(0) ); - await pool.connect(user.signer).borrow(usdc.address, amountUSDCToBorrow, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountUSDCToBorrow, 2, 0, user.address); }); it('User 5 liquidates half the USDC loan of User 3 by swapping his WETH collateral', async () => { @@ -464,7 +464,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn .toFixed(0) ); - await pool.connect(user.signer).borrow(dai.address, amountDAIToBorrow, 2, 0); + await pool.connect(user.signer).borrow(dai.address, amountDAIToBorrow, 2, 0, user.address); }); it('It is not possible to do reentrancy on repayWithCollateral()', async () => { @@ -736,7 +736,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn await pool.connect(user.signer).deposit(weth.address, amountToDepositWeth, user.address, '0'); await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, user.address, '0'); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0, user.address); const amountToRepay = amountToBorrowVariable; @@ -844,7 +844,7 @@ makeSuite('LendingPool. repayWithCollateral() with liquidator', (testEnv: TestEn await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.connect(user.signer).deposit(dai.address, amountDAIToDeposit, user.address, '0'); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0, user.address); }); it('Liquidator tries to liquidates User 5 USDC loan by swapping his WETH collateral, should revert due WETH collateral disabled', async () => { diff --git a/test/helpers/actions.ts b/test/helpers/actions.ts index 1e8dd6db..3f9fea4b 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -283,11 +283,41 @@ export const withdraw = async ( } }; +export const delegateBorrowAllowance = async ( + reserveSymbol: string, + amount: string, + interestRateMode: string, + user: SignerWithAddress, + receiver: tEthereumAddress, + expectedResult: string, + testEnv: TestEnv, + revertMessage?: string +) => { + const {pool} = testEnv; + + const reserve = await getReserveAddressFromSymbol(reserveSymbol); + const amountToDelegate = await convertToCurrencyDecimals(reserve, amount); + + const delegateAllowancePromise = pool + .connect(user.signer) + .delegateBorrowAllowance(reserve, receiver, interestRateMode, amountToDelegate.toString()); + if (expectedResult === 'revert') { + await expect(delegateAllowancePromise, revertMessage).to.be.reverted; + return; + } else { + await delegateAllowancePromise; + expect( + (await pool.getBorrowAllowance(user.address, receiver, reserve, interestRateMode)).toString() + ).to.be.equal(amountToDelegate.toString(), 'borrowAllowance are set incorrectly'); + } +}; + export const borrow = async ( reserveSymbol: string, amount: string, interestRateMode: string, user: SignerWithAddress, + onBehalfOf: tEthereumAddress, timeTravel: string, expectedResult: string, testEnv: TestEnv, @@ -299,15 +329,18 @@ export const borrow = async ( const {reserveData: reserveDataBefore, userData: userDataBefore} = await getContractsData( reserve, - user.address, - testEnv + onBehalfOf, + testEnv, + user.address ); const amountToBorrow = await convertToCurrencyDecimals(reserve, amount); if (expectedResult === 'success') { const txResult = await waitForTx( - await pool.connect(user.signer).borrow(reserve, amountToBorrow, interestRateMode, '0') + await pool + .connect(user.signer) + .borrow(reserve, amountToBorrow, interestRateMode, '0', onBehalfOf) ); const {txCost, txTimestamp} = await getTxCostAndTimestamp(txResult); @@ -322,7 +355,7 @@ export const borrow = async ( reserveData: reserveDataAfter, userData: userDataAfter, timestamp, - } = await getContractsData(reserve, user.address, testEnv); + } = await getContractsData(reserve, onBehalfOf, testEnv, user.address); const expectedReserveData = calcExpectedReserveDataAfterBorrow( amountToBorrow.toString(), @@ -369,7 +402,7 @@ export const borrow = async ( // }); } else if (expectedResult === 'revert') { await expect( - pool.connect(user.signer).borrow(reserve, amountToBorrow, interestRateMode, '0'), + pool.connect(user.signer).borrow(reserve, amountToBorrow, interestRateMode, '0', onBehalfOf), revertMessage ).to.be.reverted; } diff --git a/test/helpers/scenario-engine.ts b/test/helpers/scenario-engine.ts index 8618de3d..6a03a4d1 100644 --- a/test/helpers/scenario-engine.ts +++ b/test/helpers/scenario-engine.ts @@ -8,7 +8,8 @@ import { repay, setUseAsCollateral, swapBorrowRateMode, - rebalanceStableBorrowRate + rebalanceStableBorrowRate, + delegateBorrowAllowance, } from './actions'; import {RateMode} from '../../helpers/types'; @@ -59,7 +60,7 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv if (borrowRateMode) { if (borrowRateMode === 'none') { - RateMode.None; + rateMode = RateMode.None; } else if (borrowRateMode === 'stable') { rateMode = RateMode.Stable; } else if (borrowRateMode === 'variable') { @@ -111,6 +112,27 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv } break; + case 'delegateBorrowAllowance': + { + const {amount, toUser: toUserIndex} = action.args; + const toUser = users[parseInt(toUserIndex, 10)].address; + if (!amount || amount === '') { + throw `Invalid amount to deposit into the ${reserve} reserve`; + } + + await delegateBorrowAllowance( + reserve, + amount, + rateMode, + user, + toUser, + expected, + testEnv, + revertMessage + ); + } + break; + case 'withdraw': { const {amount} = action.args; @@ -124,13 +146,27 @@ const executeAction = async (action: Action, users: SignerWithAddress[], testEnv break; case 'borrow': { - const {amount, timeTravel} = action.args; + const {amount, timeTravel, onBehalfOf: onBehalfOfIndex} = action.args; + + const onBehalfOf = onBehalfOfIndex + ? users[parseInt(onBehalfOfIndex)].address + : user.address; if (!amount || amount === '') { throw `Invalid amount to borrow from the ${reserve} reserve`; } - await borrow(reserve, amount, rateMode, user, timeTravel, expected, testEnv, revertMessage); + await borrow( + reserve, + amount, + rateMode, + user, + onBehalfOf, + timeTravel, + expected, + testEnv, + revertMessage + ); } break; diff --git a/test/helpers/scenarios/credit-delegation.json b/test/helpers/scenarios/credit-delegation.json new file mode 100644 index 00000000..a67924ee --- /dev/null +++ b/test/helpers/scenarios/credit-delegation.json @@ -0,0 +1,148 @@ +{ + "title": "LendingPool: credit delegation", + "description": "Test cases for the credit delegation related functions.", + "stories": [ + { + "description": "User 0 deposits 1000 DAI, user 0 delegates borrowing of 1 WETH on variable to user 4, user 4 borrows 1 WETH variable on behalf of user 0", + "actions": [ + { + "name": "mint", + "args": { + "reserve": "WETH", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "approve", + "args": { + "reserve": "WETH", + "user": "0" + }, + "expected": "success" + }, + { + "name": "deposit", + "args": { + "reserve": "WETH", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "2", + "user": "0", + "borrowRateMode": "variable", + "toUser": "4" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "4", + "onBehalfOf": "0", + "borrowRateMode": "variable" + }, + "expected": "success" + } + ] + }, + { + "description": "User 4 trying to borrow 1 WETH stable on behalf of user 0, revert expected", + "actions": [ + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "4", + "onBehalfOf": "0", + "borrowRateMode": "stable" + }, + "expected": "revert", + "revertMessage": "54" + } + ] + }, + { + "description": "User 0 delegates borrowing of 1 WETH to user 4, user 4 borrows 3 WETH variable on behalf of user 0, revert expected", + "actions": [ + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "0", + "borrowRateMode": "variable", + "toUser": "4" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "3", + "user": "4", + "onBehalfOf": "0", + "borrowRateMode": "variable" + }, + "expected": "revert", + "revertMessage": "54" + } + ] + }, + { + "description": "User 0 delegates borrowing of 1 WETH on stable to user 2, user 2 borrows 1 WETH stable on behalf of user 0", + "actions": [ + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "0", + "borrowRateMode": "stable", + "toUser": "2" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "2", + "onBehalfOf": "0", + "borrowRateMode": "stable" + }, + "expected": "success" + } + ] + }, + { + "description": "User 0 delegates borrowing of 1 WETH to user 2 with wrong borrowRateMode, revert expected", + "actions": [ + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "0", + "borrowRateMode": "random", + "toUser": "2" + }, + "expected": "revert", + "revertMessage": "8" + } + ] + } + ] +} diff --git a/test/liquidation-atoken.spec.ts b/test/liquidation-atoken.spec.ts index 342c2a63..15f3d12d 100644 --- a/test/liquidation-atoken.spec.ts +++ b/test/liquidation-atoken.spec.ts @@ -63,7 +63,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => await pool .connect(borrower.signer) - .borrow(dai.address, amountDAIToBorrow, RateMode.Variable, '0'); + .borrow(dai.address, amountDAIToBorrow, RateMode.Variable, '0', borrower.address); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); @@ -261,7 +261,7 @@ makeSuite('LendingPool liquidation - liquidator receiving aToken', (testEnv) => await pool .connect(borrower.signer) - .borrow(usdc.address, amountUSDCToBorrow, RateMode.Stable, '0'); + .borrow(usdc.address, amountUSDCToBorrow, RateMode.Stable, '0', borrower.address); //drops HF below 1 diff --git a/test/liquidation-underlying.spec.ts b/test/liquidation-underlying.spec.ts index 3f2aecbc..26f10ced 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]; @@ -68,7 +89,7 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', await pool .connect(borrower.signer) - .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0'); + .borrow(dai.address, amountDAIToBorrow, RateMode.Stable, '0', borrower.address); const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); @@ -240,7 +261,7 @@ makeSuite('LendingPool liquidation - liquidator receiving the underlying asset', await pool .connect(borrower.signer) - .borrow(usdc.address, amountUSDCToBorrow, RateMode.Stable, '0'); + .borrow(usdc.address, amountUSDCToBorrow, RateMode.Stable, '0', borrower.address); //drops HF below 1 await oracle.setAssetPrice( diff --git a/test/repay-with-collateral.spec.ts b/test/repay-with-collateral.spec.ts index faefa403..abe7b680 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; @@ -65,7 +103,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await pool.connect(user.signer).deposit(weth.address, amountToDeposit, user.address, '0'); - await pool.connect(user.signer).borrow(dai.address, amountToBorrow, 2, 0); + await pool.connect(user.signer).borrow(dai.address, amountToBorrow, 2, 0, user.address); }); it('It is not possible to do reentrancy on repayWithCollateral()', async () => { @@ -187,7 +225,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await pool.connect(user.signer).deposit(weth.address, amountToDeposit, user.address, '0'); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0, user.address); }); it('User 3 repays completely his USDC loan by swapping his WETH collateral', async () => { @@ -309,9 +347,9 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await pool.connect(user.signer).deposit(weth.address, amountToDeposit, user.address, '0'); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowVariable, 2, 0, user.address); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrowStable, 1, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrowStable, 1, 0, user.address); const amountToRepay = parseUnits('80', 6); @@ -450,7 +488,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await pool.connect(user.signer).deposit(weth.address, amountToDepositWeth, user.address, '0'); await pool.connect(user.signer).deposit(dai.address, amountToDepositDAI, user.address, '0'); - await pool.connect(user.signer).borrow(dai.address, amountToBorrowVariable, 2, 0); + await pool.connect(user.signer).borrow(dai.address, amountToBorrowVariable, 2, 0, user.address); const amountToRepay = parseEther('80'); @@ -542,7 +580,7 @@ makeSuite('LendingPool. repayWithCollateral()', (testEnv: TestEnv) => { await dai.connect(user.signer).approve(pool.address, APPROVAL_AMOUNT_LENDING_POOL); await pool.connect(user.signer).deposit(dai.address, amountDAIToDeposit, user.address, '0'); - await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0); + await pool.connect(user.signer).borrow(usdc.address, amountToBorrow, 2, 0, user.address); }); it('User 5 tries to repay his USDC loan by swapping his WETH collateral, should not revert even with WETH collateral disabled', async () => {