fix: updated implementation of the atokens/debt tokens to store additional data (user index/borrow rate) per user

This commit is contained in:
The3D 2021-07-27 10:05:55 +02:00
parent d62ef92d30
commit b6ae954093
9 changed files with 199 additions and 60 deletions

View File

@ -23,4 +23,11 @@ interface IScaledBalanceToken {
* @return The scaled total supply
**/
function scaledTotalSupply() external view returns (uint256);
/**
* @dev Returns the index at the moment of the last action (mint/burn/transfer)
* @param user The address of the user
* @return The last user index
**/
function lastUserIndex(address user) external view returns (uint256);
}

View File

@ -781,8 +781,6 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage
vars.releaseUnderlying ? vars.amount : 0
);
_usersLastBorrowTimestamp[vars.asset][vars.user] = block.timestamp;
if (vars.releaseUnderlying) {
IAToken(reserveCache.aTokenAddress).transferUnderlyingTo(vars.user, vars.amount);
}
@ -906,8 +904,7 @@ contract LendingPool is VersionedInitializable, ILendingPool, LendingPoolStorage
interestRateMode,
onBehalfOf,
stableDebt,
variableDebt,
_usersLastBorrowTimestamp
variableDebt
);
uint256 paybackAmount =

View File

@ -35,7 +35,4 @@ contract LendingPoolStorage {
mapping(address => bool) _authorizedFlashBorrowers;
uint256 internal _flashLoanPremiumToProtocol;
mapping(address => mapping(address => uint256)) _usersLastBorrowTimestamp;
}

View File

@ -255,7 +255,6 @@ library ValidationLogic {
* @param onBehalfOf The address of the user msg.sender is repaying for
* @param stableDebt The borrow balance of the user
* @param variableDebt The borrow balance of the user
* @param lastUsersBorrowTimestamp The data structure that keeps track of all the latest borrowings from the users
*/
function validateRepay(
DataTypes.ReserveCache memory reserveCache,
@ -264,8 +263,7 @@ library ValidationLogic {
DataTypes.InterestRateMode rateMode,
address onBehalfOf,
uint256 stableDebt,
uint256 variableDebt,
mapping(address => mapping(address => uint256)) storage lastUsersBorrowTimestamp
uint256 variableDebt
) external view {
(bool isActive, , , , bool isPaused) = reserveCache.reserveConfiguration.getFlagsMemory();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
@ -273,18 +271,22 @@ library ValidationLogic {
require(amountSent > 0, Errors.VL_INVALID_AMOUNT);
require(
lastUsersBorrowTimestamp[asset][onBehalfOf] != uint40(block.timestamp),
Errors.VL_SAME_BLOCK_BORROW_REPAY
);
require(
(stableDebt > 0 &&
DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) ||
(variableDebt > 0 &&
DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE),
Errors.VL_NO_DEBT_OF_SELECTED_TYPE
);
if (DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) {
require(
reserveCache.stableDebtLastUpdateTimestamp != block.timestamp,
Errors.VL_SAME_BLOCK_BORROW_REPAY
);
require(stableDebt > 0, Errors.VL_NO_DEBT_OF_SELECTED_TYPE);
} else if (DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE) {
require(
IVariableDebtToken(reserveCache.variableDebtTokenAddress).lastUserIndex(onBehalfOf) !=
reserveCache.currVariableBorrowIndex,
Errors.VL_SAME_BLOCK_BORROW_REPAY
);
require(variableDebt > 0, Errors.VL_NO_DEBT_OF_SELECTED_TYPE);
} else {
revert(Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED);
}
require(
amountSent != uint256(-1) || msg.sender == onBehalfOf,

View File

@ -125,7 +125,7 @@ contract AToken is
) external override onlyLendingPool {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
_burn(user, amountScaled);
_burn(user, amountScaled, index);
IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
@ -150,7 +150,7 @@ contract AToken is
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
_mint(user, amountScaled);
_mint(user, amountScaled, index);
emit Transfer(address(0), user, amount);
emit Mint(user, amount, index);
@ -175,7 +175,7 @@ contract AToken is
// The amount to mint can easily be very small since it is a fraction of the interest ccrued.
// In that case, the treasury will experience a (very small) loss, but it
// wont cause potentially valid transactions to fail.
_mint(treasury, amount.rayDiv(index));
_mint(treasury, amount.rayDiv(index), uint128(index));
emit Transfer(address(0), treasury, amount);
emit Mint(treasury, amount, index);
@ -263,6 +263,15 @@ contract AToken is
return super.totalSupply();
}
/**
* @dev Returns the index at the moment of the last action (mint/burn/transfer)
* @param user The address of the user
* @return The last user index
**/
function lastUserIndex(address user) external view virtual override returns (uint256) {
return _usersData[user].data;
}
/**
* @dev Returns the address of the Aave treasury, receiving the fees on this aToken
**/
@ -382,6 +391,9 @@ contract AToken is
uint256 toBalanceBefore = super.balanceOf(to).rayMul(index);
super._transfer(from, to, amount.rayDiv(index));
_usersData[from].data = uint128(index);
_usersData[to].data = uint128(index);
if (validate) {
pool.finalizeTransfer(underlyingAsset, from, to, amount, fromBalanceBefore, toBalanceBefore);

View File

@ -15,7 +15,12 @@ import {IAaveIncentivesController} from '../../interfaces/IAaveIncentivesControl
abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
using SafeMath for uint256;
mapping(address => uint256) internal _balances;
struct BalanceInfo {
uint128 balance;
uint128 data;
}
mapping(address => BalanceInfo) internal _usersData;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 internal _totalSupply;
@ -65,7 +70,7 @@ abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
* @return The balance of the token
**/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
return _usersData[account].balance;
}
/**
@ -177,11 +182,12 @@ abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
_beforeTokenTransfer(sender, recipient, amount);
uint256 oldSenderBalance = _balances[sender];
_balances[sender] = oldSenderBalance.sub(amount, 'ERC20: transfer amount exceeds balance');
uint256 oldRecipientBalance = _balances[recipient];
_balances[recipient] = _balances[recipient].add(amount);
uint256 oldSenderBalance = _usersData[sender].balance;
_usersData[sender].balance = uint128(oldSenderBalance.sub(amount, 'ERC20: transfer amount exceeds balance'));
uint256 oldRecipientBalance = _usersData[recipient].balance;
require((_usersData[recipient].balance = uint128(oldRecipientBalance.add(amount))) >= oldRecipientBalance, 'ERC20: Balance overflow');
if (address(_getIncentivesController()) != address(0)) {
uint256 currentTotalSupply = _totalSupply;
_getIncentivesController().handleAction(sender, currentTotalSupply, oldSenderBalance);
@ -191,7 +197,7 @@ abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
}
}
function _mint(address account, uint256 amount) internal virtual {
function _mint(address account, uint256 amount, uint256 data) internal virtual {
require(account != address(0), 'ERC20: mint to the zero address');
_beforeTokenTransfer(address(0), account, amount);
@ -199,15 +205,16 @@ abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
uint256 oldTotalSupply = _totalSupply;
_totalSupply = oldTotalSupply.add(amount);
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.add(amount);
uint256 oldAccountBalance = _usersData[account].balance;
require((_usersData[account].balance = uint128(oldAccountBalance.add(amount))) >= oldAccountBalance, 'ERC20: Balance overflow');
require((_usersData[account].data = uint128(data)) == data, 'ERC20: Data field overflow');
if (address(_getIncentivesController()) != address(0)) {
_getIncentivesController().handleAction(account, oldTotalSupply, oldAccountBalance);
}
}
function _burn(address account, uint256 amount) internal virtual {
function _burn(address account, uint256 amount, uint256 data) internal virtual {
require(account != address(0), 'ERC20: burn from the zero address');
_beforeTokenTransfer(account, address(0), amount);
@ -215,8 +222,9 @@ abstract contract IncentivizedERC20 is Context, IERC20, IERC20Detailed {
uint256 oldTotalSupply = _totalSupply;
_totalSupply = oldTotalSupply.sub(amount);
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.sub(amount, 'ERC20: burn amount exceeds balance');
uint256 oldAccountBalance = _usersData[account].balance;
_usersData[account].balance = uint128(oldAccountBalance.sub(amount, 'ERC20: burn amount exceeds balance'));
require((_usersData[account].data = uint128(data)) == data, 'ERC20: Data field overflow');
if (address(_getIncentivesController()) != address(0)) {
_getIncentivesController().handleAction(account, oldTotalSupply, oldAccountBalance);

View File

@ -22,7 +22,6 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
uint256 internal _avgStableRate;
mapping(address => uint40) internal _timestamps;
mapping(address => uint256) internal _usersStableRate;
uint40 internal _totalSupplyTimestamp;
ILendingPool internal _pool;
@ -96,7 +95,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
* @return The stable rate of user
**/
function getUserStableRate(address user) external view virtual override returns (uint256) {
return _usersStableRate[user];
return _usersData[user].data;
}
/**
@ -105,7 +104,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
**/
function balanceOf(address account) public view virtual override returns (uint256) {
uint256 accountBalance = super.balanceOf(account);
uint256 stableRate = _usersStableRate[account];
uint256 stableRate = _usersData[account].data;
if (accountBalance == 0) {
return 0;
}
@ -120,6 +119,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
uint256 amountInRay;
uint256 newStableRate;
uint256 currentAvgStableRate;
uint256 currentStableRate;
}
/**
@ -150,16 +150,18 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
vars.previousSupply = totalSupply();
vars.currentAvgStableRate = _avgStableRate;
vars.nextSupply = _totalSupply = vars.previousSupply.add(amount);
vars.amountInRay = amount.wadToRay();
vars.currentStableRate = _usersData[onBehalfOf].data;
vars.newStableRate = _usersStableRate[onBehalfOf]
if(vars.currentStableRate == 0){
vars.newStableRate = rate;
}else {
vars.newStableRate = vars.currentStableRate
.rayMul(currentBalance.wadToRay())
.add(vars.amountInRay.rayMul(rate))
.rayDiv(currentBalance.add(amount).wadToRay());
require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW);
_usersStableRate[onBehalfOf] = vars.newStableRate;
}
//solium-disable-next-line
_totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp);
@ -171,7 +173,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
.add(rate.rayMul(vars.amountInRay))
.rayDiv(vars.nextSupply.wadToRay());
_mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply);
_mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply, vars.newStableRate);
emit Transfer(address(0), onBehalfOf, amount);
@ -200,7 +202,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
uint256 previousSupply = totalSupply();
uint256 newAvgStableRate = 0;
uint256 nextSupply = 0;
uint256 userStableRate = _usersStableRate[user];
uint256 userStableRate = _usersData[user].data;
// Since the total supply and each single user debt accrue separately,
// there might be accumulation errors so that the last borrower repaying
@ -225,7 +227,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
}
if (amount == currentBalance) {
_usersStableRate[user] = 0;
_usersData[user].data = 0;
_timestamps[user] = 0;
} else {
//solium-disable-next-line
@ -236,7 +238,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
if (balanceIncrease > amount) {
uint256 amountToMint = balanceIncrease.sub(amount);
_mint(user, amountToMint, previousSupply);
_mint(user, amountToMint, previousSupply, userStableRate);
emit Mint(
user,
user,
@ -249,7 +251,7 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
);
} else {
uint256 amountToBurn = amount.sub(balanceIncrease);
_burn(user, amountToBurn, previousSupply);
_burn(user, amountToBurn, previousSupply, userStableRate);
emit Burn(user, amountToBurn, currentBalance, balanceIncrease, newAvgStableRate, nextSupply);
}
@ -404,11 +406,15 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
function _mint(
address account,
uint256 amount,
uint256 oldTotalSupply
uint256 oldTotalSupply,
uint256 data
) internal {
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.add(amount);
uint256 oldAccountBalance = _usersData[account].balance;
require(
(_usersData[account].balance = uint128(oldAccountBalance.add(amount))) >= oldAccountBalance,
'ERC20: Balance overflow'
);
_usersData[account].data = uint128(data);
if (address(_incentivesController) != address(0)) {
_incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);
}
@ -423,11 +429,14 @@ contract StableDebtToken is IStableDebtToken, DebtTokenBase {
function _burn(
address account,
uint256 amount,
uint256 oldTotalSupply
uint256 oldTotalSupply,
uint256 data
) internal {
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.sub(amount, Errors.SDT_BURN_EXCEEDS_BALANCE);
uint256 oldAccountBalance = _usersData[account].balance;
_usersData[account].balance = uint128(
oldAccountBalance.sub(amount, Errors.SDT_BURN_EXCEEDS_BALANCE)
);
_usersData[account].data = uint128(data);
if (address(_incentivesController) != address(0)) {
_incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);
}

View File

@ -106,7 +106,7 @@ contract VariableDebtToken is DebtTokenBase, IVariableDebtToken {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
_mint(onBehalfOf, amountScaled);
_mint(onBehalfOf, amountScaled, index);
emit Transfer(address(0), onBehalfOf, amount);
emit Mint(user, onBehalfOf, amount, index);
@ -129,7 +129,7 @@ contract VariableDebtToken is DebtTokenBase, IVariableDebtToken {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
_burn(user, amountScaled);
_burn(user, amountScaled, index);
emit Transfer(user, address(0), amount);
emit Burn(user, amount, index);
@ -159,6 +159,15 @@ contract VariableDebtToken is DebtTokenBase, IVariableDebtToken {
return super.totalSupply();
}
/**
* @dev Returns the index at the moment of the last action (mint/burn/transfer)
* @param user The address of the user
* @return The last user index
**/
function lastUserIndex(address user) external view virtual override returns (uint256) {
return _usersData[user].data;
}
/**
* @dev Returns the principal balance of the user and principal total supply.
* @param user The address of the user

View File

@ -0,0 +1,98 @@
import { TestEnv, makeSuite } from './helpers/make-suite';
import {
APPROVAL_AMOUNT_LENDING_POOL,
MAX_UINT_AMOUNT,
RAY,
MAX_EXPOSURE_CAP,
MOCK_CHAINLINK_AGGREGATORS_PRICES,
oneEther,
} from '../../helpers/constants';
import { ProtocolErrors } from '../../helpers/types';
import { MintableERC20, WETH9, WETH9Mocked } from '../../types';
import { parseEther } from '@ethersproject/units';
import { BigNumber } from '@ethersproject/bignumber';
import { strategyDAI } from '../../markets/amm/reservesConfigs';
import { strategyUSDC } from '../../markets/amm/reservesConfigs';
import { ethers } from 'ethers';
import { convertToCurrencyDecimals } from '../../helpers/contracts-helpers';
const { expect } = require('chai');
makeSuite('LTV validation tests', (testEnv: TestEnv) => {
const {
VL_LTV_VALIDATION_FAILED,
RC_INVALID_EXPOSURE_CAP,
VL_COLLATERAL_CANNOT_COVER_NEW_BORROW,
} = ProtocolErrors;
const daiPrice = Number(MOCK_CHAINLINK_AGGREGATORS_PRICES.DAI);
const usdcPrice = Number(MOCK_CHAINLINK_AGGREGATORS_PRICES.USDC);
const daiLTV = Number(strategyDAI.baseLTVAsCollateral);
const usdcLTV = Number(strategyUSDC.baseLTVAsCollateral);
it('User 1 deposits 10 Dai, 10 USDC, user 2 deposits 1 WETH', async () => {
const {
pool,
dai,
usdc,
weth,
users: [user1, user2],
} = testEnv;
const daiAmount = await convertToCurrencyDecimals(dai.address, '10');
const usdcAmount = await convertToCurrencyDecimals(usdc.address, '10');
const wethAmount = await convertToCurrencyDecimals(weth.address, '1');
await dai.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT);
await usdc.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT);
await weth.connect(user2.signer).approve(pool.address, MAX_UINT_AMOUNT);
await dai.connect(user1.signer).mint(daiAmount);
await usdc.connect(user1.signer).mint(usdcAmount);
await weth.connect(user2.signer).mint(wethAmount);
await pool.connect(user1.signer).deposit(dai.address, daiAmount, user1.address, 0);
await pool.connect(user1.signer).deposit(usdc.address, usdcAmount, user1.address, 0);
await pool.connect(user2.signer).deposit(weth.address, wethAmount, user2.address, 0);
});
it('Sets the ltv of DAI to 0', async () => {
const {
configurator,
dai,
helpersContract,
users: [],
} = testEnv;
await configurator.configureReserveAsCollateral(dai.address, 0, 8000, 10500);
const ltv = (await helpersContract.getReserveConfigurationData(dai.address)).ltv;
expect(ltv).to.be.equal(0);
});
it('Borrows 0.01 weth', async () => {
const {
pool,
weth,
users: [user1],
} = testEnv;
const borrowedAmount = await convertToCurrencyDecimals(weth.address, "0.01");
pool.connect(user1.signer).borrow(weth.address, borrowedAmount, 1, 0, user1.address);
});
it('Tries to withdraw USDC (revert expected)', async () => {
const {
pool,
usdc,
users: [user1],
} = testEnv;
const withdrawnAmount = await convertToCurrencyDecimals(usdc.address, "1");
await expect(
pool.connect(user1.signer).withdraw(usdc.address, withdrawnAmount, user1.address)
).to.be.revertedWith(VL_LTV_VALIDATION_FAILED);
});
});