diff --git a/contracts/interfaces/ILendingPool.sol b/contracts/interfaces/ILendingPool.sol index f44a458a..b893c16b 100644 --- a/contracts/interfaces/ILendingPool.sol +++ b/contracts/interfaces/ILendingPool.sol @@ -349,11 +349,14 @@ interface ILendingPool { function getReserveData(address asset) external view returns (ReserveLogic.ReserveData memory); - function balanceDecreaseAllowed( - address reserve, - address user, - uint256 amount - ) external view returns (bool); + function finalizeTransfer( + address asset, + address from, + address to, + uint256 amount, + uint256 balanceFromAfter, + uint256 balanceToBefore + ) external; function getReservesList() external view returns (address[] memory); diff --git a/contracts/lendingpool/LendingPool.sol b/contracts/lendingpool/LendingPool.sol index b0f960b3..1684247e 100644 --- a/contracts/lendingpool/LendingPool.sol +++ b/contracts/lendingpool/LendingPool.sol @@ -724,29 +724,48 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage } /** - * @dev validate if a balance decrease for an asset is allowed + * @dev validates and finalizes an aToken transfer * @param asset the address of the reserve - * @param user the user related to the balance decrease + * @param from the user from which the aTokens are transferred + * @param to the user receiving the aTokens * @param amount the amount being transferred/redeemed - * @return true if the balance decrease can be allowed, false otherwise + * @param balanceFromBefore the balance of the from user before the transfer + * @param balanceToBefore the balance of the to user before the transfer */ - function balanceDecreaseAllowed( + function finalizeTransfer( address asset, - address user, - uint256 amount - ) external override view returns (bool) { + address from, + address to, + uint256 amount, + uint256 balanceFromBefore, + uint256 balanceToBefore + ) external override { _whenNotPaused(); - return - GenericLogic.balanceDecreaseAllowed( - asset, - user, - amount, - _reserves, - _usersConfig[user], - _reservesList, - _reservesCount, - _addressesProvider.getPriceOracle() - ); + + require(msg.sender == _reserves[asset].aTokenAddress, Errors.CALLER_MUST_BE_AN_ATOKEN); + + ValidationLogic.validateTransfer( + from, + _reserves, + _usersConfig[from], + _reservesList, + _reservesCount, + _addressesProvider.getPriceOracle() + ); + + uint256 reserveId = _reserves[asset].id; + + if (from != to) { + if (balanceFromBefore.sub(amount) == 0) { + UserConfiguration.Map storage fromConfig = _usersConfig[from]; + fromConfig.setUsingAsCollateral(reserveId, false); + } + + if (balanceToBefore == 0 && amount != 0) { + UserConfiguration.Map storage toConfig = _usersConfig[to]; + toConfig.setUsingAsCollateral(reserveId, true); + } + } } /** diff --git a/contracts/libraries/configuration/ReserveConfiguration.sol b/contracts/libraries/configuration/ReserveConfiguration.sol index 553cec66..ce4f722a 100644 --- a/contracts/libraries/configuration/ReserveConfiguration.sol +++ b/contracts/libraries/configuration/ReserveConfiguration.sol @@ -1,21 +1,23 @@ // SPDX-License-Identifier: agpl-3.0 pragma solidity ^0.6.8; +import {Errors} from '../helpers/Errors.sol'; + /** * @title ReserveConfiguration library * @author Aave * @notice Implements the bitmap logic to handle the reserve configuration */ library ReserveConfiguration { - uint256 constant LTV_MASK = 0xFFFFFFFFFFFFFFFF0000; - uint256 constant LIQUIDATION_THRESHOLD_MASK = 0xFFFFFFFFFFFF0000FFFF; - uint256 constant LIQUIDATION_BONUS_MASK = 0xFFFFFFFF0000FFFFFFFF; - uint256 constant DECIMALS_MASK = 0xFFFFFF00FFFFFFFFFFFF; - uint256 constant ACTIVE_MASK = 0xFFFFFEFFFFFFFFFFFFFF; - uint256 constant FROZEN_MASK = 0xFFFFFDFFFFFFFFFFFFFF; - uint256 constant BORROWING_MASK = 0xFFFFFBFFFFFFFFFFFFFF; - uint256 constant STABLE_BORROWING_MASK = 0xFFFFF7FFFFFFFFFFFFFF; - uint256 constant RESERVE_FACTOR_MASK = 0xFFFFFFFFFFFFFFFF; + uint256 constant LTV_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000; // prettier-ignore + uint256 constant LIQUIDATION_THRESHOLD_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFF; // prettier-ignore + uint256 constant LIQUIDATION_BONUS_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF; // prettier-ignore + uint256 constant DECIMALS_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFF; // prettier-ignore + uint256 constant ACTIVE_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFF; // prettier-ignore + uint256 constant FROZEN_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDFFFFFFFFFFFFFF; // prettier-ignore + uint256 constant BORROWING_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFFFFFFFFFFF; // prettier-ignore + uint256 constant STABLE_BORROWING_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFF; // prettier-ignore + uint256 constant RESERVE_FACTOR_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFF; // prettier-ignore /// @dev For the LTV, the start bit is 0 (up to 15), but we don't declare it as for 0 no bit movement is needed uint256 constant LIQUIDATION_THRESHOLD_START_BIT_POSITION = 16; @@ -27,6 +29,12 @@ library ReserveConfiguration { uint256 constant STABLE_BORROWING_ENABLED_START_BIT_POSITION = 59; uint256 constant RESERVE_FACTOR_START_BIT_POSITION = 64; + uint256 constant MAX_VALID_LTV = 65535; + uint256 constant MAX_VALID_LIQUIDATION_THRESHOLD = 65535; + uint256 constant MAX_VALID_LIQUIDATION_BONUS = 65535; + uint256 constant MAX_VALID_DECIMALS = 255; + uint256 constant MAX_VALID_RESERVE_FACTOR = 65535; + struct Map { //bit 0-15: LTV //bit 16-31: Liq. threshold @@ -47,6 +55,8 @@ library ReserveConfiguration { * @param ltv the new ltv **/ function setLtv(ReserveConfiguration.Map memory self, uint256 ltv) internal pure { + require(ltv <= MAX_VALID_LTV, Errors.INVALID_LTV); + self.data = (self.data & LTV_MASK) | ltv; } @@ -68,6 +78,8 @@ library ReserveConfiguration { internal pure { + require(threshold <= MAX_VALID_LIQUIDATION_THRESHOLD, Errors.INVALID_LIQ_THRESHOLD); + self.data = (self.data & LIQUIDATION_THRESHOLD_MASK) | (threshold << LIQUIDATION_THRESHOLD_START_BIT_POSITION); @@ -92,6 +104,8 @@ library ReserveConfiguration { * @param bonus the new liquidation bonus **/ function setLiquidationBonus(ReserveConfiguration.Map memory self, uint256 bonus) internal pure { + require(bonus <= MAX_VALID_LIQUIDATION_BONUS, Errors.INVALID_LIQ_BONUS); + self.data = (self.data & LIQUIDATION_BONUS_MASK) | (bonus << LIQUIDATION_BONUS_START_BIT_POSITION); @@ -116,6 +130,8 @@ library ReserveConfiguration { * @param decimals the decimals **/ function setDecimals(ReserveConfiguration.Map memory self, uint256 decimals) internal pure { + require(decimals <= MAX_VALID_DECIMALS, Errors.INVALID_DECIMALS); + self.data = (self.data & DECIMALS_MASK) | (decimals << RESERVE_DECIMALS_START_BIT_POSITION); } @@ -145,7 +161,7 @@ library ReserveConfiguration { * @return the active state **/ function getActive(ReserveConfiguration.Map storage self) internal view returns (bool) { - return ((self.data & ~ACTIVE_MASK) >> IS_ACTIVE_START_BIT_POSITION) != 0; + return (self.data & ~ACTIVE_MASK) != 0; } /** @@ -165,7 +181,7 @@ library ReserveConfiguration { * @return the frozen state **/ function getFrozen(ReserveConfiguration.Map storage self) internal view returns (bool) { - return ((self.data & ~FROZEN_MASK) >> IS_FROZEN_START_BIT_POSITION) != 0; + return (self.data & ~FROZEN_MASK) != 0; } /** @@ -185,7 +201,7 @@ library ReserveConfiguration { * @return the borrowing state **/ function getBorrowingEnabled(ReserveConfiguration.Map storage self) internal view returns (bool) { - return ((self.data & ~BORROWING_MASK) >> BORROWING_ENABLED_START_BIT_POSITION) != 0; + return (self.data & ~BORROWING_MASK) != 0; } /** @@ -212,8 +228,7 @@ library ReserveConfiguration { view returns (bool) { - return - ((self.data & ~STABLE_BORROWING_MASK) >> STABLE_BORROWING_ENABLED_START_BIT_POSITION) != 0; + return (self.data & ~STABLE_BORROWING_MASK) != 0; } /** @@ -225,6 +240,8 @@ library ReserveConfiguration { internal pure { + require(reserveFactor <= MAX_VALID_RESERVE_FACTOR, Errors.INVALID_RESERVE_FACTOR); + self.data = (self.data & RESERVE_FACTOR_MASK) | (reserveFactor << RESERVE_FACTOR_START_BIT_POSITION); @@ -257,10 +274,10 @@ library ReserveConfiguration { uint256 dataLocal = self.data; return ( - (dataLocal & ~ACTIVE_MASK) >> IS_ACTIVE_START_BIT_POSITION != 0, - (dataLocal & ~FROZEN_MASK) >> IS_FROZEN_START_BIT_POSITION != 0, - (dataLocal & ~BORROWING_MASK) >> BORROWING_ENABLED_START_BIT_POSITION != 0, - (dataLocal & ~STABLE_BORROWING_MASK) >> STABLE_BORROWING_ENABLED_START_BIT_POSITION != 0 + (dataLocal & ~ACTIVE_MASK) != 0, + (dataLocal & ~FROZEN_MASK) != 0, + (dataLocal & ~BORROWING_MASK) != 0, + (dataLocal & ~STABLE_BORROWING_MASK) != 0 ); } @@ -316,8 +333,7 @@ library ReserveConfiguration { ); } - - /** + /** * @dev gets the configuration flags of the reserve from a memory object * @param self the reserve configuration * @return the state flags representing active, freezed, borrowing enabled, stableRateBorrowing enabled @@ -333,10 +349,10 @@ library ReserveConfiguration { ) { return ( - (self.data & ~ACTIVE_MASK) >> IS_ACTIVE_START_BIT_POSITION != 0, - (self.data & ~FROZEN_MASK) >> IS_FROZEN_START_BIT_POSITION != 0, - (self.data & ~BORROWING_MASK) >> BORROWING_ENABLED_START_BIT_POSITION != 0, - (self.data & ~STABLE_BORROWING_MASK) >> STABLE_BORROWING_ENABLED_START_BIT_POSITION != 0 + (self.data & ~ACTIVE_MASK) != 0, + (self.data & ~FROZEN_MASK) != 0, + (self.data & ~BORROWING_MASK) != 0, + (self.data & ~STABLE_BORROWING_MASK) != 0 ); } } diff --git a/contracts/libraries/helpers/Errors.sol b/contracts/libraries/helpers/Errors.sol index 91dd385f..b515603f 100644 --- a/contracts/libraries/helpers/Errors.sol +++ b/contracts/libraries/helpers/Errors.sol @@ -46,6 +46,7 @@ library Errors { string public constant NO_MORE_RESERVES_ALLOWED = '59'; string public constant INVALID_FLASH_LOAN_EXECUTOR_RETURN = '60'; string public constant INCONSISTENT_FLASHLOAN_PARAMS = '69'; + string public constant CALLER_MUST_BE_AN_ATOKEN = '76'; // require error messages - aToken - DebtTokens string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool' @@ -89,6 +90,15 @@ library Errors { // pausable error message string public constant IS_PAUSED = '58'; // 'Pool is paused' + + // Reserve configuration + string public constant INVALID_LTV = '70'; + string public constant INVALID_LIQ_THRESHOLD = '71'; + string public constant INVALID_LIQ_BONUS = '72'; + string public constant INVALID_DECIMALS = '73'; + string public constant INVALID_RESERVE_FACTOR = '74'; + + enum CollateralManagerErrors { NO_ERROR, NO_COLLATERAL_AVAILABLE, diff --git a/contracts/libraries/logic/ValidationLogic.sol b/contracts/libraries/logic/ValidationLogic.sol index 947d84ea..78f74154 100644 --- a/contracts/libraries/logic/ValidationLogic.sol +++ b/contracts/libraries/logic/ValidationLogic.sol @@ -46,6 +46,11 @@ library ValidationLogic { * @param reserveAddress the address of the reserve * @param amount the amount to be withdrawn * @param userBalance the balance of the user + * @param reservesData the reserves state + * @param userConfig the user configuration + * @param reserves the addresses of the reserves + * @param reservesCount the number of reserves + * @param oracle the price oracle */ function validateWithdraw( address reserveAddress, @@ -389,4 +394,35 @@ library ValidationLogic { return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.NO_ERRORS); } + + /** + * @dev validates an aToken transfer. + * @param from the user from which the aTokens are being transferred + * @param reservesData the state of all the reserves + * @param userConfig the state of the user for the specific reserve + * @param reserves the addresses of all the active reserves + * @param oracle the price oracle + */ + function validateTransfer( + address from, + mapping(address => ReserveLogic.ReserveData) storage reservesData, + UserConfiguration.Map storage userConfig, + mapping(uint256 => address) storage reserves, + uint256 reservesCount, + address oracle + ) internal view { + (, , , , uint256 healthFactor) = GenericLogic.calculateUserAccountData( + from, + reservesData, + userConfig, + reserves, + reservesCount, + oracle + ); + + require( + healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + Errors.TRANSFER_NOT_ALLOWED + ); + } } diff --git a/contracts/tokenization/AToken.sol b/contracts/tokenization/AToken.sol index 5641ebf8..adc1771b 100644 --- a/contracts/tokenization/AToken.sol +++ b/contracts/tokenization/AToken.sol @@ -241,16 +241,6 @@ contract AToken is VersionedInitializable, IncentivizedERC20, IAToken { return super.totalSupply(); } - /** - * @dev Used to validate transfers before actually executing them. - * @param user address of the user to check - * @param amount the amount to check - * @return true if the user can transfer amount, false otherwise - **/ - function isTransferAllowed(address user, uint256 amount) public override view returns (bool) { - return POOL.balanceDecreaseAllowed(UNDERLYING_ASSET_ADDRESS, user, amount); - } - /** * @dev transfers the underlying asset to the target. Used by the lendingpool to transfer * assets in borrow(), redeem() and flashLoan() @@ -317,14 +307,24 @@ contract AToken is VersionedInitializable, IncentivizedERC20, IAToken { uint256 amount, bool validate ) internal { - if (validate) { - require(isTransferAllowed(from, amount), Errors.TRANSFER_NOT_ALLOWED); - } - uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS); + uint256 fromBalanceBefore = super.balanceOf(from).rayMul(index); + uint256 toBalanceBefore = super.balanceOf(to).rayMul(index); + super._transfer(from, to, amount.rayDiv(index)); + if (validate) { + POOL.finalizeTransfer( + UNDERLYING_ASSET_ADDRESS, + from, + to, + amount, + fromBalanceBefore, + toBalanceBefore + ); + } + emit BalanceTransfer(from, to, amount, index); } diff --git a/contracts/tokenization/interfaces/IAToken.sol b/contracts/tokenization/interfaces/IAToken.sol index 00f63b20..055e3f5d 100644 --- a/contracts/tokenization/interfaces/IAToken.sol +++ b/contracts/tokenization/interfaces/IAToken.sol @@ -54,8 +54,6 @@ interface IAToken is IERC20, IScaledBalanceToken { uint256 value ) external; - function isTransferAllowed(address user, uint256 amount) external view returns (bool); - /** * @dev transfer the amount of the underlying asset to the user * @param user address of the user diff --git a/helpers/types.ts b/helpers/types.ts index 32f302d1..60ddd9b6 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -122,6 +122,12 @@ export enum ProtocolErrors { IS_PAUSED = '58', // Pool is paused + INVALID_LTV = '70', + INVALID_LIQ_THRESHOLD = '71', + INVALID_LIQ_BONUS = '72', + INVALID_DECIMALS = '73', + INVALID_RESERVE_FACTOR = '74', + // old INVALID_FROM_BALANCE_AFTER_TRANSFER = 'Invalid from balance after transfer', diff --git a/test/atoken-transfer.spec.ts b/test/atoken-transfer.spec.ts index 10bf15d4..ca92e086 100644 --- a/test/atoken-transfer.spec.ts +++ b/test/atoken-transfer.spec.ts @@ -50,8 +50,8 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { ); }); - it('User 0 deposits 1 WETH and user 1 tries to borrow, but the aTokens received as a transfer are not available as collateral (revert expected)', async () => { - const {users, pool, weth} = testEnv; + it('User 0 deposits 1 WETH and user 1 tries to borrow the WETH with the received DAI as collateral', async () => { + const {users, pool, weth, helpersContract} = testEnv; const userAddress = await pool.signer.getAddress(); await weth.connect(users[0].signer).mint(await convertToCurrencyDecimals(weth.address, '1')); @@ -61,26 +61,6 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { await pool .connect(users[0].signer) .deposit(weth.address, ethers.utils.parseEther('1.0'), userAddress, '0'); - await expect( - pool - .connect(users[1].signer) - .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); - }); - - it('User 1 sets the DAI as collateral and borrows, tries to transfer everything back to user 0 (revert expected)', async () => { - const {users, pool, aDai, dai, weth} = testEnv; - await pool.connect(users[1].signer).setUserUseReserveAsCollateral(dai.address, true); - - const aDAItoTransfer = await convertToCurrencyDecimals(dai.address, '1000'); - await pool .connect(users[1].signer) .borrow( @@ -91,9 +71,32 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => { users[1].address ); + const userReserveData = await helpersContract.getUserReserveData(weth.address, users[1].address); + + expect(userReserveData.currentStableDebt.toString()).to.be.eq(ethers.utils.parseEther('0.1')); + }); + + it('User 1 tries to transfer all the DAI used as collateral back to user 0 (revert expected)', async () => { + const {users, pool, aDai, dai, weth} = testEnv; + + const aDAItoTransfer = await convertToCurrencyDecimals(dai.address, '1000'); + await expect( aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer), TRANSFER_NOT_ALLOWED ).to.be.revertedWith(TRANSFER_NOT_ALLOWED); }); + + it('User 1 tries to transfer a small amount of DAI used as collateral back to user 0', async () => { + + const {users, pool, aDai, dai, weth} = testEnv; + + const aDAItoTransfer = await convertToCurrencyDecimals(dai.address, '100'); + + await aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer); + + const user0Balance = await aDai.balanceOf(users[0].address); + + expect(user0Balance.toString()).to.be.eq(aDAItoTransfer.toString()); + }); }); diff --git a/test/configurator.spec.ts b/test/configurator.spec.ts index 891c88dc..788add5c 100644 --- a/test/configurator.spec.ts +++ b/test/configurator.spec.ts @@ -10,7 +10,63 @@ const APPROVAL_AMOUNT_LENDING_POOL = const {expect} = require('chai'); makeSuite('LendingPoolConfigurator', (testEnv: TestEnv) => { - const {CALLER_NOT_AAVE_ADMIN, RESERVE_LIQUIDITY_NOT_0} = ProtocolErrors; + const { + CALLER_NOT_AAVE_ADMIN, + RESERVE_LIQUIDITY_NOT_0, + INVALID_LTV, + INVALID_LIQ_THRESHOLD, + INVALID_LIQ_BONUS, + INVALID_DECIMALS, + INVALID_RESERVE_FACTOR, + } = ProtocolErrors; + + it('Reverts trying to set an invalid LTV', async () => { + const {configurator, weth} = testEnv; + + const invalidLtv = 65536; + + await expect(configurator.setLtv(weth.address, invalidLtv)).to.be.revertedWith(INVALID_LTV); + }); + + it('Reverts trying to set an invalid liquidation threshold', async () => { + const {configurator, weth} = testEnv; + + const invalidLiqThreshold = 65536; + + await expect( + configurator.setLiquidationThreshold(weth.address, invalidLiqThreshold) + ).to.be.revertedWith(INVALID_LIQ_THRESHOLD); + }); + + it('Reverts trying to set an invalid liquidation bonus', async () => { + const {configurator, weth} = testEnv; + + const invalidLiqBonus = 65536; + + await expect( + configurator.setLiquidationBonus(weth.address, invalidLiqBonus) + ).to.be.revertedWith(INVALID_LIQ_BONUS); + }); + + it('Reverts trying to set an invalid reserve decimals', async () => { + const {configurator, weth} = testEnv; + + const invalidDecimals = 256; + + await expect(configurator.setReserveDecimals(weth.address, invalidDecimals)).to.be.revertedWith( + INVALID_DECIMALS + ); + }); + + it('Reverts trying to set an invalid reserve factor', async () => { + const {configurator, weth} = testEnv; + + const invalidReserveFactor = 65536; + + await expect( + configurator.setReserveFactor(weth.address, invalidReserveFactor) + ).to.be.revertedWith(INVALID_RESERVE_FACTOR); + }); it('Deactivates the ETH reserve', async () => { const {configurator, weth, helpersContract} = testEnv;