diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 8d25f5ad4..69deccb81 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -6,13 +6,14 @@ import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interf import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol"; import { IPrime } from "@venusprotocol/venus-protocol/contracts/Tokens/Prime/Interfaces/IPrime.sol"; -import { ComptrollerInterface, Action } from "./ComptrollerInterface.sol"; +import { ComptrollerInterface, VTokenInterface, Action } from "./ComptrollerInterface.sol"; import { ComptrollerStorage } from "./ComptrollerStorage.sol"; import { ExponentialNoError } from "./ExponentialNoError.sol"; import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; import { MaxLoopsLimitHelper } from "./MaxLoopsLimitHelper.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; +import { IFlashLoanReceiver } from "./Flashloan/interfaces/IFlashloanReceiver.sol"; /** * @title Comptroller @@ -100,9 +101,13 @@ contract Comptroller is /// @notice Emitted when a market is unlisted event MarketUnlisted(address indexed vToken); + /// @notice Emitted when the borrowing or redeeming delegate rights are updated for an account event DelegateUpdated(address indexed approver, address indexed delegate, bool approved); + /// @notice Emitted When the flash loan is successfully executed + event FlashLoanExecuted(address receiver, VTokenInterface[] assets, uint256[] amounts); + /// @notice Thrown when collateral factor exceeds the upper bound error InvalidCollateralFactor(); @@ -163,6 +168,24 @@ contract Comptroller is /// @notice Thrown when collateral factor is not zero error CollateralFactorIsNotZero(); + /// @notice Thrown when the close factor is invalid + error InvalidCloseFactor(); + + /// @notice Thrown when the liquidation incentive is invalid + error InvalidLiquidationIncentive(); + + /// @notice Thrown when the VToken is invalid + error InvalidVToken(); + + /// @notice Thrown when the input is invalid + error InvalidInput(); + + /// @notice Thrown when the rewards distributor already exists + error RewardsDistributorAlreadyExists(); + + /// @notice Thrown when the market does not exist + error MarketNotExist(); + /** * @notice Thrown during the liquidation if user's total collateral amount is lower than * a predefined threshold. In this case only batch liquidations (either liquidateAccount @@ -199,6 +222,18 @@ contract Comptroller is /// @notice Thrown if delegate approval status is already set to the requested value error DelegationStatusUnchanged(); + /// @notice Thrown if invalid flashLoan params passed + error InvalidFlashLoanParams(); + + ///@notice Thrown if the flashLoan is not enabled for a particular market + error FlashLoanNotEnabled(address market); + + ///@notice Thrown if repayment amount is insufficient + error InsufficientReypaymentBalance(address tokenAddress); + + ///@notice Thrown if executeOperation failed + error ExecuteFlashLoanFailed(); + /// @param poolRegistry_ Pool registry address /// @custom:oz-upgrades-unsafe-allow constructor /// @custom:error ZeroAddressNotAllowed is thrown when pool registry address is zero @@ -919,6 +954,68 @@ contract Comptroller is } } + /** + * @notice Executes a flashLoan operation with the specified assets and amounts. + * @dev Transfer the specified assets to the receiver contract and ensures that the total repayment (amount + fee) + * is returned by the receiver contract after the operation for each asset. The function performs checks to ensure the validity + * of parameters, that flashLoans are enabled for the given assets, and that the total repayment is sufficient. + * Reverts on invalid parameters, disabled flashLoans, or insufficient repayment. + * @param receiver The address of the contract that will receive the flashLoan and execute the operation. + * @param assets The addresses of the assets to be loaned. + * @param amounts The amounts of each asset to be loaned. + * @custom:requirements + * - `assets.length` must be equal to `amounts.length`. + * - `assets.length` and `amounts.length` must not be zero. + * - The `receiver` address must not be the zero address. + * - FlashLoans must be enabled for each asset. + * - The `receiver` contract must repay the loan with the appropriate fee. + * @custom:reverts + * - Reverts with `InvalidFlashLoanParams()` if parameter checks fail. + * - Reverts with `FlashLoanNotEnabled(asset)` if flashLoans are disabled for any of the requested assets. + * - Reverts with `ExecuteFlashLoanFailed` if the receiver contract fails to execute the operation. + * - Reverts with `InsufficientReypaymentBalance(asset)` if the repayment (amount + fee) is insufficient after the operation. + */ + function executeFlashLoan( + address receiver, + VTokenInterface[] calldata assets, + uint256[] calldata amounts + ) external override { + // Asset and amount length must be equals and not be zero + if (assets.length != amounts.length || assets.length == 0 || receiver == address(0)) { + revert InvalidFlashLoanParams(); + } + + uint256 len = assets.length; + uint256[] memory fees = new uint256[](len); + uint256[] memory balanceBefore = new uint256[](len); + + for (uint256 j; j < len; ) { + (fees[j], ) = (assets[j]).calculateFee(receiver, amounts[j]); + + // Transfer the asset + (balanceBefore[j]) = (assets[j]).transferUnderlying(receiver, amounts[j]); + + unchecked { + ++j; + } + } + + // Call the execute operation on receiver contract + if (!IFlashLoanReceiver(receiver).executeOperation(assets, amounts, fees, receiver, "")) { + revert ExecuteFlashLoanFailed(); + } + + for (uint256 k; k < len; ) { + (assets[k]).verifyBalance(balanceBefore[k], amounts[k] + fees[k]); + + unchecked { + ++k; + } + } + + emit FlashLoanExecuted(receiver, assets, amounts); + } + /** * @notice Liquidates all borrows of the borrower. Callable only if the collateral is less than * a predefined threshold, and the account collateral can be seized to cover all borrows. If @@ -983,7 +1080,10 @@ contract Comptroller is for (uint256 i; i < marketsCount; ++i) { (, uint256 borrowBalance, ) = _safeGetAccountSnapshot(borrowMarkets[i], borrower); - require(borrowBalance == 0, "Nonzero borrow balance after liquidation"); + // require(borrowBalance == 0, "Nonzero borrow balance after liquidation"); + if (borrowBalance > 0) { + revert NonzeroBorrowBalance(); + } } } @@ -995,8 +1095,11 @@ contract Comptroller is */ function setCloseFactor(uint256 newCloseFactorMantissa) external { _checkAccessAllowed("setCloseFactor(uint256)"); - require(MAX_CLOSE_FACTOR_MANTISSA >= newCloseFactorMantissa, "Close factor greater than maximum close factor"); - require(MIN_CLOSE_FACTOR_MANTISSA <= newCloseFactorMantissa, "Close factor smaller than minimum close factor"); + if (MAX_CLOSE_FACTOR_MANTISSA < newCloseFactorMantissa || MIN_CLOSE_FACTOR_MANTISSA > newCloseFactorMantissa) { + revert InvalidCloseFactor(); + } + // require(MAX_CLOSE_FACTOR_MANTISSA >= newCloseFactorMantissa, "Close factor greater than maximum close factor"); + // require(MIN_CLOSE_FACTOR_MANTISSA <= newCloseFactorMantissa, "Close factor smaller than minimum close factor"); uint256 oldCloseFactorMantissa = closeFactorMantissa; closeFactorMantissa = newCloseFactorMantissa; @@ -1071,7 +1174,10 @@ contract Comptroller is * @custom:access Controlled by AccessControlManager */ function setLiquidationIncentive(uint256 newLiquidationIncentiveMantissa) external { - require(newLiquidationIncentiveMantissa >= MANTISSA_ONE, "liquidation incentive should be greater than 1e18"); + if (newLiquidationIncentiveMantissa < MANTISSA_ONE) { + revert InvalidLiquidationIncentive(); + } + // require(newLiquidationIncentiveMantissa >= MANTISSA_ONE, "liquidation incentive should be greater than 1e18"); _checkAccessAllowed("setLiquidationIncentive(uint256)"); @@ -1099,7 +1205,11 @@ contract Comptroller is revert MarketAlreadyListed(address(vToken)); } - require(vToken.isVToken(), "Comptroller: Invalid vToken"); // Sanity check to make sure its really a VToken + if (!vToken.isVToken()) { + revert InvalidVToken(); + } + + // require(vToken.isVToken(), "Comptroller: Invalid vToken"); // Sanity check to make sure its really a VToken Market storage newMarket = markets[address(vToken)]; newMarket.isListed = true; @@ -1133,7 +1243,10 @@ contract Comptroller is uint256 numMarkets = vTokens.length; uint256 numBorrowCaps = newBorrowCaps.length; - require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); + if (numMarkets == 0 || numMarkets != numBorrowCaps) { + revert InvalidInput(); + } + // require(numMarkets != 0 && numMarkets == numBorrowCaps, "invalid input"); _ensureMaxLoops(numMarkets); @@ -1157,8 +1270,11 @@ contract Comptroller is _checkAccessAllowed("setMarketSupplyCaps(address[],uint256[])"); uint256 vTokensCount = vTokens.length; - require(vTokensCount != 0, "invalid number of markets"); - require(vTokensCount == newSupplyCaps.length, "invalid number of markets"); + if (vTokensCount == 0 || vTokensCount != newSupplyCaps.length) { + revert InvalidInput(); + } + // require(vTokensCount != 0, "invalid number of markets"); + // require(vTokensCount == newSupplyCaps.length, "invalid number of markets"); _ensureMaxLoops(vTokensCount); @@ -1216,7 +1332,10 @@ contract Comptroller is * @custom:event Emits NewRewardsDistributor with distributor address */ function addRewardsDistributor(RewardsDistributor _rewardsDistributor) external onlyOwner { - require(!rewardsDistributorExists[address(_rewardsDistributor)], "already exists"); + if (rewardsDistributorExists[address(_rewardsDistributor)]) { + revert RewardsDistributorAlreadyExists(); + } + // require(!rewardsDistributorExists[address(_rewardsDistributor)], "already exists"); uint256 rewardsDistributorsLen = rewardsDistributors.length; _ensureMaxLoops(rewardsDistributorsLen + 1); @@ -1554,7 +1673,10 @@ contract Comptroller is * @param paused The new paused state (true=paused, false=unpaused) */ function _setActionPaused(address market, Action action, bool paused) internal { - require(markets[market].isListed, "cannot pause a market that is not listed"); + if (!markets[market].isListed) { + revert MarketNotExist(); + } + // require(markets[market].isListed, "cannot pause a market that is not listed"); _actionPaused[market][action] = paused; emit ActionPausedMarket(VToken(market), action, paused); } diff --git a/contracts/ComptrollerInterface.sol b/contracts/ComptrollerInterface.sol index 662a6aef9..4a2ef4d1a 100644 --- a/contracts/ComptrollerInterface.sol +++ b/contracts/ComptrollerInterface.sol @@ -5,6 +5,7 @@ import { ResilientOracleInterface } from "@venusprotocol/oracle/contracts/interf import { VToken } from "./VToken.sol"; import { RewardsDistributor } from "./Rewards/RewardsDistributor.sol"; +import { VTokenInterface } from "./VTokenInterfaces.sol"; enum Action { MINT, @@ -90,6 +91,8 @@ interface ComptrollerInterface { function preTransferHook(address vToken, address src, address dst, uint256 transferTokens) external; + function executeFlashLoan(address receiver, VTokenInterface[] calldata assets, uint256[] calldata amounts) external; + function isComptroller() external view returns (bool); /*** Liquidity/Liquidation Calculations ***/ diff --git a/contracts/ErrorReporter.sol b/contracts/ErrorReporter.sol index 080c0b73c..64b0e3d30 100644 --- a/contracts/ErrorReporter.sol +++ b/contracts/ErrorReporter.sol @@ -46,4 +46,9 @@ contract TokenErrorReporter { error ReduceReservesCashValidation(); error SetInterestRateModelFreshCheck(); + + error FlashLoanNotEnabled(address); + error ExecuteFlashLoanFailed(); + error InvalidComptroller(address comptroller); + error InsufficientReypaymentBalance(address tokenAddress); } diff --git a/contracts/Flashloan/base/FlashloanReceiverBase.sol b/contracts/Flashloan/base/FlashloanReceiverBase.sol new file mode 100644 index 000000000..5ed9f6ee5 --- /dev/null +++ b/contracts/Flashloan/base/FlashloanReceiverBase.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { IFlashLoanReceiver } from "../interfaces/IFlashloanReceiver.sol"; +import { ComptrollerInterface } from "../../ComptrollerInterface.sol"; + +/// @title FlashLoanReceiverBase +/// @notice A base contract for implementing flashLoan receiver logic. +/// @dev This abstract contract provides the necessary structure for inheriting contracts to implement the `IFlashLoanReceiver` interface. +/// It stores a reference to the Comptroller contract, which manages various aspects of the protocol. +abstract contract FlashLoanReceiverBase is IFlashLoanReceiver { + /// @notice The Comptroller contract that governs the protocol. + /// @dev This immutable variable stores the address of the Comptroller contract, which cannot be changed after deployment. + ComptrollerInterface public immutable COMPTROLLER; + + /** + * @notice Constructor to initialize the base contract with the Comptroller address. + * @param comptroller_ The address of the Comptroller contract that oversees the protocol. + */ + constructor(ComptrollerInterface comptroller_) { + COMPTROLLER = comptroller_; + } +} diff --git a/contracts/Flashloan/base/FlashloanSimpleReceiverBase.sol b/contracts/Flashloan/base/FlashloanSimpleReceiverBase.sol new file mode 100644 index 000000000..ecf45c1a3 --- /dev/null +++ b/contracts/Flashloan/base/FlashloanSimpleReceiverBase.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { IFlashLoanSimpleReceiver } from "../interfaces/IFlashloanSimpleReceiver.sol"; +import { VTokenInterface } from "../../VTokenInterfaces.sol"; + +/** + * @title FlashLoanSimpleReceiverBase + * @author Venus + * @notice Base contract to develop a flashLoan-receiver contract. + * @dev This contract serves as a foundational contract for implementing custom flash loan receiver logic. + * Inheritors of this contract need to implement the `executeOperation` function defined in the `IFlashLoanSimpleReceiver` interface. + */ +abstract contract FlashLoanSimpleReceiverBase is IFlashLoanSimpleReceiver { + /// @notice The VToken contract used to initiate and handle flash loan + /// @dev This is an immutable reference to the VTokenInterface, which enables the flash loan functionality. + VTokenInterface public immutable VTOKEN; + + /// @notice Initializes the base contract by setting the VToken address + /// @param vToken_ The address of the VToken contract that supports flash loan + constructor(VTokenInterface vToken_) { + VTOKEN = vToken_; + } +} diff --git a/contracts/Flashloan/interfaces/IFlashloanReceiver.sol b/contracts/Flashloan/interfaces/IFlashloanReceiver.sol new file mode 100644 index 000000000..e385f1374 --- /dev/null +++ b/contracts/Flashloan/interfaces/IFlashloanReceiver.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +import { VTokenInterface } from "../../VTokenInterfaces.sol"; + +/// @title IFlashLoanReceiver +/// @notice Interface for flashLoan receiver contract, which execute custom logic with flash-borrowed assets. +/// @dev This interface defines the method that must be implemented by any contract wishing to interact with the flashLoan system. +/// Contracts must ensure they have the means to repay both the flashLoan amount and the associated premium (fee). +interface IFlashLoanReceiver { + /** + * @notice Executes an operation after receiving the flash-borrowed assets. + * @dev Implementation of this function must ensure the borrowed amount plus the premium (fee) is repaid within the same transaction. + * @param assets The addresses of the assets that were flash-borrowed. + * @param amounts The amounts of each of the flash-borrowed assets. + * @param premiums The premiums (fees) associated with each flash-borrowed asset. + * @param initiator The address that initiated the flashLoan operation. + * @param param Additional parameters encoded as bytes. These can be used to pass custom data to the receiver contract. + * @return True if the operation succeeds and the borrowed amount plus the premium is repaid, false otherwise. + */ + function executeOperation( + VTokenInterface[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata param + ) external returns (bool); +} diff --git a/contracts/Flashloan/interfaces/IFlashloanSimpleReceiver.sol b/contracts/Flashloan/interfaces/IFlashloanSimpleReceiver.sol new file mode 100644 index 000000000..a7c000dfb --- /dev/null +++ b/contracts/Flashloan/interfaces/IFlashloanSimpleReceiver.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity 0.8.25; + +interface IFlashLoanSimpleReceiver { + /** + * @notice Executes an operation after receiving the flash-borrowed asset + * @dev Ensure that the contract can return the debt + premium, e.g., has + * enough funds to repay and has to transfer the debt + premium to the VToken + * @param asset The address of the flash-borrowed asset + * @param amount The amount of the flash-borrowed asset + * @param premium The premium (fee) associated with flash-borrowed asset. + * @param initiator The address that initiated the flashLoan operation + * @param param The byte-encoded param passed when initiating the flashLoan + * @return True if the execution of the operation succeeds, false otherwise + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata param + ) external returns (bool); +} diff --git a/contracts/VToken.sol b/contracts/VToken.sol index 0e12b02f7..748fe4954 100644 --- a/contracts/VToken.sol +++ b/contracts/VToken.sol @@ -14,6 +14,7 @@ import { InterestRateModel } from "./InterestRateModel.sol"; import { ExponentialNoError } from "./ExponentialNoError.sol"; import { TimeManagerV8 } from "@venusprotocol/solidity-utilities/contracts/TimeManagerV8.sol"; import { ensureNonzeroAddress } from "./lib/validators.sol"; +import { IFlashLoanSimpleReceiver } from "./Flashloan/interfaces/IFlashloanSimpleReceiver.sol"; /** * @title VToken @@ -106,6 +107,8 @@ contract VToken is * @param accessControlManager_ AccessControlManager contract address * @param riskManagement Addresses of risk & income related contracts * @param reserveFactorMantissa_ Percentage of borrow interest that goes to reserves (from 0 to 1e18) + * @param flashLoanEnabled_ Enable flashLoan or not for this market + * @param flashLoanFeeMantissa_ FlashLoan fee mantissa * @custom:error ZeroAddressNotAllowed is thrown when admin address is zero * @custom:error ZeroAddressNotAllowed is thrown when shortfall contract address is zero * @custom:error ZeroAddressNotAllowed is thrown when protocol share reserve address is zero @@ -121,7 +124,9 @@ contract VToken is address admin_, address accessControlManager_, RiskManagementInit memory riskManagement, - uint256 reserveFactorMantissa_ + uint256 reserveFactorMantissa_, + bool flashLoanEnabled_, + uint256 flashLoanFeeMantissa_ ) external initializer { ensureNonzeroAddress(admin_); @@ -137,7 +142,9 @@ contract VToken is admin_, accessControlManager_, riskManagement, - reserveFactorMantissa_ + reserveFactorMantissa_, + flashLoanEnabled_, + flashLoanFeeMantissa_ ); } @@ -697,6 +704,98 @@ contract VToken is emit SweepToken(address(token)); } + /** + * @notice Transfers the underlying asset to the specified address. + * @dev Can only be called by the Comptroller contract. This function performs the actual transfer of the underlying + * asset by calling the `_doTransferOut` internal function. + * @param to The address to which the underlying asset is to be transferred. + * @param amount The amount of the underlying asset to transfer. + * @custom:requirements + * - The caller must be the Comptroller contract. + * @custom:reverts + * - Reverts with "Only Comptroller" if the caller is not the Comptroller. + * @custom:event Emits FlashLoanAmountTransferred event on successful transfer of amount to receiver + */ + function transferUnderlying( + address to, + uint256 amount + ) external override nonReentrant returns (uint256 balanceBefore) { + if (msg.sender != address(comptroller)) { + revert InvalidComptroller(address(comptroller)); + } + + _doTransferOut(to, amount); + + balanceBefore = _getCashPrior(); + emit FlashLoanAmountTransferred(underlying, to, amount); + } + + /** + * @notice Executes a flashLoan operation. + * @dev Transfers the amount to the receiver contract and ensures that the total repayment (amount + fee) + * is returned by the receiver contract after the operation. The function performs checks to ensure the validity + * of parameters, that flashLoan is enabled for the given asset, and that the total repayment is sufficient. + * Reverts on invalid parameters, disabled flashLoans, or insufficient repayment. + * @param receiver The address of the contract that will receive the flashLoan and execute the operation. + * @param amount The amount of asset to be loaned. + * @custom:requirements + * - The `receiver` address must not be the zero address. + * - FlashLoans must be enabled for the asset. + * - The `receiver` contract must repay the loan with the appropriate fee. + * @custom:reverts + * - Reverts with `FlashLoanNotEnabled(asset)` if flashLoans are disabled for any of the requested assets. + * - Reverts with `ExecuteFlashLoanFailed` if the receiver contract fails to execute the operation. + * - Reverts with `InsufficientReypaymentBalance(asset)` if the repayment (amount + fee) is insufficient after the operation. + * @custom:event Emits FlashLoanExecuted event on success + */ + function executeFlashLoan(address receiver, uint256 amount) external override nonReentrant returns (uint256) { + uint256 repaymentAmount; + uint256 fee; + (fee, repaymentAmount) = calculateFee(receiver, amount); + + IFlashLoanSimpleReceiver receiverContract = IFlashLoanSimpleReceiver(receiver); + + // Transfer the underlying asset to the receiver. + _doTransferOut(receiver, amount); + + uint256 balanceBefore = _getCashPrior(); + + // Call the execute operation on receiver contract + if (!receiverContract.executeOperation(underlying, amount, fee, msg.sender, "")) { + revert ExecuteFlashLoanFailed(); + } + + verifyBalance(balanceBefore, repaymentAmount); + + emit FlashLoanExecuted(receiver, underlying, amount); + + return NO_ERROR; + } + + /** + * @notice Enable or disable flash loan for the market + * @custom:access Only Governance + * @custom:event Emits ToggleFlashLoanEnabled event on success + */ + function toggleFlashLoan() external override { + _checkAccessAllowed("toggleFlashLoan()"); + isFlashLoanEnabled = !isFlashLoanEnabled; + + emit ToggleFlashLoanEnabled(!isFlashLoanEnabled, isFlashLoanEnabled); + } + + /** + * @notice Update flashLoan fee mantissa + * @custom:access Only Governance + * @custom:event Emits FlashLoanFeeUpdated event on success + */ + function setFlashLoanFeeMantissa(uint256 fee) external override { + _checkAccessAllowed("setFlashLoanFeeMantissa(uint256)"); + + emit FlashLoanFeeUpdated(flashLoanFeeMantissa, fee); + flashLoanFeeMantissa = fee; + } + /** * @notice A public function to set new threshold of slot(block or second) difference after which funds will be sent to the protocol share reserve * @param _newReduceReservesBlockOrTimestampDelta slot(block or second) difference value @@ -883,6 +982,43 @@ contract VToken is return NO_ERROR; } + /** + * @notice Calculates the fee and repayment amount for a flash loan. + * @param receiver The address of the receiver of the flash loan. + * @param amount The amount of the flash loan. + * @return fee The calculated fee for the flash loan. + * @return repaymentAmount The total amount to be repaid (amount + fee). + * @dev This function reverts if flash loans are not enabled. + */ + function calculateFee( + address receiver, + uint256 amount + ) public view override returns (uint256 fee, uint256 repaymentAmount) { + if (!isFlashLoanEnabled) revert FlashLoanNotEnabled(address(this)); + ensureNonzeroAddress(receiver); + + fee = (amount * flashLoanFeeMantissa) / MANTISSA_ONE; + repaymentAmount = amount + fee; + } + + /** + * @notice Verifies that the balance after a flash loan is sufficient to cover the repayment amount. + * @param balanceBefore The balance before the flash loan. + * @param repaymentAmount The total amount to be repaid (amount + fee). + * @return NO_ERROR Indicates that the balance verification was successful. + * @dev This function reverts if the balance after the flash loan is insufficient to cover the repayment amount. + */ + function verifyBalance(uint256 balanceBefore, uint256 repaymentAmount) public view override returns (uint256) { + uint256 balanceAfter = _getCashPrior(); + + // balanceAfter should be greater than the fee calculated + if ((balanceAfter - balanceBefore) < repaymentAmount) { + revert InsufficientReypaymentBalance(underlying); + } + + return NO_ERROR; + } + /** * @notice User supplies assets into the market and receives vTokens in exchange * @dev Assumes interest has already been accrued up to the current block or timestamp @@ -1536,6 +1672,8 @@ contract VToken is * @param accessControlManager_ AccessControlManager contract address * @param riskManagement Addresses of risk & income related contracts * @param reserveFactorMantissa_ Percentage of borrow interest that goes to reserves (from 0 to 1e18) + * @param flashLoanEnabled_ Enable flashLoan or not for this market + * @param flashLoanFeeMantissa_ FlashLoan fee mantissa */ function _initialize( address underlying_, @@ -1548,7 +1686,9 @@ contract VToken is address admin_, address accessControlManager_, RiskManagementInit memory riskManagement, - uint256 reserveFactorMantissa_ + uint256 reserveFactorMantissa_, + bool flashLoanEnabled_, + uint256 flashLoanFeeMantissa_ ) internal onlyInitializing { __Ownable2Step_init(); __AccessControlled_init_unchained(accessControlManager_); @@ -1575,6 +1715,8 @@ contract VToken is _setShortfallContract(riskManagement.shortfall); _setProtocolShareReserve(riskManagement.protocolShareReserve); protocolSeizeShareMantissa = DEFAULT_PROTOCOL_SEIZE_SHARE_MANTISSA; + isFlashLoanEnabled = flashLoanEnabled_; + flashLoanFeeMantissa = flashLoanFeeMantissa_; // Set underlying and sanity check it underlying = underlying_; diff --git a/contracts/VTokenInterfaces.sol b/contracts/VTokenInterfaces.sol index 0f01e1f17..b2a6aed35 100644 --- a/contracts/VTokenInterfaces.sol +++ b/contracts/VTokenInterfaces.sol @@ -131,12 +131,22 @@ contract VTokenStorage { */ uint256 public reduceReservesBlockNumber; + /** + * @notice flashLoan is enabled for this market or not + */ + bool public isFlashLoanEnabled; + + /** + * @notice fee percentage collected by protocol on flashLoan + */ + uint256 public flashLoanFeeMantissa; + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps */ - uint256[48] private __gap; + uint256[46] private __gap; } /** @@ -288,6 +298,26 @@ abstract contract VTokenInterface is VTokenStorage { */ event ProtocolSeize(address indexed from, address indexed to, uint256 amount); + /** + * @notice Event emitted when flashLoanEnabled status is changed + */ + event ToggleFlashLoanEnabled(bool oldEnabled, bool enabled); + + /** + * @notice Event emitted when flashLoan is executed + */ + event FlashLoanExecuted(address receiver, address underlying, uint256 amount); + + /** + * @notice Event emitted when asset is transferred to receiver + */ + event FlashLoanAmountTransferred(address asset, address receiver, uint256 amount); + + /** + * @notice Event emitted when flashLoan fee mantissa is updated + */ + event FlashLoanFeeUpdated(uint256 oldFee, uint256 fee); + /*** User Interface ***/ function mint(uint256 mintAmount) external virtual returns (uint256); @@ -336,6 +366,10 @@ abstract contract VTokenInterface is VTokenStorage { function sweepToken(IERC20Upgradeable token) external virtual; + function transferUnderlying(address receiver, uint256 amount) external virtual returns (uint256 balanceBefore); + + function executeFlashLoan(address receiver, uint256 amount) external virtual returns (uint256); + /*** Admin Functions ***/ function setReserveFactor(uint256 newReserveFactorMantissa) external virtual; @@ -350,6 +384,10 @@ abstract contract VTokenInterface is VTokenStorage { function addReserves(uint256 addAmount) external virtual; + function toggleFlashLoan() external virtual; + + function setFlashLoanFeeMantissa(uint256 fee) external virtual; + function totalBorrowsCurrent() external virtual returns (uint256); function balanceOfUnderlying(address owner) external virtual returns (uint256); @@ -383,4 +421,11 @@ abstract contract VTokenInterface is VTokenStorage { function isVToken() external pure virtual returns (bool) { return true; } + + function calculateFee( + address receiver, + uint256 amount + ) public view virtual returns (uint256 fee, uint256 repaymentAmount); + + function verifyBalance(uint256 balanceBefore, uint256 repaymentAmount) public view virtual returns (uint256); } diff --git a/contracts/test/Mocks/MockFlashloanReceiver.sol b/contracts/test/Mocks/MockFlashloanReceiver.sol new file mode 100644 index 000000000..e5b09e308 --- /dev/null +++ b/contracts/test/Mocks/MockFlashloanReceiver.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { FlashLoanReceiverBase } from "../../Flashloan/base/FlashloanReceiverBase.sol"; +import { ComptrollerInterface } from "../../ComptrollerInterface.sol"; +import { VTokenInterface } from "../../VTokenInterfaces.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title MockFlashLoanReceiver +/// @notice A mock implementation of a flashLoan receiver contract that interacts with the Comptroller to request and handle flash loans. +/// @dev This contract extends `FlashLoanReceiverBase` and implements custom logic to request flash loans and repay them. +contract MockFlashLoanReceiver is FlashLoanReceiverBase { + /** + * @notice Constructor to initialize the flashLoan receiver with the Comptroller contract. + * @param comptroller The address of the Comptroller contract used to request flash loans. + */ + constructor(ComptrollerInterface comptroller) FlashLoanReceiverBase(comptroller) {} + + /** + * @notice Requests a flash loan from the Comptroller contract. + * @dev This function calls the `executeFlashLoan` function from the Comptroller to initiate a flash loan. + * @param assets_ An array of VToken contracts that support flash loans. + * @param amount_ An array of amounts to borrow in the flash loan for each corresponding asset. + */ + function requestFlashLoan(VTokenInterface[] calldata assets_, uint256[] memory amount_) external { + address receiver = address(this); // Receiver address is this contract itself + uint256[] memory amount = amount_; // Set the requested amounts + + // Request the flashLoan from the Comptroller contract + COMPTROLLER.executeFlashLoan(receiver, assets_, amount); + } + + /** + * @notice Executes custom logic after receiving the flash loan. + * @dev This function is called by the Comptroller contract as part of the flashLoan process. + * It must repay the loan amount plus the premium for each borrowed asset. + * @param assets The addresses of the VToken contracts for the flash-borrowed assets. + * @param amounts The amounts of each asset borrowed. + * @param premiums The fees for each flash-borrowed asset. + * @param initiator The address that initiated the flash loan. + * @param param Additional encoded parameters passed with the flash loan. + * @return True if the operation succeeds and the debt plus premium is repaid, false otherwise. + */ + function executeOperation( + VTokenInterface[] calldata assets, + uint256[] calldata amounts, + uint256[] calldata premiums, + address initiator, + bytes calldata param + ) external returns (bool) { + // 👇 Your custom logic for the flash loan should be implemented here 👇 + /** YOUR CUSTOM LOGIC HERE */ + // 👆 Your custom logic for the flash loan should be implemented above here 👆 + + // Calculate the total repayment amount (loan amount + premium) for each borrowed asset + uint256 len = assets.length; + for (uint256 k; k < len; ) { + uint256 total = amounts[k] + premiums[k]; + + // Transfer the repayment (amount + premium) back to the VToken contract + IERC20(VTokenInterface(assets[k]).underlying()).transfer(address(VTokenInterface(assets[k])), total); + unchecked { + ++k; + } + } + + // Return true to indicate successful execution of the flash loan operation + return true; + } +} diff --git a/contracts/test/Mocks/MockFlashloanSimpleReceiver.sol b/contracts/test/Mocks/MockFlashloanSimpleReceiver.sol new file mode 100644 index 000000000..5374f1d8e --- /dev/null +++ b/contracts/test/Mocks/MockFlashloanSimpleReceiver.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +pragma solidity ^0.8.10; + +import { FlashLoanSimpleReceiverBase } from "../../Flashloan/base/FlashloanSimpleReceiverBase.sol"; +import { VTokenInterface } from "../../VTokenInterfaces.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title MockFlashLoanSimpleReceiver +/// @notice This contract serves as a mock implementation for a flash loan receiver, utilizing the +/// FlashLoanSimpleReceiverBase as a foundation. It provides the ability to request a flash loan and +/// defines how the loan is repaid by implementing custom logic in the `executeOperation` function. +contract MockFlashLoanSimpleReceiver is FlashLoanSimpleReceiverBase { + /** + * @notice Constructor that initializes the flashLoan receiver with a reference to the VToken contract. + * @param vToken The address of the VTokenInterface contract that supports flashLoan functionality. + */ + constructor(VTokenInterface vToken) FlashLoanSimpleReceiverBase(vToken) {} + + /** + * @notice Requests a flash loan from the VToken contract. + * @param amount_ The amount of tokens to borrow through the flash loan. + * @dev This function calls the `executeFlashLoan` function of the VToken contract. + */ + function requestFlashLoan(uint256 amount_) external { + address receiver = address(this); // Receiver address is this contract itself + uint256 amount = amount_; // Set the requested amount + + // Request the flashLoan from the VToken contract + VTOKEN.executeFlashLoan(receiver, amount); + } + + /** + * @notice This function is invoked after receiving the flash loan to handle the loan execution. + * @dev Custom logic for the use of the borrowed amount is implemented in this function. + * It is important that the total borrowed amount plus the premium is repaid. + * @param asset The address of the token being borrowed in the flash loan. + * @param amount The amount of tokens borrowed. + * @param premium The fee for the flash loan, typically a small percentage of the borrowed amount. + * @param initiator The address that initiated the flash loan. + * @param param Additional parameters passed along with the flash loan (can be empty). + * @return Returns true if the operation is successful. + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata param + ) external returns (bool) { + // 👇 Your custom logic for the flash loan should be implemented here 👇 + /** YOUR CUSTOM LOGIC HERE */ + // 👆 Your custom logic for the flash loan should be implemented above here 👆 + + // Calculate the total repayment amount (loan amount + premium) + uint256 total = amount + premium; + + // Transfer the total amount (principal + premium) back to the VToken contract to repay the loan + IERC20(asset).transfer(address(VTOKEN), total); + + // Return true to indicate successful execution of the flash loan operation + return true; + } +} diff --git a/contracts/test/UpgradedVToken.sol b/contracts/test/UpgradedVToken.sol index efae5ac4f..fe67e7cb0 100644 --- a/contracts/test/UpgradedVToken.sol +++ b/contracts/test/UpgradedVToken.sol @@ -57,7 +57,9 @@ contract UpgradedVToken is VToken { address payable admin_, address accessControlManager_, RiskManagementInit memory riskManagement, - uint256 reserveFactorMantissa_ + uint256 reserveFactorMantissa_, + bool isFlashLoanEnabled_, + uint256 flashLoanFeeMantissa_ ) public reinitializer(2) { super._initialize( underlying_, @@ -70,7 +72,9 @@ contract UpgradedVToken is VToken { admin_, accessControlManager_, riskManagement, - reserveFactorMantissa_ + reserveFactorMantissa_, + isFlashLoanEnabled_, + flashLoanFeeMantissa_ ); } diff --git a/deploy/009-deploy-vtokens.ts b/deploy/009-deploy-vtokens.ts index 108f303fb..d6b6bb4b8 100644 --- a/deploy/009-deploy-vtokens.ts +++ b/deploy/009-deploy-vtokens.ts @@ -69,6 +69,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { multiplierPerYear2, baseRatePerYear2, kink2_, + isFlashLoanAllowed, + flashLoanFeeMantissa, } = vtoken; const token = getTokenConfig(asset, tokensConfig); @@ -180,6 +182,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { accessControlManagerAddress, [AddressOne, protocolShareReserveAddress], reserveFactor, + isFlashLoanAllowed, + flashLoanFeeMantissa, ]; await deploy(`VToken_${symbol}`, { from: deployer, diff --git a/hardhat.config.ts b/hardhat.config.ts index be7835ca3..0c7448c24 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -217,6 +217,7 @@ const config: HardhatUserConfig = { yul: !process.env.CI, }, }, + viaIR: true, evmVersion: "paris", outputSelection: { "*": { diff --git a/helpers/deploymentConfig.ts b/helpers/deploymentConfig.ts index 4b663f2f2..791c6208c 100644 --- a/helpers/deploymentConfig.ts +++ b/helpers/deploymentConfig.ts @@ -113,6 +113,8 @@ export type VTokenConfig = { borrowCap: string; vTokenReceiver: string; reduceReservesBlockDelta: string; + isFlashLoanAllowed: boolean; + flashLoanFeeMantissa: string; }; export type AccessControlEntry = { @@ -499,6 +501,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(478980, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus BTCB", @@ -517,6 +521,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1000, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -558,6 +564,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3000000, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ankrBNB", @@ -576,6 +584,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(100, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus MBOX", @@ -594,6 +604,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3184294, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus NFT", @@ -612,6 +624,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(24654278679, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus RACA", @@ -630,6 +644,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3805812642, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus stkBNB", @@ -648,6 +664,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(324, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD", @@ -666,6 +684,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1698253, 18), vTokenReceiver: "account:deployer", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -974,6 +994,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(200_000, 18), vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Stable Coins)", @@ -992,6 +1014,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(400_000, 6), // USDT has 6 decimals on testnet vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Stable Coins)", @@ -1010,6 +1034,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(400_000, 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus agEUR (Stablecoins)", @@ -1028,6 +1054,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(50000, 18), vTokenReceiver: "0xc444949e0054a23c44fc45789738bdf64aed2391", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1075,6 +1103,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("10500000", 18), vTokenReceiver: "0x109E8083a64c7DedE513e8b580c5b08B96f9cE73", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ALPACA (DeFi)", @@ -1093,6 +1123,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1750000", 18), vTokenReceiver: "0xAD9CADe20100B8b945da48e1bCbd805C38d8bE77", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (DeFi)", @@ -1111,6 +1143,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 6), // USDT has 6 decimals on testnet vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (DeFi)", @@ -1129,6 +1163,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ANKR (DeFi)", @@ -1147,6 +1183,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("6656161", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ankrBNB (DeFi)", @@ -1165,6 +1203,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("4000", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus PLANET (DeFi)", @@ -1183,6 +1223,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("500000000", 18), vTokenReceiver: "0x0554d6079eBc222AD12405E52b264Bdb5B65D1cf", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TWT (DeFi)", @@ -1201,6 +1243,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("500000", 18), vTokenReceiver: "0x0848dB7cB495E7b9aDA1D4dC972b9A526D014D84", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1248,6 +1292,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2800000000", 18), vTokenReceiver: "0x6Ee74536B3Ff10Ff639aa781B7220121287F6Fa5", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus FLOKI (GameFi)", @@ -1266,6 +1312,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("28000000000", 18), // FLOKI has 18 decimals on testnet vTokenReceiver: "0x17e98a24f992BB7bcd62d6722d714A3C74814B94", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (GameFi)", @@ -1284,6 +1332,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 6), // USDT has 6 decimals on testnet vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (GameFi)", @@ -1302,6 +1352,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1343,6 +1395,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("5600", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus BNBx (Liquid Staked BNB)", @@ -1361,6 +1415,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1272", 18), vTokenReceiver: "0xF0348E1748FCD45020151C097D234DbbD5730BE7", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus stkBNB (Liquid Staked BNB)", @@ -1379,6 +1435,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("378", 18), vTokenReceiver: "0xccc022502d6c65e1166fd34147040f05880f7972", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBNB (Liquid Staked BNB)", @@ -1397,6 +1455,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("56000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Liquid Staked BNB)", @@ -1415,6 +1475,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 6), // USDT has 6 decimals on testnet vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Liquid Staked BNB)", @@ -1433,6 +1495,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus SnBNB (Liquid Staked BNB)", @@ -1451,6 +1515,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("100", 18), vTokenReceiver: "0xDC2D855A95Ee70d7282BebD35c96f905CDE31f55", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1510,6 +1576,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1050000000000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus NFT (Tron)", @@ -1528,6 +1596,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2800000000", 18), // NFT has 18 decimals on testnet vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WIN (Tron)", @@ -1546,6 +1616,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2100000000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TRX (Tron)", @@ -1564,6 +1636,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("7700000", 6), // Note 6 decimals vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Tron)", @@ -1582,6 +1656,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 6), // USDT has 6 decimals on testnet vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Tron)", @@ -1600,6 +1676,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1653,6 +1731,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("800000000000000", 9), vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Meme)", @@ -1671,6 +1751,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("900000", 6), vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -1706,6 +1788,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("5", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -1724,6 +1808,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("200", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ETH (Liquid Staked ETH)", @@ -1742,6 +1828,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("400", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bsctestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -2039,6 +2127,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(200_000, 18), vTokenReceiver: "0x09702Ea135d9D707DD51f530864f2B9220aAD87B", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Stablecoins)", @@ -2057,6 +2147,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(400_000, 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Stablecoins)", @@ -2075,6 +2167,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(400_000, 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus agEUR (Stablecoins)", @@ -2093,6 +2187,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(50000, 18), vTokenReceiver: "0xc444949e0054a23c44fc45789738bdf64aed2391", // community wallet reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2140,6 +2236,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("10500000", 18), vTokenReceiver: "0x109E8083a64c7DedE513e8b580c5b08B96f9cE73", // biswap team treasury reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ALPACA (DeFi)", @@ -2158,6 +2256,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1750000", 18), vTokenReceiver: "0xAD9CADe20100B8b945da48e1bCbd805C38d8bE77", // alpaca team treasury reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (DeFi)", @@ -2176,6 +2276,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (DeFi)", @@ -2194,6 +2296,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", // tron ecosystem treasury reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ANKR (DeFi)", @@ -2212,6 +2316,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("6656161", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", // ankr team treasury reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { @@ -2231,6 +2337,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("4000", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", // ankr team treasury reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus PLANET (DeFi)", @@ -2249,6 +2357,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("500000000", 18), vTokenReceiver: "0x0554d6079eBc222AD12405E52b264Bdb5B65D1cf", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TWT (DeFi)", @@ -2267,6 +2377,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("500000", 18), vTokenReceiver: "0x0848dB7cB495E7b9aDA1D4dC972b9A526D014D84", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2315,6 +2427,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2800000000", 18), vTokenReceiver: "0x6Ee74536B3Ff10Ff639aa781B7220121287F6Fa5", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus FLOKI (GameFi)", @@ -2333,6 +2447,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("28000000000", 9), // Note 9 decimals vTokenReceiver: "0x17e98a24f992BB7bcd62d6722d714A3C74814B94", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (GameFi)", @@ -2351,6 +2467,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (GameFi)", @@ -2369,6 +2487,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2410,6 +2530,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("5600", 18), vTokenReceiver: "0xAE1c38847Fb90A13a2a1D7E5552cCD80c62C6508", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus BNBx (Liquid Staked BNB)", @@ -2428,6 +2550,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1272", 18), vTokenReceiver: "0xF0348E1748FCD45020151C097D234DbbD5730BE7", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus stkBNB (Liquid Staked BNB)", @@ -2446,6 +2570,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("378", 18), vTokenReceiver: "0xccc022502d6c65e1166fd34147040f05880f7972", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBNB (Liquid Staked BNB)", @@ -2464,6 +2590,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("56000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Liquid Staked BNB)", @@ -2482,6 +2610,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Liquid Staked BNB)", @@ -2500,6 +2630,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus SnBNB (Liquid Staked BNB)", @@ -2518,6 +2650,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("100", 18), vTokenReceiver: "0xDC2D855A95Ee70d7282BebD35c96f905CDE31f55", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2577,6 +2711,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1050000000000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus NFT (Tron)", @@ -2595,6 +2731,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2800000000", 6), // Note 6 decimals vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WIN (Tron)", @@ -2613,6 +2751,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2100000000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TRX (Tron)", @@ -2631,6 +2771,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("7700000", 6), // Note 6 decimals vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Tron)", @@ -2649,6 +2791,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("14880000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDD (Tron)", @@ -2667,6 +2811,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1600000", 18), vTokenReceiver: "0x3DdfA8eC3052539b6C9549F12cEA2C295cfF5296", reduceReservesBlockDelta: "28800", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2720,6 +2866,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("800000000000000", 9), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Meme)", @@ -2738,6 +2886,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("900000", 18), vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, reduceReservesBlockDelta: "100", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -2773,6 +2923,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("5", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -2791,6 +2943,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("200", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ETH (Liquid Staked ETH)", @@ -2809,6 +2963,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("400", 18), reduceReservesBlockDelta: "28800", vTokenReceiver: preconfiguredAddresses.bscmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -3085,6 +3241,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(250, 8), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -3103,6 +3261,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4600, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -3121,6 +3281,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(9_000_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -3139,6 +3301,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(9_000_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus crvUSD (Core)", @@ -3157,6 +3321,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(9_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus CRV (Core)", @@ -3175,6 +3341,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_500_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus DAI (Core)", @@ -3193,6 +3361,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(45_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TUSD (Core)", @@ -3211,6 +3381,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_800_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, // TBD + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus Frax (Core)", @@ -3229,6 +3401,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(8_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, // TBD + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus Staked FRAX (Core)", @@ -3247,6 +3421,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, // TBD + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus eBTC", @@ -3265,6 +3441,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("12.5", 8), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus EIGEN", @@ -3283,6 +3461,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_500_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus LBTC (Core)", @@ -3373,6 +3553,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4_500_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Stablecoins)", @@ -3391,6 +3573,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4_500_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus crvUSD (Stablecoins)", @@ -3409,6 +3593,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4_500_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -3437,6 +3623,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus CRV (Curve)", @@ -3455,6 +3643,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_500_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -3501,6 +3691,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Liquid Staked ETH)", @@ -3519,6 +3711,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(18_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -3537,6 +3731,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(750, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus PT-wETH-26DEC2024 (Liquid Staked ETH)", @@ -3555,6 +3751,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(375, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus rsETH (Liquid Staked ETH)", @@ -3573,6 +3771,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3_600, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus sfrxETH (Liquid Staked ETH)", @@ -3591,6 +3791,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ezETH (Liquid Staked ETH)", @@ -3609,6 +3811,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_400, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETHs (Liquid Staked ETH)", @@ -3627,6 +3831,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(0, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus pufETH (Liquid Staked ETH)", @@ -3645,6 +3851,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("300", 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -3981,6 +4189,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(850, 8), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -3999,6 +4209,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(18_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -4020,6 +4232,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(45_000_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -4041,6 +4255,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(45_000_000, 6), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus crvUSD (Core)", @@ -4059,6 +4275,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(45_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x7a16fF8270133F063aAb6C9977183D9e72835428", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus DAI (Core)", @@ -4077,6 +4295,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(45_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus TUSD (Core)", @@ -4095,6 +4315,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_800_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus Frax (Core)", @@ -4113,6 +4335,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(8_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x6e74053a3798e0fC9a9775F7995316b27f21c4D2", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus Staked FRAX (Core)", @@ -4131,6 +4355,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x6e74053a3798e0fC9a9775F7995316b27f21c4D2", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus eBTC", @@ -4149,6 +4375,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("12.5", 8), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus EIGEN", @@ -4167,6 +4395,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_500_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus LBTC (Core)", @@ -4275,6 +4505,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x7a16fF8270133F063aAb6C9977183D9e72835428", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus CRV (Curve)", @@ -4293,6 +4525,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3_000_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x7a16fF8270133F063aAb6C9977183D9e72835428", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -4345,6 +4579,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Liquid Staked ETH)", @@ -4363,6 +4599,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(18_000, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -4381,6 +4619,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(750, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: `0xF6C612c745Ba4546075DB62902c1Eb3255CdAe28`, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus PT-wETH-26DEC2024 (Liquid Staked ETH)", @@ -4399,6 +4639,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(0, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus rsETH (Liquid Staked ETH)", @@ -4417,6 +4659,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3_600, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: "0x7AAd74b7f0d60D5867B59dbD377a71783425af47", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus sfrxETH (Liquid Staked ETH)", @@ -4435,6 +4679,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ETHEREUM, vTokenReceiver: "0x6e74053a3798e0fC9a9775F7995316b27f21c4D2", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ezETH (Liquid Staked ETH)", @@ -4453,6 +4699,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_400, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ETHEREUM, vTokenReceiver: "0x1E3233E8d972cfFc7D0f83aFAE4354a0Db74e34E", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETHs (Liquid Staked ETH)", @@ -4471,6 +4719,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(0, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ETHEREUM, vTokenReceiver: "0x86fBaEB3D6b5247F420590D303a6ffC9cd523790", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus pufETH (Liquid Staked ETH)", @@ -4489,6 +4739,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("300", 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.ethereum.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -4670,6 +4922,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(250, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBTESTNET, vTokenReceiver: preconfiguredAddresses.opbnbtestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ETH (Core)", @@ -4688,6 +4942,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4600, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBTESTNET, vTokenReceiver: preconfiguredAddresses.opbnbtestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -4706,6 +4962,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(9_000_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBTESTNET, vTokenReceiver: preconfiguredAddresses.opbnbtestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBNB (Core)", @@ -4724,6 +4982,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(56_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBTESTNET, vTokenReceiver: preconfiguredAddresses.opbnbtestnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -4805,6 +5065,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("0.55", 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBMAINNET, vTokenReceiver: preconfiguredAddresses.opbnbmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ETH (Core)", @@ -4823,6 +5085,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(16, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBMAINNET, vTokenReceiver: preconfiguredAddresses.opbnbmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -4841,6 +5105,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBMAINNET, vTokenReceiver: preconfiguredAddresses.opbnbmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBNB (Core)", @@ -4859,6 +5125,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(75, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBMAINNET, vTokenReceiver: preconfiguredAddresses.opbnbmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus FDUSD (Core)", @@ -4877,6 +5145,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OPBNBMAINNET, vTokenReceiver: preconfiguredAddresses.opbnbmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -4972,6 +5242,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("0.55", 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -4990,6 +5262,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(16, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -5008,6 +5282,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -5026,6 +5302,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ARB (Core)", @@ -5044,6 +5322,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(16, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -5079,6 +5359,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(800, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -5097,6 +5379,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_300, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_SEPOLIA, vTokenReceiver: preconfiguredAddresses.arbitrumsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Liquid Staked ETH)", @@ -5115,6 +5399,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(12_500, 18), reduceReservesBlockDelta: DEFAULT_REDUCE_RESERVES_BLOCK_DELTA, vTokenReceiver: preconfiguredAddresses.sepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], @@ -5210,6 +5496,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("500", 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -5228,6 +5516,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("23500", 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -5249,6 +5539,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("49000000", 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -5270,6 +5562,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("18000000", 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus ARB (Core)", @@ -5288,6 +5582,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("9000000", 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -5334,6 +5630,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(800, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: "0x5A9d695c518e95CD6Ea101f2f25fC2AE18486A61", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus weETH (Liquid Staked ETH)", @@ -5352,6 +5650,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_300, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: "0x46cba1e9b1e5db32da28428f2fb85587bcb785e7", + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Liquid Staked ETH)", @@ -5370,6 +5670,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(12_500, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ARBITRUM_ONE, vTokenReceiver: preconfiguredAddresses.arbitrumone.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -5472,6 +5774,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(23_500, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBTC (Core)", @@ -5490,6 +5794,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(500, 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -5508,6 +5814,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(18_000_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC.e (Core)", @@ -5526,6 +5834,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(49_000_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { @@ -5545,6 +5855,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(2_350_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -5563,6 +5875,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_SEPOLIA, vTokenReceiver: preconfiguredAddresses.zksyncsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -5669,6 +5983,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_700, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WBTC (Core)", @@ -5687,6 +6003,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(20, 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -5708,6 +6026,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(3_300_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC.e (Core)", @@ -5729,6 +6049,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(4_200_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { @@ -5748,6 +6070,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(12_500_000, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -5769,6 +6093,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(1_000_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_ZKSYNC_MAINNET, vTokenReceiver: preconfiguredAddresses.zksyncmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -5882,6 +6208,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("0.55", 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_SEPOLIA, vTokenReceiver: preconfiguredAddresses.opsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -5900,6 +6228,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(16, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_SEPOLIA, vTokenReceiver: preconfiguredAddresses.opsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -5918,6 +6248,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_SEPOLIA, vTokenReceiver: preconfiguredAddresses.opsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -5936,6 +6268,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(130_000, 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_SEPOLIA, vTokenReceiver: preconfiguredAddresses.opsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus OP (Core)", @@ -5954,6 +6288,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit(16, 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_SEPOLIA, vTokenReceiver: preconfiguredAddresses.opsepolia.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [ @@ -6054,6 +6390,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("50", 8), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_MAINNET, vTokenReceiver: preconfiguredAddresses.opmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus WETH (Core)", @@ -6072,6 +6410,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("2700", 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_MAINNET, vTokenReceiver: preconfiguredAddresses.opmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDT (Core)", @@ -6090,6 +6430,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("3600000", 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_MAINNET, vTokenReceiver: preconfiguredAddresses.opmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus OP (Core)", @@ -6108,6 +6450,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("1500000", 18), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_MAINNET, vTokenReceiver: preconfiguredAddresses.opmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, { name: "Venus USDC (Core)", @@ -6126,6 +6470,8 @@ export const globalConfig: NetworkConfig = { borrowCap: convertToUnit("9000000", 6), reduceReservesBlockDelta: REDUCE_RESERVES_BLOCK_DELTA_OP_MAINNET, vTokenReceiver: preconfiguredAddresses.opmainnet.VTreasury, + isFlashLoanAllowed: false, + flashLoanFeeMantissa: "0", }, ], rewards: [], diff --git a/tests/hardhat/Comptroller/Flashloan.ts b/tests/hardhat/Comptroller/Flashloan.ts new file mode 100644 index 000000000..2cc809494 --- /dev/null +++ b/tests/hardhat/Comptroller/Flashloan.ts @@ -0,0 +1,197 @@ +import { FakeContract, MockContract, smock } from "@defi-wonderland/smock"; +import { loadFixture, setBalance } from "@nomicfoundation/hardhat-network-helpers"; +import chai from "chai"; +import { BigNumber } from "ethers"; +import { parseEther, parseUnits } from "ethers/lib/utils"; +import { ethers, upgrades } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { convertToUnit } from "../../../helpers/utils"; +import { + AccessControlManager, + Comptroller, + ERC20Harness, + InterestRateModel, + MockFlashLoanReceiver, + MockFlashLoanReceiver__factory, + PoolRegistry, + ProtocolShareReserve, + VTokenHarness, + VTokenHarness__factory, +} from "../../../typechain"; +import { makeVToken, mockUnderlying } from "../util/TokenTestHelpers"; + +const { expect } = chai; +chai.use(smock.matchers); + +const MAX_LOOP_LIMIT = 150; + +const flashLoanAmount1 = parseUnits("20", 18); +const flashLoanAmount2 = parseUnits("30", 18); +const feeMantissaTokenA = parseUnits("0.01", 18); +const feeMantissaTokenB = parseUnits("0.02", 18); + +// Declare the types here +type FlashLoanContractsFixture = { + accessControlManager: FakeContract; + protocolShareReserve: FakeContract; + interestRateModel: FakeContract; + poolRegistry: FakeContract; + comptroller: Comptroller; + underlyingA: MockContract; + underlyingB: MockContract; + VTokenA: VTokenHarness; + VTokenB: VTokenHarness; +}; + +// Create a fixture will deploy all the required contracts for flashLoan +const flashLoanTestFixture = async (): Promise => { + const [admin] = await ethers.getSigners(); + const accessControlManager = await smock.fake("AccessControlManager"); + accessControlManager.isAllowedToCall.returns(true); + const protocolShareReserve = await smock.fake("ProtocolShareReserve"); + const interestRateModel = await smock.fake("InterestRateModel"); + interestRateModel.isInterestRateModel.returns(true); + + const poolRegistry = await smock.fake("PoolRegistry"); + + const underlyingA = await mockUnderlying("Mock AAVE", "AAVE"); + const underlyingB = await mockUnderlying("Mock UNI", "UNI"); + + const Comptroller = await ethers.getContractFactory("Comptroller"); + const comptrollerBeacon = await upgrades.deployBeacon(Comptroller, { constructorArgs: [poolRegistry.address] }); + + const comptroller = await upgrades.deployBeaconProxy(comptrollerBeacon, Comptroller, [ + MAX_LOOP_LIMIT, + accessControlManager.address, + ]); + + const VTokenA = await makeVToken( + { + underlying: underlyingA, + comptroller, + accessControlManager, + admin, + interestRateModel, + protocolShareReserve, + isFlashLoanAllowed: true, + flashLoanFeeMantissa: feeMantissaTokenA, + }, + { kind: "VTokenHarness" }, + ); + + const VTokenB = await makeVToken( + { + underlying: underlyingB, + comptroller, + accessControlManager, + admin, + interestRateModel, + protocolShareReserve, + isFlashLoanAllowed: true, + flashLoanFeeMantissa: feeMantissaTokenB, + }, + { kind: "VTokenHarness" }, + ); + + await comptroller.setMarketSupplyCaps( + [VTokenA.address, VTokenB.address], + [convertToUnit(1, 50), convertToUnit(1, 50)], + ); + + await setBalance(poolRegistry.address, parseEther("1")); + await comptroller.connect(poolRegistry.wallet).supportMarket(VTokenA.address); + await comptroller.connect(poolRegistry.wallet).supportMarket(VTokenB.address); + + return { + accessControlManager, + protocolShareReserve, + interestRateModel, + poolRegistry, + comptroller, + underlyingA, + underlyingB, + VTokenA, + VTokenB, + }; +}; + +describe("FlashLoan", async () => { + let alice: SignerWithAddress; + let acmUser: SignerWithAddress; + let contracts: FlashLoanContractsFixture; + let VTokenA: VTokenHarness; + let VTokenB: VTokenHarness; + let underlyingA: MockContract; + let underlyingB: MockContract; + let comptroller: Comptroller; + let mockReceiverContract: MockFlashLoanReceiver; + + beforeEach(async () => { + [alice, acmUser] = await ethers.getSigners(); + contracts = await loadFixture(flashLoanTestFixture); + ({ comptroller, VTokenA, VTokenB, underlyingA, underlyingB } = contracts); + }); + + describe("FlashLoan Multi-Assets", async () => { + beforeEach(async () => { + const MockFlashLoanReceiver = await ethers.getContractFactory( + "MockFlashLoanReceiver", + ); + mockReceiverContract = await MockFlashLoanReceiver.deploy(comptroller.address); + await mockReceiverContract.deployed(); + }); + + it("Should revert if flashLoan is not enabled", async () => { + await VTokenA.connect(acmUser).toggleFlashLoan(); + expect(await VTokenA.isFlashLoanEnabled()).to.be.false; + + await expect( + mockReceiverContract.requestFlashLoan( + [VTokenA.address.toString(), VTokenB.address.toString()], + [flashLoanAmount1, flashLoanAmount2], + ), + ).to.be.revertedWithCustomError(comptroller, "FlashLoanNotEnabled"); + }); + + it("FlashLoan for multiple underlying", async () => { + // Admin Enable flashLoan for multiple vToken + expect(await VTokenA.isFlashLoanEnabled()).to.be.true; + expect(await VTokenB.isFlashLoanEnabled()).to.be.true; + + // Set the balance of mockReceiver in order to pay for flashLoan fee + await underlyingA.harnessSetBalance(mockReceiverContract.address, parseUnits("10", 18)); + await underlyingB.harnessSetBalance(mockReceiverContract.address, parseUnits("20", 18)); + + await underlyingA.harnessSetBalance(VTokenA.address, parseUnits("50", 18)); + await underlyingB.harnessSetBalance(VTokenB.address, parseUnits("50", 18)); + + // Get the balance before the flashLoan + const beforeBalanceVTokenA = await underlyingA.balanceOf(VTokenA.address); + const beforeBalanceVTokenB = await underlyingB.balanceOf(VTokenB.address); + + // Execute the flashLoan from the mockReceiverContract + const flashLoan = await mockReceiverContract + .connect(alice) + .requestFlashLoan([VTokenA.address, VTokenB.address], [flashLoanAmount1, flashLoanAmount2]); + + // Get the balance after the flashLoan + const afterBalanceVTokenA = await underlyingA.balanceOf(VTokenA.address); + const afterBalanceVTokenB = await underlyingB.balanceOf(VTokenB.address); + + const feeOnFlashLoanTokenA = BigNumber.from(flashLoanAmount1).mul(feeMantissaTokenA).div(parseUnits("1", 18)); + const feeOnFlashLoanTokenB = BigNumber.from(flashLoanAmount2).mul(feeMantissaTokenB).div(parseUnits("1", 18)); + + expect(afterBalanceVTokenA).to.be.equal(beforeBalanceVTokenA.add(feeOnFlashLoanTokenA)); + expect(afterBalanceVTokenB).to.be.equal(beforeBalanceVTokenB.add(feeOnFlashLoanTokenB)); + + await expect(flashLoan) + .to.emit(comptroller, "FlashLoanExecuted") + .withArgs( + mockReceiverContract.address, + [VTokenA.address, VTokenB.address], + [flashLoanAmount1, flashLoanAmount2], + ); + }); + }); +}); diff --git a/tests/hardhat/Comptroller/liquidateAccountTest.ts b/tests/hardhat/Comptroller/liquidateAccountTest.ts index 75fbd4f06..6bac7ea07 100644 --- a/tests/hardhat/Comptroller/liquidateAccountTest.ts +++ b/tests/hardhat/Comptroller/liquidateAccountTest.ts @@ -338,8 +338,9 @@ describe("liquidateAccount", () => { beforeLiquidation: { supply: 0, borrows: parseUnits("1", 18) }, afterLiquidation: { supply: 0, borrows: 0 }, }); - await expect(comptroller.connect(liquidator).liquidateAccount(user.address, [])).to.be.revertedWith( - "Nonzero borrow balance after liquidation", + await expect(comptroller.connect(liquidator).liquidateAccount(user.address, [])).to.be.revertedWithCustomError( + comptroller, + "NonzeroBorrowBalance", ); }); }); diff --git a/tests/hardhat/Comptroller/pauseTest.ts b/tests/hardhat/Comptroller/pauseTest.ts index f15875109..79879ad31 100644 --- a/tests/hardhat/Comptroller/pauseTest.ts +++ b/tests/hardhat/Comptroller/pauseTest.ts @@ -101,8 +101,9 @@ describe("Comptroller", () => { }); it("reverts if the market is not listed", async () => { - await expect(comptroller.setActionsPaused([SKT.address], [1], true)).to.be.revertedWith( - "cannot pause a market that is not listed", + await expect(comptroller.setActionsPaused([SKT.address], [1], true)).to.be.revertedWithCustomError( + comptroller, + "MarketNotExist", ); }); diff --git a/tests/hardhat/Comptroller/setters.ts b/tests/hardhat/Comptroller/setters.ts index e50da4007..af440a37e 100644 --- a/tests/hardhat/Comptroller/setters.ts +++ b/tests/hardhat/Comptroller/setters.ts @@ -105,8 +105,9 @@ describe("setters", async () => { it("reverts if re-adding same rewardDistributor", async () => { await comptroller.addRewardsDistributor(newRewardsDistributor.address); - await expect(comptroller.addRewardsDistributor(newRewardsDistributor.address)).to.be.revertedWith( - "already exists", + await expect(comptroller.addRewardsDistributor(newRewardsDistributor.address)).to.be.revertedWithCustomError( + comptroller, + "RewardsDistributorAlreadyExists", ); }); }); @@ -147,17 +148,24 @@ describe("setters", async () => { describe("SupplyAndBorrowCaps", async () => { it("reverts if token data is invalid", async () => { - await expect(comptroller.setMarketSupplyCaps([], [1, 2])).to.be.revertedWith("invalid number of markets"); + await expect(comptroller.setMarketSupplyCaps([], [1, 2])).to.be.revertedWithCustomError( + comptroller, + "InvalidInput", + ); }); it("reverts if supply and token data is invalid", async () => { - await expect(comptroller.setMarketSupplyCaps([OMG.address], [1, 2])).to.be.revertedWith( - "invalid number of markets", + await expect(comptroller.setMarketSupplyCaps([OMG.address], [1, 2])).to.be.revertedWithCustomError( + comptroller, + "InvalidInput", ); }); it("reverts if borrow and token data is invalid", async () => { - await expect(comptroller.setMarketBorrowCaps([OMG.address], [1, 2])).to.be.revertedWith("invalid input"); + await expect(comptroller.setMarketBorrowCaps([OMG.address], [1, 2])).to.be.revertedWithCustomError( + comptroller, + "InvalidInput", + ); }); }); diff --git a/tests/hardhat/Fork/constants.ts b/tests/hardhat/Fork/constants.ts index 1c88e6133..0fd6d6789 100644 --- a/tests/hardhat/Fork/constants.ts +++ b/tests/hardhat/Fork/constants.ts @@ -181,7 +181,7 @@ export const contractAddresses = { ACC1: "0xc7f050b6F465b876c764A866d6337EabBab08Cd4", ACC2: "0xce0180B3B992649CBc3C8e1cF95b4A52Be9bA3AF", ACC3: "0x13E0a421c17Ff1e7FFccFa05714957cF530b3aa4", - BLOCK_NUMBER: 58181663, + BLOCK_NUMBER: 111100817, }, arbitrumone: { ADMIN: "0x14e0E151b33f9802b3e75b621c1457afc44DcAA0", diff --git a/tests/hardhat/Fork/flashloan.ts b/tests/hardhat/Fork/flashloan.ts new file mode 100644 index 000000000..e4de2575d --- /dev/null +++ b/tests/hardhat/Fork/flashloan.ts @@ -0,0 +1,203 @@ +import { mine } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { expect } from "chai"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers, network } from "hardhat"; + +import { getMaxBorrowRateMantissa } from "../../../helpers/deploymentConfig"; +import { getBlockOrTimestampBasedDeploymentInfo } from "../../../helpers/deploymentUtils"; +import { + AccessControlManager, + AccessControlManager__factory, + Comptroller, + Comptroller__factory, + ERC20, + ERC20__factory, + MockFlashLoanReceiver, + MockFlashLoanReceiver__factory, + UpgradeableBeacon__factory, + VToken, + VToken__factory, +} from "../../../typechain"; +import { getContractAddresses, initMainnetUser, setForkBlock } from "./utils"; + +const FORK = process.env.FORK === "true"; +const FORKED_NETWORK = process.env.FORKED_NETWORK || "bscmainnet"; +const { ACC2, TOKEN1, TOKEN2, VTOKEN2, ACM, VTOKEN1, POOL_REGISTRY, COMPTROLLER, TOKEN1_HOLDER, BLOCK_NUMBER, ADMIN } = + getContractAddresses(FORKED_NETWORK as string); + +const blocksToMine: number = 30000; + +const AddressZero = "0x0000000000000000000000000000000000000000"; +const WETH_HOLDER = "0xd7512902999b34af2B2940Eb8827CC8345DC77C6"; +const COMPTROLLER_BEACON = "0x12Dcb8D9F1eE7Ad7410F5B36B07bcC7891ab4cEf"; +const VTOKEN_BEACON = "0x74ae9919F5866cE148c81331a5FCdE71b81c4056"; +const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(network.name); +const MAX_BORROW_RATE_MANTISSA = getMaxBorrowRateMantissa(network.name); + +const ARBFlashLoanFeeMantissa = parseUnits("0.01", 18); +const WETHFlashLoanFeeMantissa = parseUnits("0.03", 18); +const ARBFlashLoanAmount = parseUnits("50", 18); +const WETHFlashLoanAmount = parseUnits("20", 18); + +// Giving permission to Timelock to call chainlink setDirectPrice and setTokenConfig +async function grantPermissions() { + accessControlManager = AccessControlManager__factory.connect(ACM, impersonatedTimelock); + let tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vARB.address, "toggleFlashLoan()", ADMIN); + await tx.wait(); + + tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vARB.address, "setFlashLoanFeeMantissa(uint256)", ADMIN); + await tx.wait(); + + tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vWETH.address, "toggleFlashLoan()", ADMIN); + await tx.wait(); + + tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vWETH.address, "setFlashLoanFeeMantissa(uint256)", ADMIN); + await tx.wait(); +} + +let impersonatedTimelock: SignerWithAddress; +let john: SignerWithAddress; +let comptroller: Comptroller; +let arbHolder: SignerWithAddress; +let wETHHolder: SignerWithAddress; +let accessControlManager: AccessControlManager; +let ARB: ERC20; +let WETH: ERC20; +let vARB: VToken; +let vWETH: VToken; +let mockFlashLoanReceiver: MockFlashLoanReceiver; + +async function setup() { + await setForkBlock(BLOCK_NUMBER); + + impersonatedTimelock = await initMainnetUser(ADMIN, parseUnits("2")); + john = await initMainnetUser(ACC2, parseUnits("2")); + arbHolder = await initMainnetUser(TOKEN1_HOLDER, parseUnits("2")); + wETHHolder = await initMainnetUser(WETH_HOLDER, parseUnits("2")); + ARB = ERC20__factory.connect(TOKEN2, impersonatedTimelock); + WETH = ERC20__factory.connect(TOKEN1, impersonatedTimelock); + vARB = VToken__factory.connect(VTOKEN2, impersonatedTimelock); + vWETH = VToken__factory.connect(VTOKEN1, impersonatedTimelock); + comptroller = Comptroller__factory.connect(COMPTROLLER, impersonatedTimelock); + + // Deploy the comptroller and VToken + const Comptroller = await ethers.getContractFactory("Comptroller", impersonatedTimelock); + const upgradeComptrollerImpl = await Comptroller.deploy(POOL_REGISTRY); + await upgradeComptrollerImpl.deployed(); + + const VToken = await ethers.getContractFactory("VToken", impersonatedTimelock); + const upgradeVTokenImpl = await VToken.deploy(isTimeBased, blocksPerYear, MAX_BORROW_RATE_MANTISSA); + await upgradeVTokenImpl.deployed(); + + // Upgrade the comptroller implementation + const comptrollerBeacon = UpgradeableBeacon__factory.connect(COMPTROLLER_BEACON, impersonatedTimelock); + const vTokenBeacon = UpgradeableBeacon__factory.connect(VTOKEN_BEACON, impersonatedTimelock); + + await comptrollerBeacon.connect(impersonatedTimelock).upgradeTo(upgradeComptrollerImpl.address); + await vTokenBeacon.connect(impersonatedTimelock).upgradeTo(upgradeVTokenImpl.address); + + expect(await comptrollerBeacon.implementation()).to.be.equal(upgradeComptrollerImpl.address); + expect(await vTokenBeacon.implementation()).to.be.equal(upgradeVTokenImpl.address); + + await grantPermissions(); +} + +if (FORK) { + describe("FlashLoan Fork Test", async () => { + beforeEach(async () => { + // Run the setup function before each test to initialize the environment + await setup(); + + // Deploy a mock flashLoan receiver contract to simulate flashLoan interactions in tests + const MockFlashLoanReceiver = await ethers.getContractFactory( + "MockFlashLoanReceiver", + ); + mockFlashLoanReceiver = await MockFlashLoanReceiver.deploy(comptroller.address); + }); + + it("Should revert if flashLoan not enabled", async () => { + // Attempt to execute a flashLoan when the flashLoan feature is disabled, which should revert + await expect( + comptroller + .connect(john) + .executeFlashLoan( + mockFlashLoanReceiver.address, + [vARB.address, vWETH.address], + [ARBFlashLoanAmount, WETHFlashLoanAmount], + ), + ).to.be.revertedWithCustomError(vARB, "FlashLoanNotEnabled"); + }); + + it("Should revert if asset and amount arrays are mismatched", async () => { + // Attempt to execute a flashLoan with mismatched arrays for assets and amounts, which should revert + await expect( + comptroller.connect(john).executeFlashLoan( + mockFlashLoanReceiver.address, + [vARB.address], // Only one asset provided + [ARBFlashLoanAmount, WETHFlashLoanAmount], // Two loan amounts provided + ), + ).to.be.revertedWithCustomError(comptroller, "InvalidFlashLoanParams"); + }); + + it("Should revert if receiver is zero address", async () => { + // Attempt to execute a flashLoan with a zero address as the receiver, which should revert + await expect( + comptroller.connect(john).executeFlashLoan( + mockFlashLoanReceiver.address, + [AddressZero], // Zero address as an asset, which is invalid + [ARBFlashLoanAmount, WETHFlashLoanAmount], + ), + ).to.be.revertedWithCustomError(comptroller, "InvalidFlashLoanParams"); + }); + + it("Should be able to do flashLoan for ARB & WETH", async () => { + // Transfer ARB and WETH tokens to Alice to set up initial balances + await ARB.connect(arbHolder).transfer(vARB.address, parseUnits("100", 18)); + await ARB.connect(arbHolder).transfer(mockFlashLoanReceiver.address, parseUnits("10", 18)); + await WETH.connect(wETHHolder).transfer(vWETH.address, parseUnits("50", 18)); + await WETH.connect(wETHHolder).transfer(mockFlashLoanReceiver.address, parseUnits("5", 18)); + + // Mine blocks as required by the test setup + await mine(blocksToMine); + + const balanceBeforeARB = await ARB.balanceOf(vARB.address); + const balanceBeforeWETH = await WETH.balanceOf(vWETH.address); + + // Enable the flashLoan and set fee mantissa on vARB and vWETH contracts + await vARB.connect(impersonatedTimelock).toggleFlashLoan(); + await vWETH.connect(impersonatedTimelock).toggleFlashLoan(); + + await vARB.connect(impersonatedTimelock).setFlashLoanFeeMantissa(ARBFlashLoanFeeMantissa); + await vWETH.connect(impersonatedTimelock).setFlashLoanFeeMantissa(WETHFlashLoanFeeMantissa); + + // John initiates a flashLoan of ARB and WETH through the comptroller contract + await comptroller + .connect(john) + .executeFlashLoan( + mockFlashLoanReceiver.address, + [vARB.address, vWETH.address], + [ARBFlashLoanAmount, WETHFlashLoanAmount], + ); + + // Record ARB and WETH balances in vARB and vWETH contracts after flashLoan + const balanceAfterARB = await ARB.balanceOf(vARB.address); + const balanceAfterWETH = await WETH.balanceOf(vWETH.address); + + const ARBFlashLoanFee = ARBFlashLoanAmount.mul(ARBFlashLoanFeeMantissa).div(parseUnits("1", 18)); + const WETHFlashLoanFee = WETHFlashLoanAmount.mul(WETHFlashLoanFeeMantissa).div(parseUnits("1", 18)); + + // Validate that ARB and WETH balances in the contracts increased, confirming repayment plus fees + expect(balanceAfterARB).to.be.equal(balanceBeforeARB.add(ARBFlashLoanFee)); + expect(balanceAfterWETH).to.be.equal(balanceBeforeWETH.add(WETHFlashLoanFee)); + }); + }); +} diff --git a/tests/hardhat/Fork/flashloanSimple.ts b/tests/hardhat/Fork/flashloanSimple.ts new file mode 100644 index 000000000..92d58681c --- /dev/null +++ b/tests/hardhat/Fork/flashloanSimple.ts @@ -0,0 +1,153 @@ +import { smock } from "@defi-wonderland/smock"; +import { mine } from "@nomicfoundation/hardhat-network-helpers"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import chai from "chai"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers, network } from "hardhat"; + +import { getMaxBorrowRateMantissa } from "../../../helpers/deploymentConfig"; +import { getBlockOrTimestampBasedDeploymentInfo } from "../../../helpers/deploymentUtils"; +import { + AccessControlManager, + AccessControlManager__factory, + IERC20, + IERC20__factory, + MockFlashLoanSimpleReceiver, + MockFlashLoanSimpleReceiver__factory, + UpgradeableBeacon__factory, + VToken, + VToken__factory, +} from "../../../typechain"; +import { getContractAddresses, initMainnetUser, setForkBlock } from "./utils"; + +const { expect } = chai; +chai.use(smock.matchers); + +const blocksToMine: number = 30000; + +const FORK = process.env.FORK === "true"; +const FORKED_NETWORK = process.env.FORKED_NETWORK || "bscmainnet"; + +let john: SignerWithAddress; +let impersonatedTimelock: SignerWithAddress; +let ARB: IERC20; +let vARB: VToken; +let accessControlManager: AccessControlManager; +let arbHolder: SignerWithAddress; +let mockReceiverSimpleFlashLoan: MockFlashLoanSimpleReceiver; + +const AddressZero = "0x0000000000000000000000000000000000000000"; +const VTOKEN_BEACON_ARB = "0x74ae9919F5866cE148c81331a5FCdE71b81c4056"; +const { isTimeBased, blocksPerYear } = getBlockOrTimestampBasedDeploymentInfo(network.name); +const MAX_BORROW_RATE_MANTISSA = getMaxBorrowRateMantissa(network.name); + +const { VTOKEN2, ACM, ACC1, TOKEN2, ADMIN, BLOCK_NUMBER, TOKEN1_HOLDER } = getContractAddresses( + FORKED_NETWORK as string, +); + +const flashLoanFeeMantissa = parseUnits("0.01", 18); +const flashLoanAmount = parseUnits("100", 18); + +async function configureTimelock() { + impersonatedTimelock = await initMainnetUser(ADMIN, ethers.utils.parseUnits("2")); +} + +// Giving permission to Timelock to interact with the contracts +async function grantPermissions() { + accessControlManager = AccessControlManager__factory.connect(ACM, impersonatedTimelock); + let tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vARB.address, "toggleFlashLoan()", ADMIN); + await tx.wait(); + + tx = await accessControlManager + .connect(impersonatedTimelock) + .giveCallPermission(vARB.address, "setFlashLoanFeeMantissa(uint256)", ADMIN); + await tx.wait(); +} + +if (FORK) { + describe("FlashLoan Fork Test", async () => { + async function setup() { + // Set the forked blockchain to a specific block number to create a consistent testing environment + await setForkBlock(BLOCK_NUMBER); + + // Configure the timelock address, typically the admin account, by funding it with tokens + await configureTimelock(); + + john = await initMainnetUser(ACC1, parseUnits("2", 18)); + arbHolder = await initMainnetUser(TOKEN1_HOLDER, parseUnits("2", 18)); + + ARB = IERC20__factory.connect(TOKEN2, impersonatedTimelock); + vARB = VToken__factory.connect(VTOKEN2, impersonatedTimelock); + + // Deploy a new VToken implementation to upgrade the current VToken contract + const VTOKEN_IMPL = await ethers.getContractFactory("VToken", impersonatedTimelock); + const upgradeImpl = await VTOKEN_IMPL.deploy(isTimeBased, blocksPerYear, MAX_BORROW_RATE_MANTISSA); + await upgradeImpl.deployed(); + + // Connect to the upgradeable beacon managing VToken implementations for ARB and upgrade it + const beaconVToken = UpgradeableBeacon__factory.connect(VTOKEN_BEACON_ARB, impersonatedTimelock); + await beaconVToken.connect(impersonatedTimelock).upgradeTo(upgradeImpl.address); + + expect(await beaconVToken.callStatic.implementation()).to.be.equal(upgradeImpl.address); + + // Grant necessary permissions to users or contracts + await grantPermissions(); + } + + beforeEach(async () => { + // Run setup before each test to reset the environment + await setup(); + + // Deploy a mock flashLoan receiver to test flashLoan functionality + const MockFlashLoanSimpleReceiver = await ethers.getContractFactory( + "MockFlashLoanSimpleReceiver", + ); + mockReceiverSimpleFlashLoan = await MockFlashLoanSimpleReceiver.deploy(vARB.address); + }); + + it("Should revert if flashLoan not enabled", async () => { + // Attempt to take a flashLoan when the flashLoan feature is disabled should fail + await expect( + vARB.connect(john).executeFlashLoan(mockReceiverSimpleFlashLoan.address, flashLoanAmount), + ).to.be.revertedWithCustomError(vARB, "FlashLoanNotEnabled"); + }); + + it("Should revert if receiver is zero address", async () => { + // Enable flashLoan feature for testing + await vARB.connect(impersonatedTimelock).toggleFlashLoan(); + + // Attempt to take a flashLoan with zero address as receiver should fail + await expect(vARB.connect(john).executeFlashLoan(AddressZero, flashLoanAmount)).to.be.revertedWithCustomError( + vARB, + "ZeroAddressNotAllowed", + ); + }); + + it("Should flashLoan ARB", async () => { + // Transfer ARB tokens to test users for setting up the flashLoan test + await ARB.connect(arbHolder).transfer(vARB.address, parseUnits("1000", 18)); + await ARB.connect(arbHolder).transfer(mockReceiverSimpleFlashLoan.address, parseUnits("50", 18)); + + // Record vARB contract's ARB balance before flashLoan + const balanceBefore = await ARB.balanceOf(vARB.address); + + // Mine blocks if necessary for time-based operations + await mine(blocksToMine); + + // Enable flashLoan feature by the admin + await vARB.connect(impersonatedTimelock).toggleFlashLoan(); + await vARB.connect(impersonatedTimelock).setFlashLoanFeeMantissa(flashLoanFeeMantissa); + + // John initiates a flashLoan of 2000 ARB through the mock receiver + await vARB.connect(john).executeFlashLoan(mockReceiverSimpleFlashLoan.address, parseUnits("100", 18)); + + // Check if the ARB balance in vARB increased, validating flashLoan repayment with fees + const balanceAfter = await ARB.balanceOf(vARB.address); + const totalFlashLoanFee = flashLoanAmount.mul(flashLoanFeeMantissa).div(parseUnits("1", 18)); + + expect(balanceAfter).to.be.equal(balanceBefore.add(totalFlashLoanFee)); + }); + }); +} diff --git a/tests/hardhat/Tokens/flashLoanSimple.ts b/tests/hardhat/Tokens/flashLoanSimple.ts new file mode 100644 index 000000000..8ffb9b205 --- /dev/null +++ b/tests/hardhat/Tokens/flashLoanSimple.ts @@ -0,0 +1,163 @@ +import { FakeContract, MockContract, smock } from "@defi-wonderland/smock"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import chai from "chai"; +import { BigNumber } from "ethers"; +import { parseUnits } from "ethers/lib/utils"; +import { ethers } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { + AccessControlManager, + Comptroller, + ERC20Harness, + MockFlashLoanSimpleReceiver, + MockFlashLoanSimpleReceiver__factory, + VTokenHarness, +} from "../../../typechain"; +import { initMainnetUser } from "../Fork/utils"; +import { VTokenTestFixture, vTokenTestFixture } from "../util/TokenTestHelpers"; + +const { expect } = chai; +chai.use(smock.matchers); + +const flashLoanAmount = parseUnits("2", 18).toBigInt(); +const feeMantissa = parseUnits("0.01", 18); + +describe("FlashLoan Simple", () => { + let minter: SignerWithAddress; + let alice: SignerWithAddress; + let receiver: SignerWithAddress; + let acmUser: SignerWithAddress; + let contracts: VTokenTestFixture; + let underlying: MockContract; + let vToken: VTokenHarness; + let mockReceiverSimple: MockFlashLoanSimpleReceiver; + let accessControlManager: FakeContract; + let comptroller: FakeContract; + let comptrollerSigner: SignerWithAddress; + + beforeEach(async () => { + [minter, alice, acmUser, receiver] = await ethers.getSigners(); + contracts = await loadFixture(vTokenTestFixture); + ({ vToken, underlying, accessControlManager, comptroller } = contracts); + comptrollerSigner = await initMainnetUser(comptroller.address, ethers.utils.parseUnits("2")); + }); + + describe("Enable/disable flash loan feature", () => { + it("Should have access to toggle flash loan feature", async () => { + accessControlManager.isAllowedToCall.returns(false); + + expect(await vToken.isFlashLoanEnabled()).to.be.false; + await expect(vToken.connect(acmUser).toggleFlashLoan()).to.be.revertedWithCustomError(vToken, "Unauthorized"); + expect(await vToken.isFlashLoanEnabled()).to.be.false; + }); + + it("Enable flashLoan feature", async () => { + accessControlManager.isAllowedToCall.returns(true); + + expect(await vToken.isFlashLoanEnabled()).to.be.false; + await vToken.connect(acmUser).toggleFlashLoan(); + expect(await vToken.isFlashLoanEnabled()).to.be.true; + }); + + it("Disable flashLoan feature", async () => { + expect(await vToken.isFlashLoanEnabled()).to.be.false; + await vToken.connect(acmUser).toggleFlashLoan(); + expect(await vToken.isFlashLoanEnabled()).to.be.true; + + await vToken.connect(acmUser).toggleFlashLoan(); + expect(await vToken.isFlashLoanEnabled()).to.be.false; + }); + + it("Emit ToggleFlashLoanEnabled event on toggle flashLoan feature", async () => { + let result = await vToken.connect(acmUser).toggleFlashLoan(); + await expect(result).to.emit(vToken, "ToggleFlashLoanEnabled").withArgs(false, true); + + result = await vToken.connect(acmUser).toggleFlashLoan(); + await expect(result).to.emit(vToken, "ToggleFlashLoanEnabled").withArgs(true, false); + }); + }); + + describe("Set fee on flashLoan", () => { + it("Should have access to set fee on flashLoan", async () => { + accessControlManager.isAllowedToCall.returns(false); + + await expect(vToken.connect(acmUser).setFlashLoanFeeMantissa(feeMantissa)).to.be.revertedWithCustomError( + vToken, + "Unauthorized", + ); + }); + + it("Set fee on flashLoan", async () => { + accessControlManager.isAllowedToCall.returns(true); + + await vToken.connect(acmUser).setFlashLoanFeeMantissa(feeMantissa); + expect(await vToken.flashLoanFeeMantissa()).to.be.equal(feeMantissa); + }); + + it("Emit FlashLoanFeeUpdated event on set fee on flashLoan", async () => { + const result = await vToken.connect(acmUser).setFlashLoanFeeMantissa(feeMantissa); + await expect(result).to.emit(vToken, "FlashLoanFeeUpdated").withArgs(0, feeMantissa); + }); + }); + + describe("Transfer underlying assets to receiver contract", () => { + beforeEach(async () => { + await underlying.harnessSetBalance(vToken.address, parseUnits("1", 18)); + }); + + it("Revert if not comptroller", async () => { + await expect( + vToken.connect(acmUser).transferUnderlying(minter.address, parseUnits("1", 18)), + ).to.be.revertedWithCustomError(vToken, "InvalidComptroller"); + }); + + it("Only comptroller can transfer underlying assets to receiver contract", async () => { + await vToken.connect(comptrollerSigner).transferUnderlying(minter.address, parseUnits("1", 18)); + expect(await underlying.balanceOf(minter.address)).to.be.equal(parseUnits("1", 18)); + }); + + it("Emit TransferUnderlying event on transfer underlying assets to receiver contract", async () => { + const result = await vToken.connect(comptrollerSigner).transferUnderlying(receiver.address, parseUnits("1", 18)); + await expect(result) + .to.emit(vToken, "FlashLoanAmountTransferred") + .withArgs(underlying.address, receiver.address, parseUnits("1", 18)); + }); + }); + + describe("FlashLoan Single Asset", () => { + beforeEach(async () => { + const MockFlashLoanSimpleReceiver = await ethers.getContractFactory( + "MockFlashLoanSimpleReceiver", + ); + mockReceiverSimple = await MockFlashLoanSimpleReceiver.deploy(vToken.address); + await mockReceiverSimple.deployed(); + + await vToken.connect(acmUser).setFlashLoanFeeMantissa(feeMantissa); + await underlying.harnessSetBalance(mockReceiverSimple.address, parseUnits("1", 18)); + await underlying.harnessSetBalance(vToken.address, parseUnits("10", 18)); + }); + + it("Should revert if the flashLoan is not enabled", async () => { + await expect(mockReceiverSimple.requestFlashLoan(flashLoanAmount)).to.be.revertedWithCustomError( + vToken, + "FlashLoanNotEnabled", + ); + }); + + it("FlashLoan for single underlying", async () => { + await vToken.connect(acmUser).toggleFlashLoan(); + + const balanceBeforeflashLoan = await underlying.balanceOf(vToken.address); + const flashLoan = await mockReceiverSimple.connect(alice).requestFlashLoan(flashLoanAmount); + const balanceAfterflashLoan = await underlying.balanceOf(vToken.address); + + const fee = BigNumber.from(flashLoanAmount).mul(feeMantissa).div(parseUnits("1", 18)); + + expect(balanceAfterflashLoan).to.be.equal(balanceBeforeflashLoan.add(fee)); + await expect(flashLoan) + .to.emit(vToken, "FlashLoanExecuted") + .withArgs(mockReceiverSimple.address, underlying.address, flashLoanAmount); + }); + }); +}); diff --git a/tests/hardhat/util/TokenTestHelpers.ts b/tests/hardhat/util/TokenTestHelpers.ts index 019e983b2..4e5f1e733 100644 --- a/tests/hardhat/util/TokenTestHelpers.ts +++ b/tests/hardhat/util/TokenTestHelpers.ts @@ -38,6 +38,8 @@ interface VTokenParameters { isTimeBased: boolean; blocksPerYear: BigNumberish; maxBorrowRateMantissa: BigNumberish; + isFlashLoanAllowed: boolean; + flashLoanFeeMantissa: BigNumberish; } const getNameAndSymbol = async (underlying: AddressOrContract): Promise<[string, string]> => { @@ -110,6 +112,8 @@ const deployVTokenDependencies = async ( @@ -146,6 +150,8 @@ export const makeVToken = async ; return vToken; diff --git a/tests/integration/index.ts b/tests/integration/index.ts index eeac37af7..4260e1233 100644 --- a/tests/integration/index.ts +++ b/tests/integration/index.ts @@ -484,8 +484,9 @@ describe("Straight Cases For Single User Liquidation and healing", function () { dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBNX.address).returns(convertToUnit("100", 12)); dummyPriceOracle.getUnderlyingPrice.whenCalledWith(vBTCB.address).returns(convertToUnit("100", 12)); await Comptroller.setPriceOracle(dummyPriceOracle.address); - await expect(Comptroller.connect(acc1Signer).liquidateAccount(acc2, [param])).to.be.revertedWith( - "Nonzero borrow balance after liquidation", + await expect(Comptroller.connect(acc1Signer).liquidateAccount(acc2, [param])).to.be.revertedWithCustomError( + Comptroller, + "NonzeroBorrowBalance", ); });