mirror of
https://github.com/Instadapp/aave-protocol-v2.git
synced 2024-07-29 21:47:30 +00:00
removal of TokenDistributor, FeeProvider and FlashLoanVars
This commit is contained in:
parent
7d23041849
commit
d60f7b6272
|
@ -25,7 +25,6 @@ contract LendingPoolAddressesProvider is Ownable, ILendingPoolAddressesProvider
|
|||
event PriceOracleUpdated(address indexed newAddress);
|
||||
event LendingRateOracleUpdated(address indexed newAddress);
|
||||
event FeeProviderUpdated(address indexed newAddress);
|
||||
event TokenDistributorUpdated(address indexed newAddress);
|
||||
|
||||
event ProxyCreated(bytes32 id, address indexed newAddress);
|
||||
|
||||
|
@ -146,15 +145,6 @@ contract LendingPoolAddressesProvider is Ownable, ILendingPoolAddressesProvider
|
|||
emit LendingRateOracleUpdated(lendingRateOracle);
|
||||
}
|
||||
|
||||
function getTokenDistributor() external override view returns (address) {
|
||||
return _addresses[TOKEN_DISTRIBUTOR];
|
||||
}
|
||||
|
||||
function setTokenDistributor(address tokenDistributor) external override onlyOwner {
|
||||
_addresses[TOKEN_DISTRIBUTOR] = tokenDistributor;
|
||||
emit TokenDistributorUpdated(tokenDistributor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev internal function to update the implementation of a specific component of the protocol
|
||||
* @param id the id of the contract to be updated
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
// SPDX-License-Identifier: agpl-3.0
|
||||
pragma solidity ^0.6.8;
|
||||
|
||||
import '../libraries/openzeppelin-upgradeability/VersionedInitializable.sol';
|
||||
import '../interfaces/IFeeProvider.sol';
|
||||
import '../libraries/WadRayMath.sol';
|
||||
|
||||
/**
|
||||
* @title FeeProvider contract
|
||||
* @notice Implements calculation for the fees applied by the protocol
|
||||
* @author Aave
|
||||
**/
|
||||
contract FeeProvider is IFeeProvider, VersionedInitializable {
|
||||
using WadRayMath for uint256;
|
||||
|
||||
// percentage of the fee to be calculated on the loan amount
|
||||
uint256 public originationFeePercentage;
|
||||
|
||||
uint256 public constant FEE_PROVIDER_REVISION = 0x1;
|
||||
|
||||
function getRevision() internal override pure returns (uint256) {
|
||||
return FEE_PROVIDER_REVISION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev initializes the FeeProvider after it's added to the proxy
|
||||
* @param _addressesProvider the address of the LendingPoolAddressesProvider
|
||||
*/
|
||||
function initialize(address _addressesProvider) public initializer {
|
||||
/// @notice origination fee is set as default as 25 basis points of the loan amount (0.0025%)
|
||||
originationFeePercentage = 0.0025 * 1e18;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev calculates the origination fee for every loan executed on the platform.
|
||||
* @param _user can be used in the future to apply discount to the origination fee based on the
|
||||
* _user account (eg. stake AAVE tokens in the lending pool, or deposit > 1M USD etc.)
|
||||
* @param _amount the amount of the loan
|
||||
**/
|
||||
function calculateLoanOriginationFee(address _user, uint256 _amount)
|
||||
external
|
||||
override
|
||||
view
|
||||
returns (uint256)
|
||||
{
|
||||
return _amount.wadMul(originationFeePercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev returns the origination fee percentage
|
||||
**/
|
||||
function getLoanOriginationFeePercentage() external override view returns (uint256) {
|
||||
return originationFeePercentage;
|
||||
}
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
// SPDX-License-Identifier: agpl-3.0
|
||||
pragma solidity ^0.6.8;
|
||||
|
||||
import '@openzeppelin/contracts/math/SafeMath.sol';
|
||||
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import '@openzeppelin/contracts/token/ERC20/ERC20Burnable.sol';
|
||||
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';
|
||||
|
||||
import '../libraries/openzeppelin-upgradeability/VersionedInitializable.sol';
|
||||
import '../interfaces/IExchangeAdapter.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
|
||||
import {PercentageMath} from '../libraries/PercentageMath.sol';
|
||||
|
||||
/// @title TokenDistributor
|
||||
/// @author Aave
|
||||
/// @notice Receives tokens and manages the distribution amongst receivers
|
||||
/// The usage is as follows:
|
||||
/// - The distribution addresses and percentages are set up on construction
|
||||
/// - The Kyber Proxy is approved for a list of tokens in construction, which will be later burnt
|
||||
/// - At any moment, anyone can call distribute() with a list of token addresses in order to distribute
|
||||
/// the accumulated token amounts and/or ETH in this contract to all the receivers with percentages
|
||||
/// - If the address(0) is used as receiver, this contract will trade in Kyber to tokenToBurn (LEND)
|
||||
/// and burn it (sending to address(0) the tokenToBurn)
|
||||
contract TokenDistributor is ReentrancyGuard, VersionedInitializable {
|
||||
using SafeMath for uint256;
|
||||
using PercentageMath for uint256;
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
struct Distribution {
|
||||
address[] receivers;
|
||||
uint256[] percentages;
|
||||
}
|
||||
|
||||
event DistributionUpdated(address[] receivers, uint256[] percentages);
|
||||
event Distributed(address receiver, uint256 percentage, uint256 amount);
|
||||
event Setup(address tokenToBurn, IExchangeAdapter exchangeAdapter, address _recipientBurn);
|
||||
event Burn(uint256 amount);
|
||||
|
||||
uint256 public constant IMPLEMENTATION_REVISION = 0x3;
|
||||
|
||||
/// @notice DEPRECATED
|
||||
uint256 public constant MAX_UINT = 2**256 - 1;
|
||||
|
||||
/// @notice DEPRECATED
|
||||
uint256 public constant MAX_UINT_MINUS_ONE = (2**256 - 1) - 1;
|
||||
|
||||
/// @notice DEPRECATED
|
||||
uint256 public constant MIN_CONVERSION_RATE = 1;
|
||||
|
||||
/// @notice DEPRECATED
|
||||
address public constant KYBER_ETH_MOCK_ADDRESS = address(
|
||||
0x00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
|
||||
);
|
||||
|
||||
/// @notice Defines how tokens and ETH are distributed on each call to .distribute()
|
||||
Distribution private distribution;
|
||||
|
||||
/// @notice Instead of using 100 for percentages, higher base to have more precision in the distribution
|
||||
uint256 public constant DISTRIBUTION_BASE = 10000;
|
||||
|
||||
/// @notice The address of the token to burn (LEND token)
|
||||
address public tokenToBurn;
|
||||
|
||||
/// @notice Address to send tokens to "burn".
|
||||
/// Because of limitations on OZ ERC20, on dev it's needed to use the 0x00000...1 address instead of address(0)
|
||||
/// So this param needs to be received on construction
|
||||
address public recipientBurn;
|
||||
|
||||
/// @notice Smart contract implementing the logic to interact with a particular exchange.
|
||||
/// Will be called by DELEGATECALL
|
||||
IExchangeAdapter public exchangeAdapter;
|
||||
|
||||
/// @notice Called by the proxy when setting this contract as implementation
|
||||
function initialize(
|
||||
address _recipientBurn,
|
||||
address _tokenToBurn,
|
||||
IExchangeAdapter _exchangeAdapter,
|
||||
address[] memory _receivers,
|
||||
uint256[] memory _percentages,
|
||||
IERC20[] memory _tokens
|
||||
) public initializer {
|
||||
recipientBurn = _recipientBurn;
|
||||
tokenToBurn = _tokenToBurn;
|
||||
exchangeAdapter = _exchangeAdapter;
|
||||
internalSetTokenDistribution(_receivers, _percentages);
|
||||
approveExchange(_tokens);
|
||||
emit Setup(_tokenToBurn, _exchangeAdapter, _recipientBurn);
|
||||
}
|
||||
|
||||
/// @notice In order to receive ETH transfers
|
||||
receive() external payable {}
|
||||
|
||||
/// @notice "Infinite" approval for all the tokens initialized
|
||||
/// @param _tokens List of IERC20 to approve
|
||||
function approveExchange(IERC20[] memory _tokens) public {
|
||||
(bool _success, ) = address(exchangeAdapter).delegatecall(
|
||||
abi.encodeWithSelector(exchangeAdapter.approveExchange.selector, _tokens)
|
||||
);
|
||||
}
|
||||
|
||||
/// @notice Distributes the whole balance of a list of _tokens balances in this contract
|
||||
/// @param _tokens list of ERC20 tokens to distribute
|
||||
function distribute(IERC20[] memory _tokens) public {
|
||||
for (uint256 i = 0; i < _tokens.length; i++) {
|
||||
uint256 _balanceToDistribute = _tokens[i].balanceOf(address(this));
|
||||
|
||||
if (_balanceToDistribute <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
internalDistributeTokenWithAmount(_tokens[i], _balanceToDistribute);
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Distributes specific amounts of a list of _tokens
|
||||
/// @param _tokens list of ERC20 tokens to distribute
|
||||
/// @param _amounts list of amounts to distribute per token
|
||||
function distributeWithAmounts(IERC20[] memory _tokens, uint256[] memory _amounts) public {
|
||||
for (uint256 i = 0; i < _tokens.length; i++) {
|
||||
internalDistributeTokenWithAmount(_tokens[i], _amounts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Distributes specific total balance's percentages of a list of _tokens
|
||||
/// @param _tokens list of ERC20 tokens to distribute
|
||||
/// @param _percentages list of percentages to distribute per token
|
||||
function distributeWithPercentages(IERC20[] memory _tokens, uint256[] memory _percentages)
|
||||
public
|
||||
{
|
||||
for (uint256 i = 0; i < _tokens.length; i++) {
|
||||
uint256 _amountToDistribute = _tokens[i].balanceOf(address(this)).percentMul(
|
||||
_percentages[i]
|
||||
);
|
||||
|
||||
if (_amountToDistribute <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
internalDistributeTokenWithAmount(_tokens[i], _amountToDistribute);
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Sets _receivers addresses with _percentages for each one
|
||||
/// @param _receivers Array of addresses receiving a percentage of the distribution, both user addresses
|
||||
/// or contracts
|
||||
/// @param _percentages Array of percentages each _receivers member will get
|
||||
function internalSetTokenDistribution(address[] memory _receivers, uint256[] memory _percentages)
|
||||
internal
|
||||
{
|
||||
require(_receivers.length == _percentages.length, 'Array lengths should be equal');
|
||||
|
||||
distribution = Distribution({receivers: _receivers, percentages: _percentages});
|
||||
emit DistributionUpdated(_receivers, _percentages);
|
||||
}
|
||||
|
||||
/// @notice Distributes a specific amount of a token owned by this contract
|
||||
/// @param _token The ERC20 token to distribute
|
||||
/// @param _amountToDistribute The specific amount to distribute
|
||||
function internalDistributeTokenWithAmount(IERC20 _token, uint256 _amountToDistribute) internal {
|
||||
address _tokenAddress = address(_token);
|
||||
Distribution memory _distribution = distribution;
|
||||
for (uint256 j = 0; j < _distribution.receivers.length; j++) {
|
||||
uint256 _amount = _amountToDistribute.mul(_distribution.percentages[j]).div(
|
||||
DISTRIBUTION_BASE
|
||||
);
|
||||
|
||||
//avoid transfers/burns of 0 tokens
|
||||
if (_amount == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_distribution.receivers[j] != address(0)) {
|
||||
_token.safeTransfer(_distribution.receivers[j], _amount);
|
||||
emit Distributed(_distribution.receivers[j], _distribution.percentages[j], _amount);
|
||||
} else {
|
||||
uint256 _amountToBurn = _amount;
|
||||
// If the token to burn is already tokenToBurn, we don't trade, burning directly
|
||||
if (_tokenAddress != tokenToBurn) {
|
||||
(bool _success, bytes memory _result) = address(exchangeAdapter).delegatecall(
|
||||
abi.encodeWithSelector(
|
||||
exchangeAdapter.exchange.selector,
|
||||
_tokenAddress,
|
||||
tokenToBurn,
|
||||
_amount,
|
||||
10
|
||||
)
|
||||
);
|
||||
require(_success, 'ERROR_ON_EXCHANGE');
|
||||
_amountToBurn = abi.decode(_result, (uint256));
|
||||
}
|
||||
_burn(_amountToBurn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @notice Internal function to send _amount of tokenToBurn to the 0x0 address
|
||||
/// @param _amount The amount to burn
|
||||
function _burn(uint256 _amount) internal {
|
||||
require(
|
||||
IERC20(tokenToBurn).transfer(recipientBurn, _amount),
|
||||
'INTERNAL_BURN. Reverted transfer to recipientBurn address'
|
||||
);
|
||||
emit Burn(_amount);
|
||||
}
|
||||
|
||||
/// @notice Returns the receivers and percentages of the contract Distribution
|
||||
/// @return receivers array of addresses and percentages array on uints
|
||||
function getDistribution()
|
||||
public
|
||||
view
|
||||
returns (address[] memory receivers, uint256[] memory percentages)
|
||||
{
|
||||
receivers = distribution.receivers;
|
||||
percentages = distribution.percentages;
|
||||
}
|
||||
|
||||
/// @notice Gets the revision number of the contract
|
||||
/// @return The revision numeric reference
|
||||
function getRevision() internal override pure returns (uint256) {
|
||||
return IMPLEMENTATION_REVISION;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
// SPDX-License-Identifier: agpl-3.0
|
||||
pragma solidity ^0.6.8;
|
||||
|
||||
/**
|
||||
* @title IFeeProvider interface
|
||||
* @notice Interface for the Aave fee provider.
|
||||
**/
|
||||
|
||||
interface IFeeProvider {
|
||||
function calculateLoanOriginationFee(address _user, uint256 _amount)
|
||||
external
|
||||
view
|
||||
returns (uint256);
|
||||
|
||||
function getLoanOriginationFeePercentage() external view returns (uint256);
|
||||
}
|
|
@ -15,10 +15,6 @@ interface ILendingPoolAddressesProvider {
|
|||
|
||||
function setLendingPoolConfiguratorImpl(address configurator) external;
|
||||
|
||||
function getTokenDistributor() external view returns (address);
|
||||
|
||||
function setTokenDistributor(address tokenDistributor) external;
|
||||
|
||||
function getFeeProvider() external view returns (address);
|
||||
|
||||
function setFeeProviderImpl(address feeProvider) external;
|
||||
|
|
|
@ -20,7 +20,6 @@ import '../libraries/UserConfiguration.sol';
|
|||
import '../tokenization/interfaces/IStableDebtToken.sol';
|
||||
import '../tokenization/interfaces/IVariableDebtToken.sol';
|
||||
|
||||
import '../interfaces/IFeeProvider.sol';
|
||||
import '../flashloan/interfaces/IFlashLoanReceiver.sol';
|
||||
import './LendingPoolLiquidationManager.sol';
|
||||
import '../interfaces/IPriceOracleGetter.sol';
|
||||
|
@ -45,10 +44,8 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
uint256 private constant REBALANCE_DOWN_RATE_DELTA = (1e27) / 5;
|
||||
uint256 private constant MAX_STABLE_RATE_BORROW_SIZE_PERCENT = 25;
|
||||
uint256 private constant FLASHLOAN_FEE_TOTAL = 9;
|
||||
uint256 private constant FLASHLOAN_FEE_PROTOCOL = 3000;
|
||||
|
||||
LendingPoolAddressesProvider public addressesProvider;
|
||||
IFeeProvider feeProvider;
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
mapping(address => ReserveLogic.ReserveData) internal reserves;
|
||||
|
@ -162,7 +159,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
* @param _reserve the address of the reserve
|
||||
* @param _amount the amount requested
|
||||
* @param _totalFee the total fee on the amount
|
||||
* @param _protocolFee the part of the fee for the protocol
|
||||
* @param _timestamp the timestamp of the action
|
||||
**/
|
||||
event FlashLoan(
|
||||
|
@ -170,7 +166,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
address indexed _reserve,
|
||||
uint256 _amount,
|
||||
uint256 _totalFee,
|
||||
uint256 _protocolFee,
|
||||
uint256 _timestamp
|
||||
);
|
||||
|
||||
|
@ -246,7 +241,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
**/
|
||||
function initialize(LendingPoolAddressesProvider _addressesProvider) public initializer {
|
||||
addressesProvider = _addressesProvider;
|
||||
feeProvider = IFeeProvider(addressesProvider.getFeeProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -265,7 +259,7 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
|
||||
ValidationLogic.validateDeposit(reserve, _amount);
|
||||
|
||||
AToken aToken = AToken(payable(reserve.aTokenAddress));
|
||||
AToken aToken = AToken(reserve.aTokenAddress);
|
||||
|
||||
bool isFirstDeposit = aToken.balanceOf(msg.sender) == 0;
|
||||
|
||||
|
@ -646,15 +640,6 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
}
|
||||
}
|
||||
|
||||
struct FlashLoanLocalVars {
|
||||
uint256 availableLiquidityBefore;
|
||||
uint256 totalFeeBips;
|
||||
uint256 protocolFeeBips;
|
||||
uint256 amountFee;
|
||||
uint256 protocolFee;
|
||||
address payable aTokenAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev allows smartcontracts to access the liquidity of the pool within one transaction,
|
||||
* as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts
|
||||
|
@ -669,29 +654,21 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
uint256 _amount,
|
||||
bytes memory _params
|
||||
) public nonReentrant {
|
||||
FlashLoanLocalVars memory vars;
|
||||
|
||||
ReserveLogic.ReserveData storage reserve = reserves[_reserve];
|
||||
|
||||
vars.aTokenAddress = payable(reserve.aTokenAddress);
|
||||
address payable aTokenAddress = payable(reserve.aTokenAddress);
|
||||
|
||||
//check that the reserve has enough available liquidity
|
||||
vars.availableLiquidityBefore = IERC20(_reserve).balanceOf(vars.aTokenAddress);
|
||||
uint256 availableLiquidityBefore = IERC20(_reserve).balanceOf(aTokenAddress);
|
||||
|
||||
//calculate amount fee
|
||||
vars.amountFee = _amount.mul(FLASHLOAN_FEE_TOTAL).div(10000);
|
||||
|
||||
//protocol fee is the part of the amountFee reserved for the protocol - the rest goes to depositors
|
||||
vars.protocolFee = vars.amountFee.mul(FLASHLOAN_FEE_PROTOCOL).div(10000);
|
||||
uint256 amountFee = _amount.mul(FLASHLOAN_FEE_TOTAL).div(10000);
|
||||
|
||||
require(
|
||||
vars.availableLiquidityBefore >= _amount,
|
||||
availableLiquidityBefore >= _amount,
|
||||
'There is not enough liquidity available to borrow'
|
||||
);
|
||||
require(
|
||||
vars.amountFee > 0 && vars.protocolFee > 0,
|
||||
'The requested amount is too small for a FlashLoan.'
|
||||
);
|
||||
require(amountFee > 0, 'The requested amount is too small for a FlashLoan.');
|
||||
|
||||
//get the FlashLoanReceiver instance
|
||||
IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver);
|
||||
|
@ -699,34 +676,23 @@ contract LendingPool is ReentrancyGuard, VersionedInitializable {
|
|||
address payable userPayable = address(uint160(_receiver));
|
||||
|
||||
//transfer funds to the receiver
|
||||
AToken(vars.aTokenAddress).transferUnderlyingTo(userPayable, _amount);
|
||||
AToken(aTokenAddress).transferUnderlyingTo(userPayable, _amount);
|
||||
|
||||
//execute action of the receiver
|
||||
receiver.executeOperation(_reserve, vars.aTokenAddress, _amount, vars.amountFee, _params);
|
||||
receiver.executeOperation(_reserve, aTokenAddress, _amount, amountFee, _params);
|
||||
|
||||
//check that the actual balance of the core contract includes the returned amount
|
||||
uint256 availableLiquidityAfter = IERC20(_reserve).balanceOf(vars.aTokenAddress);
|
||||
uint256 availableLiquidityAfter = IERC20(_reserve).balanceOf(aTokenAddress);
|
||||
|
||||
require(
|
||||
availableLiquidityAfter == vars.availableLiquidityBefore.add(vars.amountFee),
|
||||
availableLiquidityAfter == availableLiquidityBefore.add(amountFee),
|
||||
'The actual balance of the protocol is inconsistent'
|
||||
);
|
||||
|
||||
reserve.updateStateOnFlashLoan(
|
||||
_reserve,
|
||||
vars.availableLiquidityBefore,
|
||||
vars.amountFee.sub(vars.protocolFee),
|
||||
vars.protocolFee
|
||||
);
|
||||
|
||||
//transfer funds to the receiver
|
||||
AToken(vars.aTokenAddress).transferUnderlyingTo(
|
||||
addressesProvider.getTokenDistributor(),
|
||||
vars.protocolFee
|
||||
);
|
||||
reserve.updateStateOnFlashLoan(_reserve, availableLiquidityBefore, amountFee);
|
||||
|
||||
//solium-disable-next-line
|
||||
emit FlashLoan(_receiver, _reserve, _amount, vars.amountFee, vars.protocolFee, block.timestamp);
|
||||
emit FlashLoan(_receiver, _reserve, _amount, amountFee, block.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,7 +39,6 @@ contract LendingPoolLiquidationManager is ReentrancyGuard, VersionedInitializabl
|
|||
using UserConfiguration for UserConfiguration.Map;
|
||||
|
||||
LendingPoolAddressesProvider public addressesProvider;
|
||||
IFeeProvider feeProvider;
|
||||
|
||||
mapping(address => ReserveLogic.ReserveData) internal reserves;
|
||||
mapping(address => UserConfiguration.Map) internal usersConfig;
|
||||
|
|
|
@ -11,7 +11,6 @@ import {UserConfiguration} from './UserConfiguration.sol';
|
|||
import {WadRayMath} from './WadRayMath.sol';
|
||||
import {PercentageMath} from './PercentageMath.sol';
|
||||
import '../interfaces/IPriceOracleGetter.sol';
|
||||
import {IFeeProvider} from '../interfaces/IFeeProvider.sol';
|
||||
import '@nomiclabs/buidler/console.sol';
|
||||
|
||||
/**
|
||||
|
@ -60,8 +59,10 @@ library GenericLogic {
|
|||
address[] calldata _reserves,
|
||||
address _oracle
|
||||
) external view returns (bool) {
|
||||
|
||||
if (!_userConfig.isBorrowingAny() || !_userConfig.isUsingAsCollateral(_reservesData[_reserve].index)) {
|
||||
if (
|
||||
!_userConfig.isBorrowingAny() ||
|
||||
!_userConfig.isUsingAsCollateral(_reservesData[_reserve].index)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import {ReserveLogic} from './ReserveLogic.sol';
|
|||
import {WadRayMath} from './WadRayMath.sol';
|
||||
|
||||
import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol';
|
||||
import {IFeeProvider} from '../interfaces/IFeeProvider.sol';
|
||||
|
||||
/**
|
||||
* @title ReserveConfiguration library
|
||||
|
|
|
@ -5,7 +5,7 @@ import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
|
|||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {MathUtils} from './MathUtils.sol';
|
||||
import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
|
||||
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
|
||||
import {IStableDebtToken} from '../tokenization/interfaces/IStableDebtToken.sol';
|
||||
import {ReserveConfiguration} from './ReserveConfiguration.sol';
|
||||
|
||||
|
@ -227,8 +227,7 @@ library ReserveLogic {
|
|||
ReserveData storage _reserve,
|
||||
address _reserveAddress,
|
||||
uint256 _availableLiquidityBefore,
|
||||
uint256 _income,
|
||||
uint256 _protocolFee
|
||||
uint256 _income
|
||||
) external {
|
||||
//compounding the cumulated interest
|
||||
_reserve.updateCumulativeIndexesAndTimestamp();
|
||||
|
@ -252,8 +251,7 @@ library ReserveLogic {
|
|||
view
|
||||
returns (uint256)
|
||||
{
|
||||
return
|
||||
IERC20(_reserveAddress).balanceOf(address(this)).add(_reserve.getTotalBorrows());
|
||||
return IERC20(_reserveAddress).balanceOf(address(this)).add(_reserve.getTotalBorrows());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,7 +5,6 @@ import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
|
|||
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
|
||||
import {WadRayMath} from './WadRayMath.sol';
|
||||
import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol';
|
||||
import {IFeeProvider} from '../interfaces/IFeeProvider.sol';
|
||||
|
||||
/**
|
||||
* @title UserConfiguration library
|
||||
|
@ -20,39 +19,43 @@ library UserConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* @dev sets if the user is borrowing the reserve identified by _reserveIndex
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @param _borrowing true if the user is borrowing the reserve, false otherwise
|
||||
**/
|
||||
* @dev sets if the user is borrowing the reserve identified by _reserveIndex
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @param _borrowing true if the user is borrowing the reserve, false otherwise
|
||||
**/
|
||||
function setBorrowing(
|
||||
UserConfiguration.Map storage _self,
|
||||
uint256 _reserveIndex,
|
||||
bool _borrowing
|
||||
) internal {
|
||||
_self.data = (_self.data & ~(1 << _reserveIndex*2)) | uint256(_borrowing ? 1 : 0) << (_reserveIndex * 2);
|
||||
_self.data =
|
||||
(_self.data & ~(1 << (_reserveIndex * 2))) |
|
||||
(uint256(_borrowing ? 1 : 0) << (_reserveIndex * 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev sets if the user is using as collateral the reserve identified by _reserveIndex
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @param _usingAsCollateral true if the user is usin the reserve as collateral, false otherwise
|
||||
**/
|
||||
* @dev sets if the user is using as collateral the reserve identified by _reserveIndex
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @param _usingAsCollateral true if the user is usin the reserve as collateral, false otherwise
|
||||
**/
|
||||
function setUsingAsCollateral(
|
||||
UserConfiguration.Map storage _self,
|
||||
uint256 _reserveIndex,
|
||||
bool _usingAsCollateral
|
||||
) internal {
|
||||
_self.data = (_self.data & ~(1 << _reserveIndex*2+1)) | uint256(_usingAsCollateral ? 1 : 0) << (_reserveIndex * 2 + 1);
|
||||
_self.data =
|
||||
(_self.data & ~(1 << (_reserveIndex * 2 + 1))) |
|
||||
(uint256(_usingAsCollateral ? 1 : 0) << (_reserveIndex * 2 + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev used to validate if a user has been using the reserve for borrowing or as collateral
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve for borrowing or as collateral, false otherwise
|
||||
**/
|
||||
* @dev used to validate if a user has been using the reserve for borrowing or as collateral
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve for borrowing or as collateral, false otherwise
|
||||
**/
|
||||
function isUsingAsCollateralOrBorrowing(UserConfiguration.Map memory _self, uint256 _reserveIndex)
|
||||
internal
|
||||
view
|
||||
|
@ -62,11 +65,11 @@ library UserConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* @dev used to validate if a user has been using the reserve for borrowing
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve for borrowing, false otherwise
|
||||
**/
|
||||
* @dev used to validate if a user has been using the reserve for borrowing
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve for borrowing, false otherwise
|
||||
**/
|
||||
function isBorrowing(UserConfiguration.Map memory _self, uint256 _reserveIndex)
|
||||
internal
|
||||
view
|
||||
|
@ -76,11 +79,11 @@ library UserConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* @dev used to validate if a user has been using the reserve as collateral
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve as collateral, false otherwise
|
||||
**/
|
||||
* @dev used to validate if a user has been using the reserve as collateral
|
||||
* @param _self the configuration object
|
||||
* @param _reserveIndex the index of the reserve in the bitmap
|
||||
* @return true if the user has been using a reserve as collateral, false otherwise
|
||||
**/
|
||||
function isUsingAsCollateral(UserConfiguration.Map memory _self, uint256 _reserveIndex)
|
||||
internal
|
||||
view
|
||||
|
@ -90,20 +93,20 @@ library UserConfiguration {
|
|||
}
|
||||
|
||||
/**
|
||||
* @dev used to validate if a user has been borrowing from any reserve
|
||||
* @param _self the configuration object
|
||||
* @return true if the user has been borrowing any reserve, false otherwise
|
||||
**/
|
||||
* @dev used to validate if a user has been borrowing from any reserve
|
||||
* @param _self the configuration object
|
||||
* @return true if the user has been borrowing any reserve, false otherwise
|
||||
**/
|
||||
function isBorrowingAny(UserConfiguration.Map memory _self) internal view returns (bool) {
|
||||
return _self.data & BORROWING_MASK != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev used to validate if a user has not been using any reserve
|
||||
* @param _self the configuration object
|
||||
* @return true if the user has been borrowing any reserve, false otherwise
|
||||
**/
|
||||
function isEmpty(UserConfiguration.Map memory _self) internal view returns(bool) {
|
||||
* @dev used to validate if a user has not been using any reserve
|
||||
* @param _self the configuration object
|
||||
* @return true if the user has been borrowing any reserve, false otherwise
|
||||
**/
|
||||
function isEmpty(UserConfiguration.Map memory _self) internal view returns (bool) {
|
||||
return _self.data == 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
|
|||
import {ReserveConfiguration} from './ReserveConfiguration.sol';
|
||||
import {UserConfiguration} from './UserConfiguration.sol';
|
||||
import {IPriceOracleGetter} from '../interfaces/IPriceOracleGetter.sol';
|
||||
import {IFeeProvider} from '../interfaces/IFeeProvider.sol';
|
||||
import '@nomiclabs/buidler/console.sol';
|
||||
|
||||
/**
|
||||
|
|
|
@ -146,7 +146,8 @@
|
|||
},
|
||||
"TokenDistributor": {
|
||||
"buidlerevm": {
|
||||
"address": "0x2cfcA5785261fbC88EFFDd46fCFc04c22525F9e4"
|
||||
"address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10",
|
||||
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
|
||||
},
|
||||
"localhost": {
|
||||
"address": "0x2cfcA5785261fbC88EFFDd46fCFc04c22525F9e4"
|
||||
|
@ -164,7 +165,7 @@
|
|||
},
|
||||
"MockFlashLoanReceiver": {
|
||||
"buidlerevm": {
|
||||
"address": "0x3bDA11B584dDff7F66E0cFe1da1562c92B45db60"
|
||||
"address": "0xDf73fC454FA018051D4a1509e63D11530A59DE10"
|
||||
},
|
||||
"localhost": {
|
||||
"address": "0x3bDA11B584dDff7F66E0cFe1da1562c92B45db60"
|
||||
|
@ -172,7 +173,7 @@
|
|||
},
|
||||
"WalletBalanceProvider": {
|
||||
"buidlerevm": {
|
||||
"address": "0x392E5355a0e88Bd394F717227c752670fb3a8020",
|
||||
"address": "0x2cfcA5785261fbC88EFFDd46fCFc04c22525F9e4",
|
||||
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
|
||||
},
|
||||
"localhost": {
|
||||
|
@ -412,7 +413,7 @@
|
|||
},
|
||||
"AaveProtocolTestHelpers": {
|
||||
"buidlerevm": {
|
||||
"address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460"
|
||||
"address": "0xC6bA6049F86d528698B5924B8fC2FE7289D38578"
|
||||
},
|
||||
"localhost": {
|
||||
"address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460"
|
||||
|
@ -450,7 +451,7 @@
|
|||
},
|
||||
"MockAToken": {
|
||||
"buidlerevm": {
|
||||
"address": "0xD4B5A49d5bA242572ec3f4A8E52B97a10AF2543a",
|
||||
"address": "0x3b050AFb4ac4ACE646b31fF3639C1CD43aC31460",
|
||||
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
|
||||
},
|
||||
"localhost": {
|
||||
|
@ -470,7 +471,7 @@
|
|||
},
|
||||
"MockStableDebtToken": {
|
||||
"buidlerevm": {
|
||||
"address": "0x6aaF7e94e099291a94ed8E245c90f4766CE9bB7C",
|
||||
"address": "0xEBAB67ee3ef604D5c250A53b4b8fcbBC6ec3007C",
|
||||
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
|
||||
},
|
||||
"localhost": {
|
||||
|
@ -480,7 +481,7 @@
|
|||
},
|
||||
"MockVariableDebtToken": {
|
||||
"buidlerevm": {
|
||||
"address": "0x40A939911b662656C0EE71c19B954DB1911Dc8e3",
|
||||
"address": "0xBE36BE5680244Ae1A6F983E4A6f6E1c142cdAbe3",
|
||||
"deployer": "0xc783df8a850f42e7F7e57013759C285caa701eB6"
|
||||
},
|
||||
"localhost": {
|
||||
|
|
|
@ -23,7 +23,6 @@ import {MockAggregator} from '../types/MockAggregator';
|
|||
import {LendingRateOracle} from '../types/LendingRateOracle';
|
||||
import {DefaultReserveInterestRateStrategy} from '../types/DefaultReserveInterestRateStrategy';
|
||||
import {LendingPoolLiquidationManager} from '../types/LendingPoolLiquidationManager';
|
||||
import {TokenDistributor} from '../types/TokenDistributor';
|
||||
import {InitializableAdminUpgradeabilityProxy} from '../types/InitializableAdminUpgradeabilityProxy';
|
||||
import {MockFlashLoanReceiver} from '../types/MockFlashLoanReceiver';
|
||||
import {WalletBalanceProvider} from '../types/WalletBalanceProvider';
|
||||
|
@ -206,9 +205,6 @@ export const deployLendingPoolLiquidationManager = async () => {
|
|||
return (await liquidationManager.deployed()) as LendingPoolLiquidationManager;
|
||||
};
|
||||
|
||||
export const deployTokenDistributor = async () =>
|
||||
await deployContract<TokenDistributor>(eContractid.TokenDistributor, []);
|
||||
|
||||
export const deployInitializableAdminUpgradeabilityProxy = async () =>
|
||||
await deployContract<InitializableAdminUpgradeabilityProxy>(
|
||||
eContractid.InitializableAdminUpgradeabilityProxy,
|
||||
|
@ -402,14 +398,6 @@ export const getMockFlashLoanReceiver = async (address?: tEthereumAddress) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getTokenDistributor = async (address?: tEthereumAddress) => {
|
||||
return await getContract<TokenDistributor>(
|
||||
eContractid.TokenDistributor,
|
||||
address ||
|
||||
(await getDb().get(`${eContractid.TokenDistributor}.${BRE.network.name}`).value()).address
|
||||
);
|
||||
};
|
||||
|
||||
export const getLendingRateOracle = async (address?: tEthereumAddress) => {
|
||||
return await getContract<LendingRateOracle>(
|
||||
eContractid.LendingRateOracle,
|
||||
|
|
|
@ -14,8 +14,6 @@ import {
|
|||
deployLendingRateOracle,
|
||||
deployDefaultReserveInterestRateStrategy,
|
||||
deployLendingPoolLiquidationManager,
|
||||
deployTokenDistributor,
|
||||
deployInitializableAdminUpgradeabilityProxy,
|
||||
deployMockFlashLoanReceiver,
|
||||
deployWalletBalancerProvider,
|
||||
getFeeProvider,
|
||||
|
@ -29,7 +27,7 @@ import {
|
|||
deployGenericAToken,
|
||||
} from '../helpers/contracts-helpers';
|
||||
import {LendingPoolAddressesProvider} from '../types/LendingPoolAddressesProvider';
|
||||
import {Wallet, ContractTransaction, ethers, Signer} from 'ethers';
|
||||
import {ContractTransaction, Signer} from 'ethers';
|
||||
import {
|
||||
TokenContractId,
|
||||
eContractid,
|
||||
|
@ -62,7 +60,7 @@ import path from 'path';
|
|||
import fs from 'fs';
|
||||
|
||||
['misc'].forEach((folder) => {
|
||||
const tasksPath = path.join('/src/', 'tasks', folder);
|
||||
const tasksPath = path.join(__dirname, '../', 'tasks', folder);
|
||||
fs.readdirSync(tasksPath).forEach((task) => require(`${tasksPath}/${task}`));
|
||||
});
|
||||
|
||||
|
@ -509,29 +507,6 @@ const buildTestEnv = async (deployer: Signer, secondaryWallet: Signer) => {
|
|||
await addressesProvider.setLendingPoolLiquidationManager(liquidationManager.address)
|
||||
);
|
||||
|
||||
const {receivers, percentages} = getFeeDistributionParamsCommon(lendingPoolManager);
|
||||
|
||||
const tokenDistributorImpl = await deployTokenDistributor();
|
||||
const tokenDistributorProxy = await deployInitializableAdminUpgradeabilityProxy();
|
||||
const implementationParams = tokenDistributorImpl.interface.encodeFunctionData('initialize', [
|
||||
ZERO_ADDRESS,
|
||||
tokensAddressesWithoutUsd.LEND,
|
||||
'0x0000000000000000000000000000000000000000', // TODO: finish removal
|
||||
receivers,
|
||||
percentages,
|
||||
Object.values(tokensAddressesWithoutUsd),
|
||||
]);
|
||||
await waitForTx(
|
||||
await tokenDistributorProxy['initialize(address,address,bytes)'](
|
||||
tokenDistributorImpl.address,
|
||||
await secondaryWallet.getAddress(),
|
||||
implementationParams
|
||||
)
|
||||
);
|
||||
await waitForTx(await addressesProvider.setTokenDistributor(tokenDistributorProxy.address));
|
||||
|
||||
await insertContractAddressInDb(eContractid.TokenDistributor, tokenDistributorProxy.address);
|
||||
|
||||
const mockFlashLoanReceiver = await deployMockFlashLoanReceiver(addressesProvider.address);
|
||||
await insertContractAddressInDb(eContractid.MockFlashLoanReceiver, mockFlashLoanReceiver.address);
|
||||
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import {TestEnv, makeSuite} from './helpers/make-suite';
|
||||
import {APPROVAL_AMOUNT_LENDING_POOL, oneRay} from '../helpers/constants';
|
||||
import {
|
||||
convertToCurrencyDecimals,
|
||||
getMockFlashLoanReceiver,
|
||||
getTokenDistributor,
|
||||
} from '../helpers/contracts-helpers';
|
||||
import {convertToCurrencyDecimals, getMockFlashLoanReceiver} from '../helpers/contracts-helpers';
|
||||
import {ethers} from 'ethers';
|
||||
import {MockFlashLoanReceiver} from '../types/MockFlashLoanReceiver';
|
||||
import {TokenDistributor} from '../types/TokenDistributor';
|
||||
import {BRE} from '../helpers/misc-utils';
|
||||
import {ProtocolErrors} from '../helpers/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
|
@ -16,7 +10,6 @@ const {expect} = require('chai');
|
|||
|
||||
makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
||||
let _mockFlashLoanReceiver = {} as MockFlashLoanReceiver;
|
||||
let _tokenDistributor = {} as TokenDistributor;
|
||||
const {
|
||||
INCONSISTENT_PROTOCOL_BALANCE,
|
||||
TOO_SMALL_FLASH_LOAN,
|
||||
|
@ -25,7 +18,6 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
|||
|
||||
before(async () => {
|
||||
_mockFlashLoanReceiver = await getMockFlashLoanReceiver();
|
||||
_tokenDistributor = await getTokenDistributor();
|
||||
});
|
||||
|
||||
it('Deposits ETH into the reserve', async () => {
|
||||
|
@ -60,16 +52,13 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
|||
.plus(reserveData.totalBorrowsStable.toString())
|
||||
.plus(reserveData.totalBorrowsVariable.toString());
|
||||
|
||||
const tokenDistributorBalance = await weth.balanceOf(_tokenDistributor.address);
|
||||
|
||||
expect(totalLiquidity.toString()).to.be.equal('1000504000000000000');
|
||||
expect(totalLiquidity.toString()).to.be.equal('1000720000000000000');
|
||||
expect(currentLiquidityRate.toString()).to.be.equal('0');
|
||||
expect(currentLiquidityIndex.toString()).to.be.equal('1000504000000000000000000000');
|
||||
expect(tokenDistributorBalance).to.be.equal('216000000000000');
|
||||
expect(currentLiquidityIndex.toString()).to.be.equal('1000720000000000000000000000');
|
||||
});
|
||||
|
||||
it('Takes an ETH flashloan as big as the available liquidity', async () => {
|
||||
const {pool, deployer, weth} = testEnv;
|
||||
const {pool, weth} = testEnv;
|
||||
|
||||
const txResult = await pool.flashLoan(
|
||||
_mockFlashLoanReceiver.address,
|
||||
|
@ -79,7 +68,6 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
|||
);
|
||||
|
||||
const reserveData = await pool.getReserveData(weth.address);
|
||||
const tokenDistributorBalance = await weth.balanceOf(_tokenDistributor.address);
|
||||
|
||||
const currentLiqudityRate = reserveData.liquidityRate;
|
||||
const currentLiquidityIndex = reserveData.liquidityIndex;
|
||||
|
@ -88,10 +76,9 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
|||
.plus(reserveData.totalBorrowsStable.toString())
|
||||
.plus(reserveData.totalBorrowsVariable.toString());
|
||||
|
||||
expect(totalLiquidity.toString()).to.be.equal('1001134317520000000');
|
||||
expect(totalLiquidity.toString()).to.be.equal('1001620454000000000');
|
||||
expect(currentLiqudityRate.toString()).to.be.equal('0');
|
||||
expect(currentLiquidityIndex.toString()).to.be.equal('1001134317520000000000000000');
|
||||
expect(tokenDistributorBalance.toString()).to.be.equal('486136080000000');
|
||||
});
|
||||
|
||||
it('Takes WETH flashloan, does not return the funds (revert expected)', async () => {
|
||||
|
@ -180,22 +167,15 @@ makeSuite('LendingPool FlashLoan function', (testEnv: TestEnv) => {
|
|||
const currentLiquidityIndex = reserveData.liquidityIndex.toString();
|
||||
const currentUserBalance = userData.currentATokenBalance.toString();
|
||||
|
||||
const expectedLiquidity = ethers.utils.parseEther('1000.315');
|
||||
|
||||
const tokenDistributorBalance = await dai.balanceOf(_tokenDistributor.address);
|
||||
const expectedLiquidity = ethers.utils.parseEther('1000.450');
|
||||
|
||||
expect(totalLiquidity).to.be.equal(expectedLiquidity, 'Invalid total liquidity');
|
||||
expect(currentLiqudityRate).to.be.equal('0', 'Invalid liquidity rate');
|
||||
expect(currentLiquidityIndex).to.be.equal(
|
||||
new BigNumber('1.000315').multipliedBy(oneRay).toFixed(),
|
||||
new BigNumber('1.00045').multipliedBy(oneRay).toFixed(),
|
||||
'Invalid liquidity index'
|
||||
);
|
||||
expect(currentUserBalance.toString()).to.be.equal(expectedLiquidity, 'Invalid user balance');
|
||||
|
||||
expect(tokenDistributorBalance.toString()).to.be.equal(
|
||||
ethers.utils.parseEther('0.135'),
|
||||
'Invalid token distributor balance'
|
||||
);
|
||||
});
|
||||
|
||||
it('Takes out a 500 DAI flashloan, does not return the funds (revert expected)', async () => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user