diff --git a/contracts/mocks/tests/BorrowRepayTestMock.sol b/contracts/mocks/tests/BorrowRepayTestMock.sol new file mode 100644 index 00000000..dda8a428 --- /dev/null +++ b/contracts/mocks/tests/BorrowRepayTestMock.sol @@ -0,0 +1,51 @@ +pragma solidity 0.6.12; + +import {ILendingPool} from '../../interfaces/ILendingPool.sol'; +import {MintableERC20} from '../tokens/MintableERC20.sol'; + +contract BorrowRepayTestMock { + ILendingPool _pool; + address _weth; + address _dai; + + constructor(ILendingPool pool, address weth, address dai) public { + _pool = pool; + _weth = weth; + _dai = dai; + } + + function executeBorrowRepayVariable() external { + //mints 1 eth + MintableERC20(_weth).mint(1e18); + //deposits weth in the protocol + MintableERC20(_weth).approve(address(_pool),type(uint256).max); + _pool.deposit(_weth, 1e18, address(this),0); + //borrow 1 wei of weth at variable + _pool.borrow(_weth, 1, 2, 0, address(this)); + //repay 1 wei of weth (expected to fail) + _pool.repay(_weth, 1, 2, address(this)); + } + + function executeBorrowRepayStable() external { + //mints 1 eth + MintableERC20(_weth).mint(1e18); + //mints 1 dai + MintableERC20(_dai).mint(1e18); + //deposits weth in the protocol + MintableERC20(_weth).approve(address(_pool),type(uint256).max); + _pool.deposit(_weth, 1e18, address(this),0); + + //deposits dai in the protocol + MintableERC20(_dai).approve(address(_pool),type(uint256).max); + _pool.deposit(_dai, 1e18, address(this),0); + + //disabling dai as collateral so it can be borrowed at stable + _pool.setUserUseReserveAsCollateral(_dai, false); + //borrow 1 wei of dai at stable + _pool.borrow(_dai, 1, 1, 0, address(this)); + + //repay 1 wei of dai (expected to fail) + _pool.repay(_dai, 1, 1, address(this)); + } + +} diff --git a/contracts/protocol/lendingpool/LendingPool.sol b/contracts/protocol/lendingpool/LendingPool.sol index c88aecad..2396d8f5 100644 --- a/contracts/protocol/lendingpool/LendingPool.sol +++ b/contracts/protocol/lendingpool/LendingPool.sol @@ -240,7 +240,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage ///@inheritdoc ILendingPool function rebalanceStableBorrowRate(address asset, address user) external override whenNotPaused { - DataTypes.ReserveData storage reserve = _reserves[asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); @@ -793,6 +792,9 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage vars.releaseUnderlying ? vars.amount : 0 ); + _lastBorrower = vars.user; + _lastBorrowTimestamp = uint40(block.timestamp); + if (vars.releaseUnderlying) { IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount); } @@ -908,6 +910,8 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage DataTypes.InterestRateMode interestRateMode = DataTypes.InterestRateMode(rateMode); ValidationLogic.validateRepay( + _lastBorrower, + _lastBorrowTimestamp, reserveCache, amount, interestRateMode, diff --git a/contracts/protocol/lendingpool/LendingPoolStorage.sol b/contracts/protocol/lendingpool/LendingPoolStorage.sol index b8516bbf..6289e008 100644 --- a/contracts/protocol/lendingpool/LendingPoolStorage.sol +++ b/contracts/protocol/lendingpool/LendingPoolStorage.sol @@ -33,4 +33,8 @@ contract LendingPoolStorage { mapping(address => bool) _authorizedFlashBorrowers; uint256 internal _flashLoanPremiumToProtocol; + + address internal _lastBorrower; + + uint40 internal _lastBorrowTimestamp; } diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index 8f576c40..8635ba8f 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -113,6 +113,7 @@ library Errors { string public constant RL_STABLE_DEBT_NOT_ZERO = '89'; string public constant RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90'; string public constant LP_CALLER_NOT_EOA = '91'; + string public constant VL_SAME_BLOCK_BORROW_REPAY = '94'; string public constant LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95'; string public constant LPC_FLASHLOAN_PREMIUM_INVALID = '96'; diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index 7f64887a..9c7ea35a 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -255,6 +255,8 @@ library ValidationLogic { * @param variableDebt The borrow balance of the user */ function validateRepay( + address lastBorrower, + uint40 lastBorrowTimestamp, DataTypes.ReserveCache memory reserveCache, uint256 amountSent, DataTypes.InterestRateMode rateMode, @@ -268,6 +270,11 @@ library ValidationLogic { require(amountSent > 0, Errors.VL_INVALID_AMOUNT); + require( + lastBorrower != onBehalfOf || lastBorrowTimestamp != uint40(block.timestamp), + Errors.VL_SAME_BLOCK_BORROW_REPAY + ); + require( (stableDebt > 0 && DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) || @@ -347,7 +354,6 @@ library ValidationLogic { IERC20 variableDebtToken, address aTokenAddress ) external view { - // to avoid potential abuses using flashloans, the rebalance stable rate must happen through an EOA require(!address(msg.sender).isContract(), Errors.LP_CALLER_NOT_EOA); diff --git a/helpers/types.ts b/helpers/types.ts index 360050e5..d80e51e4 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -187,6 +187,7 @@ export enum ProtocolErrors { RL_ATOKEN_SUPPLY_NOT_ZERO = '88', RL_STABLE_DEBT_NOT_ZERO = '89', RL_VARIABLE_DEBT_SUPPLY_NOT_ZERO = '90', + VL_SAME_BLOCK_BORROW_REPAY = '94', LPC_FLASHLOAN_PREMIUMS_MISMATCH = '95', LPC_FLASHLOAN_PREMIUM_INVALID = '96', diff --git a/test-suites/test-aave/borrow-repay-same-tx.ts b/test-suites/test-aave/borrow-repay-same-tx.ts new file mode 100644 index 00000000..d1fed4f0 --- /dev/null +++ b/test-suites/test-aave/borrow-repay-same-tx.ts @@ -0,0 +1,52 @@ +import { TestEnv, makeSuite } from './helpers/make-suite'; +import { + APPROVAL_AMOUNT_LENDING_POOL, + MAX_UINT_AMOUNT, + RAY, + MAX_BORROW_CAP, + MAX_SUPPLY_CAP, +} from '../../helpers/constants'; +import { ProtocolErrors } from '../../helpers/types'; +import { + BorrowRepayTestMock, + BorrowRepayTestMockFactory, + MintableERC20, + WETH9, + WETH9Mocked, +} from '../../types'; +import { parseEther } from '@ethersproject/units'; +import { BigNumber } from '@ethersproject/bignumber'; +import { waitForTx } from '../../helpers/misc-utils'; +import { getFirstSigner } from '../../helpers/contracts-getters'; + +const { expect } = require('chai'); + +makeSuite('Borrow/repay in the same tx', (testEnv: TestEnv) => { + const { VL_SAME_BLOCK_BORROW_REPAY } = ProtocolErrors; + const unitParse = async (token: WETH9Mocked | MintableERC20, nb: string) => + BigNumber.from(nb).mul(BigNumber.from('10').pow((await token.decimals()) - 3)); + + let testContract: BorrowRepayTestMock; + + it('Deploys the test contract', async () => { + const { weth, dai, pool } = testEnv; + + testContract = await ( + await new BorrowRepayTestMockFactory(await getFirstSigner()) + ).deploy(pool.address, weth.address, dai.address); + }); + + it('Executes a test borrow/repay in the same transaction at variable (revert expected)', async () => { + await expect(testContract.executeBorrowRepayVariable()).to.be.revertedWith( + VL_SAME_BLOCK_BORROW_REPAY, + 'Borrow/repay in the same transaction did not revert as expected' + ); + }); + + it('Executes a test borrow/repay in the same transaction at stabke (revert expected)', async () => { + await expect(testContract.executeBorrowRepayStable()).to.be.revertedWith( + VL_SAME_BLOCK_BORROW_REPAY, + 'Borrow/repay in the same transaction did not revert as expected' + ); + }); +});