initial implementation of the credit delegation + basic tests

This commit is contained in:
andyk 2020-09-09 15:33:05 +03:00
parent 23f99d30f0
commit 3173bee782
12 changed files with 250 additions and 49 deletions

View File

@ -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;
/**

View File

@ -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

View File

@ -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';
}

View File

@ -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);

View File

@ -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"
}
},

View File

@ -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),

View File

@ -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 {

View File

@ -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;

View File

@ -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"
}
]
}
]
}

View File

@ -61,7 +61,8 @@ export const getReserveData = async (
export const getUserData = async (
pool: LendingPool,
reserve: string,
user: string
user: tEthereumAddress,
sender?: tEthereumAddress
): Promise<UserReserveData> => {
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),

View File

@ -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

View File

@ -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(