Merge branch 'fix/99' into 'master'

Resolve "Set the isUsedAsCollateral flag on aToken transfers"

Closes #99

See merge request aave-tech/protocol-v2!107
This commit is contained in:
Ernesto Boado 2020-10-29 14:48:48 +00:00
commit b7f47d160e
8 changed files with 122 additions and 62 deletions

View File

@ -349,11 +349,14 @@ interface ILendingPool {
function getReserveData(address asset) external view returns (ReserveLogic.ReserveData memory); function getReserveData(address asset) external view returns (ReserveLogic.ReserveData memory);
function balanceDecreaseAllowed( function finalizeTransfer(
address reserve, address asset,
address user, address from,
uint256 amount address to,
) external view returns (bool); uint256 amount,
uint256 balanceFromAfter,
uint256 balanceToBefore
) external;
function getReservesList() external view returns (address[] memory); function getReservesList() external view returns (address[] memory);

View File

@ -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 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 * @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 asset,
address user, address from,
uint256 amount address to,
) external override view returns (bool) { uint256 amount,
uint256 balanceFromBefore,
uint256 balanceToBefore
) external override {
_whenNotPaused(); _whenNotPaused();
return
GenericLogic.balanceDecreaseAllowed( require(msg.sender == _reserves[asset].aTokenAddress, Errors.CALLER_MUST_BE_AN_ATOKEN);
asset,
user, ValidationLogic.validateTransfer(
amount, from,
_reserves, _reserves,
_usersConfig[user], _usersConfig[from],
_reservesList, _reservesList,
_reservesCount, _reservesCount,
_addressesProvider.getPriceOracle() _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);
}
}
} }
/** /**

View File

@ -46,6 +46,7 @@ library Errors {
string public constant NO_MORE_RESERVES_ALLOWED = '59'; string public constant NO_MORE_RESERVES_ALLOWED = '59';
string public constant INVALID_FLASH_LOAN_EXECUTOR_RETURN = '60'; string public constant INVALID_FLASH_LOAN_EXECUTOR_RETURN = '60';
string public constant INCONSISTENT_FLASHLOAN_PARAMS = '69'; string public constant INCONSISTENT_FLASHLOAN_PARAMS = '69';
string public constant CALLER_MUST_BE_AN_ATOKEN = '76';
// require error messages - aToken - DebtTokens // require error messages - aToken - DebtTokens
string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool' string public constant CALLER_MUST_BE_LENDING_POOL = '28'; // 'The caller of this function must be a lending pool'

View File

@ -46,6 +46,11 @@ library ValidationLogic {
* @param reserveAddress the address of the reserve * @param reserveAddress the address of the reserve
* @param amount the amount to be withdrawn * @param amount the amount to be withdrawn
* @param userBalance the balance of the user * @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( function validateWithdraw(
address reserveAddress, address reserveAddress,
@ -389,4 +394,35 @@ library ValidationLogic {
return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.NO_ERRORS); 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
);
}
} }

View File

@ -241,16 +241,6 @@ contract AToken is VersionedInitializable, IncentivizedERC20, IAToken {
return super.totalSupply(); 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 * @dev transfers the underlying asset to the target. Used by the lendingpool to transfer
* assets in borrow(), redeem() and flashLoan() * assets in borrow(), redeem() and flashLoan()
@ -317,14 +307,24 @@ contract AToken is VersionedInitializable, IncentivizedERC20, IAToken {
uint256 amount, uint256 amount,
bool validate bool validate
) internal { ) internal {
if (validate) {
require(isTransferAllowed(from, amount), Errors.TRANSFER_NOT_ALLOWED);
}
uint256 index = POOL.getReserveNormalizedIncome(UNDERLYING_ASSET_ADDRESS); 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)); 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); emit BalanceTransfer(from, to, amount, index);
} }

View File

@ -54,8 +54,6 @@ interface IAToken is IERC20, IScaledBalanceToken {
uint256 value uint256 value
) external; ) external;
function isTransferAllowed(address user, uint256 amount) external view returns (bool);
/** /**
* @dev transfer the amount of the underlying asset to the user * @dev transfer the amount of the underlying asset to the user
* @param user address of the user * @param user address of the user

View File

@ -265,4 +265,4 @@
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6" "deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
} }
} }
} }

View File

@ -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 () => { 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} = testEnv; const {users, pool, weth, helpersContract} = testEnv;
const userAddress = await pool.signer.getAddress(); const userAddress = await pool.signer.getAddress();
await weth.connect(users[0].signer).mint(await convertToCurrencyDecimals(weth.address, '1')); await weth.connect(users[0].signer).mint(await convertToCurrencyDecimals(weth.address, '1'));
@ -61,26 +61,6 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => {
await pool await pool
.connect(users[0].signer) .connect(users[0].signer)
.deposit(weth.address, ethers.utils.parseEther('1.0'), userAddress, '0'); .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 await pool
.connect(users[1].signer) .connect(users[1].signer)
.borrow( .borrow(
@ -91,9 +71,32 @@ makeSuite('AToken: Transfer', (testEnv: TestEnv) => {
users[1].address 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( await expect(
aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer), aDai.connect(users[1].signer).transfer(users[0].address, aDAItoTransfer),
TRANSFER_NOT_ALLOWED TRANSFER_NOT_ALLOWED
).to.be.revertedWith(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());
});
}); });