diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index a7a5e1ca..bfeffc74 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -28,6 +28,12 @@ interface ILendingPool { **/ event Withdraw(address indexed reserve, address indexed user, uint256 amount); + event BorrowAllowanceDelegated( + address indexed fromUser, + address indexed toUser, + address indexed asset, + uint256 amount + ); /** * @dev emitted on borrow * @param reserve the address of the reserve @@ -39,7 +45,8 @@ interface ILendingPool { **/ event Borrow( address indexed reserve, - address indexed user, + address user, + address indexed onBehalfOf, uint256 amount, uint256 borrowRateMode, uint256 borrowRate, @@ -149,6 +156,18 @@ interface ILendingPool { **/ function withdraw(address reserve, uint256 amount) external; + function delegateBorrowAllowance( + address user, + address asset, + uint256 amount + ) external; + + function getBorrowAllowance( + address fromUser, + address toUser, + address asset + ) external view returns (uint256); + /** * @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower * already deposited enough collateral. @@ -160,7 +179,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 9e4e6707..5f2f8158 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -48,7 +48,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { mapping(address => ReserveLogic.ReserveData) internal _reserves; mapping(address => UserConfiguration.Map) internal _usersConfig; - + mapping(address => mapping(address => mapping(address => uint256))) internal _borrowAllowance; address[] internal _reservesList; /** @@ -157,6 +157,23 @@ contract LendingPool is VersionedInitializable, ILendingPool { emit Withdraw(asset, msg.sender, amount); } + function getBorrowAllowance( + address fromUser, + address toUser, + address asset + ) external override view returns (uint256) { + return _borrowAllowance[fromUser][asset][toUser]; + } + + function delegateBorrowAllowance( + address user, + address asset, + uint256 amount + ) external override { + _borrowAllowance[msg.sender][asset][user] = amount; + emit BorrowAllowanceDelegated(msg.sender, user, asset, amount); + } + /** * @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower * already deposited enough collateral. @@ -169,12 +186,19 @@ contract LendingPool is VersionedInitializable, ILendingPool { address asset, uint256 amount, uint256 interestRateMode, - uint16 referralCode + uint16 referralCode, + address onBehalfOf ) external override { + if (onBehalfOf != msg.sender) { + _borrowAllowance[onBehalfOf][asset][msg.sender] = _borrowAllowance[onBehalfOf][asset][msg + .sender] + .sub(amount, Errors.BORROW_ALLOWANCE_ARE_NOT_ENOUGH); + } _executeBorrow( ExecuteBorrowParams( asset, msg.sender, + onBehalfOf, amount, interestRateMode, _reserves[asset].aTokenAddress, @@ -450,21 +474,20 @@ 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( ExecuteBorrowParams( asset, msg.sender, + msg.sender, vars.amountPlusPremium.sub(vars.availableBalance), mode, vars.aTokenAddress, @@ -694,6 +717,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { struct ExecuteBorrowParams { address asset; address user; + address onBehalfOf; uint256 amount; uint256 interestRateMode; address aTokenAddress; @@ -707,7 +731,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(); @@ -717,6 +741,7 @@ contract LendingPool is VersionedInitializable, ILendingPool { ValidationLogic.validateBorrow( reserve, + vars.onBehalfOf, vars.asset, vars.amount, amountInETH, @@ -728,13 +753,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 @@ -746,24 +769,29 @@ contract LendingPool is VersionedInitializable, ILendingPool { currentStableRate = reserve.currentStableBorrowRate; IStableDebtToken(reserve.stableDebtTokenAddress).mint( - vars.user, + vars.onBehalfOf, vars.amount, currentStableRate ); } else { - IVariableDebtToken(reserve.variableDebtTokenAddress).mint(vars.user, vars.amount); + IVariableDebtToken(reserve.variableDebtTokenAddress).mint(vars.onBehalfOf, 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(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/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 974aa5e1..ba3ee2b9 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 = '52'; // User borrows on behalf, but allowance are too small // require error messages - aToken string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool' @@ -70,7 +71,7 @@ library Errors { string public constant NO_ERRORS = '42'; // 'No errors' //require error messages - Math libraries - string public constant MULTIPLICATION_OVERFLOW = '44'; - string public constant ADDITION_OVERFLOW = '45'; + string public constant MULTIPLICATION_OVERFLOW = '44'; + string public constant ADDITION_OVERFLOW = '45'; string public constant DIVISION_BY_ZERO = '46'; } diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 7a640458..69c4c4b6 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -100,6 +100,7 @@ library ValidationLogic { /** * @dev validates a borrow. * @param reserve the reserve state from which the user is borrowing + * @param userAddress the address of the user * @param reserveAddress the address of the reserve * @param amount the amount to be borrowed * @param amountInETH the amount to be borrowed, in ETH @@ -113,6 +114,7 @@ library ValidationLogic { function validateBorrow( ReserveLogic.ReserveData storage reserve, + address userAddress, address reserveAddress, uint256 amount, uint256 amountInETH, @@ -151,7 +153,7 @@ library ValidationLogic { vars.currentLiquidationThreshold, vars.healthFactor ) = GenericLogic.calculateUserAccountData( - msg.sender, + userAddress, reservesData, userConfig, reserves, @@ -192,7 +194,7 @@ library ValidationLogic { require( !userConfig.isUsingAsCollateral(reserve.index) || reserve.configuration.getLtv() == 0 || - amount > IERC20(reserve.aTokenAddress).balanceOf(msg.sender), + amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress), Errors.CALLATERAL_SAME_AS_BORROWING_CURRENCY ); @@ -321,10 +323,10 @@ library ValidationLogic { } /** - * @dev validates a flashloan action - * @param mode the flashloan mode (0 = classic flashloan, 1 = open a stable rate loan, 2 = open a variable rate loan) - * @param premium the premium paid on the flashloan - **/ + * @dev validates a flashloan action + * @param mode the flashloan mode (0 = classic flashloan, 1 = open a stable rate loan, 2 = open a variable rate loan) + * @param premium the premium paid on the flashloan + **/ function validateFlashloan(uint256 mode, uint256 premium) internal pure { require(premium > 0, Errors.REQUESTED_AMOUNT_TOO_SMALL); require(mode <= uint256(ReserveLogic.InterestRateMode.VARIABLE), Errors.INVALID_FLASHLOAN_MODE); diff --git a/deployed-contracts.json b/deployed-contracts.json index d04816b9..56111eb8 100644 --- a/deployed-contracts.json +++ b/deployed-contracts.json @@ -111,7 +111,7 @@ }, "DefaultReserveInterestRateStrategy": { "buidlerevm": { - "address": "0x626FdE749F9d499d3777320CAf29484B624ab84a", + "address": "0x2530ce07D254eA185E8e0bCC37a39e2FbA3bE548", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -422,7 +422,7 @@ }, "StableDebtToken": { "buidlerevm": { - "address": "0xB660Fdd109a95718cB9d20E3A89EE6cE342aDcB6", + "address": "0x0Cf45557d25a4e4c0F1aC65EF6c48ae67c61a0E6", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -432,7 +432,7 @@ }, "VariableDebtToken": { "buidlerevm": { - "address": "0x830bceA96E56DBC1F8578f75fBaC0AF16B32A07d", + "address": "0x7fAeC7791277Ff512c41CA903c177B2Ed952dDAc", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "localhost": { @@ -446,7 +446,7 @@ "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" }, "buidlerevm": { - "address": "0xA0AB1cB92A4AF81f84dCd258155B5c25D247b54E", + "address": "0x33958cC3535Fc328369EAC2B2Bebd120D67C7fa1", "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" } }, diff --git a/test/atoken-transfer.spec.ts b/test/atoken-transfer.spec.ts index 1c161608..c986fb9b 100644 --- a/test/atoken-transfer.spec.ts +++ b/test/atoken-transfer.spec.ts @@ -103,7 +103,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); }); @@ -116,7 +122,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/helpers/actions.ts b/test/helpers/actions.ts index be0daac5..c8e93d2d 100644 --- a/test/helpers/actions.ts +++ b/test/helpers/actions.ts @@ -278,11 +278,34 @@ export const withdraw = async ( } }; +export const delegateBorrowAllowance = async ( + reserveSymbol: string, + amount: string, + user: SignerWithAddress, + receiver: tEthereumAddress, + testEnv: TestEnv +) => { + const {pool} = testEnv; + + const reserve = await getReserveAddressFromSymbol(reserveSymbol); + const amountToDelegate = await convertToCurrencyDecimals(reserve, amount); + + await pool + .connect(user.signer) + .delegateBorrowAllowance(receiver, reserve, amountToDelegate.toString()); + expect( + ( + await pool['getBorrowAllowance(address,address,address)'](user.address, receiver, reserve) + ).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, @@ -294,15 +317,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); @@ -317,7 +343,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(), @@ -364,7 +390,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; } @@ -845,10 +871,15 @@ const getTxCostAndTimestamp = async (tx: ContractReceipt) => { return {txCost, txTimestamp}; }; -const getContractsData = async (reserve: string, user: string, testEnv: TestEnv) => { +const getContractsData = async ( + reserve: string, + user: string, + testEnv: TestEnv, + sender?: string +) => { const {pool} = testEnv; const reserveData = await getReserveData(pool, reserve); - const userData = await getUserData(pool, reserve, user); + const userData = await getUserData(pool, reserve, user, sender || user); const timestamp = await timeLatest(); return { diff --git a/test/helpers/scenario-engine.ts b/test/helpers/scenario-engine.ts index 735d3b84..8092d671 100644 --- a/test/helpers/scenario-engine.ts +++ b/test/helpers/scenario-engine.ts @@ -12,6 +12,7 @@ import { redirectInterestStream, redirectInterestStreamOf, allowInterestRedirectionTo, + delegateBorrowAllowance, } from './actions'; import {RateMode} from '../../helpers/types'; @@ -102,6 +103,18 @@ 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, user, toUser, testEnv); + } + break; + case 'withdraw': { const {amount} = action.args; @@ -115,13 +128,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/borrow-repay-variable.json b/test/helpers/scenarios/borrow-repay-variable.json index 46c17610..6440c822 100644 --- a/test/helpers/scenarios/borrow-repay-variable.json +++ b/test/helpers/scenarios/borrow-repay-variable.json @@ -955,6 +955,85 @@ "expected": "success" } ] + }, + { + "description": "User 0 deposits 1000 DAI, user 0 delegates borrowing of 1 WETH to user 4, user 4 borrows 1 WETH on behalf of user 0", + "actions": [ + { + "name": "mint", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "approve", + "args": { + "reserve": "DAI", + "user": "0" + }, + "expected": "success" + }, + { + "name": "deposit", + "args": { + "reserve": "DAI", + "amount": "1000", + "user": "0" + }, + "expected": "success" + }, + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "0", + "toUser": "4" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "4", + "onBehalfOf": "0", + "borrowRateMode": "variable" + }, + "expected": "success" + } + ] + }, + { + "description": "User 0 delegates borrowing of 1 WETH to user 4, user 4 borrows 2 WETH on behalf of user 0, revert expected", + "actions": [ + { + "name": "delegateBorrowAllowance", + "args": { + "reserve": "WETH", + "amount": "1", + "user": "0", + "toUser": "4" + }, + "expected": "success" + }, + { + "name": "borrow", + "args": { + "reserve": "WETH", + "amount": "2", + "user": "4", + "onBehalfOf": "0", + "borrowRateMode": "variable" + }, + "expected": "revert", + "revertMessage": "52" + } + ] } ] } diff --git a/test/helpers/utils/helpers.ts b/test/helpers/utils/helpers.ts index c20c67ac..9dac685b 100644 --- a/test/helpers/utils/helpers.ts +++ b/test/helpers/utils/helpers.ts @@ -61,7 +61,8 @@ export const getReserveData = async ( export const getUserData = async ( pool: LendingPool, reserve: string, - user: string + user: tEthereumAddress, + sender?: tEthereumAddress ): Promise => { const [userData, aTokenData] = await Promise.all([ pool.getUserReserveData(reserve, user), @@ -77,7 +78,7 @@ export const getUserData = async ( ] = aTokenData; const token = await getMintableErc20(reserve); - const walletBalance = new BigNumber((await token.balanceOf(user)).toString()); + const walletBalance = new BigNumber((await token.balanceOf(sender || user)).toString()); return { principalATokenBalance: new BigNumber(principalATokenBalance), diff --git a/test/liquidation-atoken.spec.ts b/test/liquidation-atoken.spec.ts index 921114f0..9c01b735 100644 --- a/test/liquidation-atoken.spec.ts +++ b/test/liquidation-atoken.spec.ts @@ -59,7 +59,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); @@ -252,7 +252,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 064e3856..5c46e403 100644 --- a/test/liquidation-underlying.spec.ts +++ b/test/liquidation-underlying.spec.ts @@ -56,7 +56,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); @@ -222,7 +222,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(