mirror of
				https://github.com/Instadapp/aave-protocol-v2.git
				synced 2024-07-29 21:47:30 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			470 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Solidity
		
	
	
	
	
	
			
		
		
	
	
			470 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Solidity
		
	
	
	
	
	
| // SPDX-License-Identifier: agpl-3.0
 | |
| pragma solidity 0.6.12;
 | |
| pragma experimental ABIEncoderV2;
 | |
| 
 | |
| import {SafeMath} from '../../../dependencies/openzeppelin/contracts/SafeMath.sol';
 | |
| import {IERC20} from '../../../dependencies/openzeppelin/contracts/IERC20.sol';
 | |
| import {ReserveLogic} from './ReserveLogic.sol';
 | |
| import {GenericLogic} from './GenericLogic.sol';
 | |
| import {WadRayMath} from '../math/WadRayMath.sol';
 | |
| import {PercentageMath} from '../math/PercentageMath.sol';
 | |
| import {SafeERC20} from '../../../dependencies/openzeppelin/contracts/SafeERC20.sol';
 | |
| import {ReserveConfiguration} from '../configuration/ReserveConfiguration.sol';
 | |
| import {UserConfiguration} from '../configuration/UserConfiguration.sol';
 | |
| import {Errors} from '../helpers/Errors.sol';
 | |
| import {Helpers} from '../helpers/Helpers.sol';
 | |
| import {IReserveInterestRateStrategy} from '../../../interfaces/IReserveInterestRateStrategy.sol';
 | |
| import {DataTypes} from '../types/DataTypes.sol';
 | |
| 
 | |
| /**
 | |
|  * @title ReserveLogic library
 | |
|  * @author Aave
 | |
|  * @notice Implements functions to validate the different actions of the protocol
 | |
|  */
 | |
| library ValidationLogic {
 | |
|   using ReserveLogic for DataTypes.ReserveData;
 | |
|   using SafeMath for uint256;
 | |
|   using WadRayMath for uint256;
 | |
|   using PercentageMath for uint256;
 | |
|   using SafeERC20 for IERC20;
 | |
|   using ReserveConfiguration for DataTypes.ReserveConfigurationMap;
 | |
|   using UserConfiguration for DataTypes.UserConfigurationMap;
 | |
| 
 | |
|   uint256 public constant REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD = 4000;
 | |
|   uint256 public constant REBALANCE_UP_USAGE_RATIO_THRESHOLD = 0.95 * 1e27; //usage ratio of 95%
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a deposit action
 | |
|    * @param reserve The reserve object on which the user is depositing
 | |
|    * @param amount The amount to be deposited
 | |
|    */
 | |
|   function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) external view {
 | |
|     (bool isActive, bool isFrozen, , ) = reserve.configuration.getFlags();
 | |
| 
 | |
|     require(amount != 0, Errors.VL_INVALID_AMOUNT);
 | |
|     require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
|     require(!isFrozen, Errors.VL_RESERVE_FROZEN);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a withdraw action
 | |
|    * @param reserveAddress The address of the reserve
 | |
|    * @param amount The amount to be withdrawn
 | |
|    * @param userBalance The balance of the user
 | |
|    * @param reservesData The reserves state
 | |
|    * @param userConfig The user configuration
 | |
|    * @param reserves The addresses of the reserves
 | |
|    * @param reservesCount The number of reserves
 | |
|    * @param oracle The price oracle
 | |
|    */
 | |
|   function validateWithdraw(
 | |
|     address reserveAddress,
 | |
|     uint256 amount,
 | |
|     uint256 userBalance,
 | |
|     mapping(address => DataTypes.ReserveData) storage reservesData,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     mapping(uint256 => address) storage reserves,
 | |
|     uint256 reservesCount,
 | |
|     address oracle
 | |
|   ) external view {
 | |
|     require(amount != 0, Errors.VL_INVALID_AMOUNT);
 | |
|     require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE);
 | |
| 
 | |
|     (bool isActive, , , ) = reservesData[reserveAddress].configuration.getFlags();
 | |
|     require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
| 
 | |
|     require(
 | |
|       GenericLogic.balanceDecreaseAllowed(
 | |
|         reserveAddress,
 | |
|         msg.sender,
 | |
|         amount,
 | |
|         reservesData,
 | |
|         userConfig,
 | |
|         reserves,
 | |
|         reservesCount,
 | |
|         oracle
 | |
|       ),
 | |
|       Errors.VL_TRANSFER_NOT_ALLOWED
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   struct ValidateBorrowLocalVars {
 | |
|     uint256 currentLtv;
 | |
|     uint256 currentLiquidationThreshold;
 | |
|     uint256 amountOfCollateralNeededETH;
 | |
|     uint256 userCollateralBalanceETH;
 | |
|     uint256 userBorrowBalanceETH;
 | |
|     uint256 availableLiquidity;
 | |
|     uint256 healthFactor;
 | |
|     bool isActive;
 | |
|     bool isFrozen;
 | |
|     bool borrowingEnabled;
 | |
|     bool stableRateBorrowingEnabled;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a borrow action
 | |
|    * @param asset The address of the asset to borrow
 | |
|    * @param reserve The reserve state from which the user is borrowing
 | |
|    * @param userAddress The address of the user
 | |
|    * @param amount The amount to be borrowed
 | |
|    * @param amountInETH The amount to be borrowed, in ETH
 | |
|    * @param interestRateMode The interest rate mode at which the user is borrowing
 | |
|    * @param maxStableLoanPercent The max amount of the liquidity that can be borrowed at stable rate, in percentage
 | |
|    * @param reservesData The state of all the reserves
 | |
|    * @param userConfig The state of the user for the specific reserve
 | |
|    * @param reserves The addresses of all the active reserves
 | |
|    * @param oracle The price oracle
 | |
|    */
 | |
| 
 | |
|   function validateBorrow(
 | |
|     address asset,
 | |
|     DataTypes.ReserveData storage reserve,
 | |
|     address userAddress,
 | |
|     uint256 amount,
 | |
|     uint256 amountInETH,
 | |
|     uint256 interestRateMode,
 | |
|     uint256 maxStableLoanPercent,
 | |
|     mapping(address => DataTypes.ReserveData) storage reservesData,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     mapping(uint256 => address) storage reserves,
 | |
|     uint256 reservesCount,
 | |
|     address oracle
 | |
|   ) external view {
 | |
|     ValidateBorrowLocalVars memory vars;
 | |
| 
 | |
|     (vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.stableRateBorrowingEnabled) = reserve
 | |
|       .configuration
 | |
|       .getFlags();
 | |
| 
 | |
|     require(vars.isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
|     require(!vars.isFrozen, Errors.VL_RESERVE_FROZEN);
 | |
|     require(amount != 0, Errors.VL_INVALID_AMOUNT);
 | |
| 
 | |
|     require(vars.borrowingEnabled, Errors.VL_BORROWING_NOT_ENABLED);
 | |
| 
 | |
|     //validate interest rate mode
 | |
|     require(
 | |
|       uint256(DataTypes.InterestRateMode.VARIABLE) == interestRateMode ||
 | |
|         uint256(DataTypes.InterestRateMode.STABLE) == interestRateMode,
 | |
|       Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED
 | |
|     );
 | |
| 
 | |
|     (
 | |
|       vars.userCollateralBalanceETH,
 | |
|       vars.userBorrowBalanceETH,
 | |
|       vars.currentLtv,
 | |
|       vars.currentLiquidationThreshold,
 | |
|       vars.healthFactor
 | |
|     ) = GenericLogic.calculateUserAccountData(
 | |
|       userAddress,
 | |
|       reservesData,
 | |
|       userConfig,
 | |
|       reserves,
 | |
|       reservesCount,
 | |
|       oracle
 | |
|     );
 | |
| 
 | |
|     require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0);
 | |
| 
 | |
|     require(
 | |
|       vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
 | |
|       Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
 | |
|     );
 | |
| 
 | |
|     //add the current already borrowed amount to the amount requested to calculate the total collateral needed.
 | |
|     vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv(
 | |
|       vars.currentLtv
 | |
|     ); //LTV is calculated in percentage
 | |
| 
 | |
|     require(
 | |
|       vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
 | |
|       Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW
 | |
|     );
 | |
| 
 | |
|     /**
 | |
|      * Following conditions need to be met if the user is borrowing at a stable rate:
 | |
|      * 1. Reserve must be enabled for stable rate borrowing
 | |
|      * 2. Users cannot borrow from the reserve if their collateral is (mostly) the same currency
 | |
|      *    they are borrowing, to prevent abuses.
 | |
|      * 3. Users will be able to borrow only a portion of the total available liquidity
 | |
|      **/
 | |
| 
 | |
|     if (interestRateMode == uint256(DataTypes.InterestRateMode.STABLE)) {
 | |
|       //check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve
 | |
| 
 | |
|       require(vars.stableRateBorrowingEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED);
 | |
| 
 | |
|       require(
 | |
|         !userConfig.isUsingAsCollateral(reserve.id) ||
 | |
|           reserve.configuration.getLtv() == 0 ||
 | |
|           amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress),
 | |
|         Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY
 | |
|       );
 | |
| 
 | |
|       vars.availableLiquidity = IERC20(asset).balanceOf(reserve.aTokenAddress);
 | |
| 
 | |
|       //calculate the max available loan size in stable rate mode as a percentage of the
 | |
|       //available liquidity
 | |
|       uint256 maxLoanSizeStable = vars.availableLiquidity.percentMul(maxStableLoanPercent);
 | |
| 
 | |
|       require(amount <= maxLoanSizeStable, Errors.VL_AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a repay action
 | |
|    * @param reserve The reserve state from which the user is repaying
 | |
|    * @param amountSent The amount sent for the repayment. Can be an actual value or uint(-1)
 | |
|    * @param onBehalfOf The address of the user msg.sender is repaying for
 | |
|    * @param stableDebt The borrow balance of the user
 | |
|    * @param variableDebt The borrow balance of the user
 | |
|    */
 | |
|   function validateRepay(
 | |
|     DataTypes.ReserveData storage reserve,
 | |
|     uint256 amountSent,
 | |
|     DataTypes.InterestRateMode rateMode,
 | |
|     address onBehalfOf,
 | |
|     uint256 stableDebt,
 | |
|     uint256 variableDebt
 | |
|   ) external view {
 | |
|     bool isActive = reserve.configuration.getActive();
 | |
| 
 | |
|     require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
| 
 | |
|     require(amountSent > 0, Errors.VL_INVALID_AMOUNT);
 | |
| 
 | |
|     require(
 | |
|       (stableDebt > 0 &&
 | |
|         DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) ||
 | |
|         (variableDebt > 0 &&
 | |
|           DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE),
 | |
|       Errors.VL_NO_DEBT_OF_SELECTED_TYPE
 | |
|     );
 | |
| 
 | |
|     require(
 | |
|       amountSent != uint256(-1) || msg.sender == onBehalfOf,
 | |
|       Errors.VL_NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a swap of borrow rate mode.
 | |
|    * @param reserve The reserve state on which the user is swapping the rate
 | |
|    * @param userConfig The user reserves configuration
 | |
|    * @param stableDebt The stable debt of the user
 | |
|    * @param variableDebt The variable debt of the user
 | |
|    * @param currentRateMode The rate mode of the borrow
 | |
|    */
 | |
|   function validateSwapRateMode(
 | |
|     DataTypes.ReserveData storage reserve,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     uint256 stableDebt,
 | |
|     uint256 variableDebt,
 | |
|     DataTypes.InterestRateMode currentRateMode
 | |
|   ) external view {
 | |
|     (bool isActive, bool isFrozen, , bool stableRateEnabled) = reserve.configuration.getFlags();
 | |
| 
 | |
|     require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
|     require(!isFrozen, Errors.VL_RESERVE_FROZEN);
 | |
| 
 | |
|     if (currentRateMode == DataTypes.InterestRateMode.STABLE) {
 | |
|       require(stableDebt > 0, Errors.VL_NO_STABLE_RATE_LOAN_IN_RESERVE);
 | |
|     } else if (currentRateMode == DataTypes.InterestRateMode.VARIABLE) {
 | |
|       require(variableDebt > 0, Errors.VL_NO_VARIABLE_RATE_LOAN_IN_RESERVE);
 | |
|       /**
 | |
|        * user wants to swap to stable, before swapping we need to ensure that
 | |
|        * 1. stable borrow rate is enabled on the reserve
 | |
|        * 2. user is not trying to abuse the reserve by depositing
 | |
|        * more collateral than he is borrowing, artificially lowering
 | |
|        * the interest rate, borrowing at variable, and switching to stable
 | |
|        **/
 | |
|       require(stableRateEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED);
 | |
| 
 | |
|       require(
 | |
|         !userConfig.isUsingAsCollateral(reserve.id) ||
 | |
|           reserve.configuration.getLtv() == 0 ||
 | |
|           stableDebt.add(variableDebt) > IERC20(reserve.aTokenAddress).balanceOf(msg.sender),
 | |
|         Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY
 | |
|       );
 | |
|     } else {
 | |
|       revert(Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a stable borrow rate rebalance action
 | |
|    * @param reserve The reserve state on which the user is getting rebalanced
 | |
|    * @param reserveAddress The address of the reserve
 | |
|    * @param stableDebtToken The stable debt token instance
 | |
|    * @param variableDebtToken The variable debt token instance
 | |
|    * @param aTokenAddress The address of the aToken contract
 | |
|    */
 | |
|   function validateRebalanceStableBorrowRate(
 | |
|     DataTypes.ReserveData storage reserve,
 | |
|     address reserveAddress,
 | |
|     IERC20 stableDebtToken,
 | |
|     IERC20 variableDebtToken,
 | |
|     address aTokenAddress
 | |
|   ) external view {
 | |
|     (bool isActive, , , ) = reserve.configuration.getFlags();
 | |
| 
 | |
|     require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
 | |
| 
 | |
|     //if the usage ratio is below 95%, no rebalances are needed
 | |
|     uint256 totalDebt =
 | |
|       stableDebtToken.totalSupply().add(variableDebtToken.totalSupply()).wadToRay();
 | |
|     uint256 availableLiquidity = IERC20(reserveAddress).balanceOf(aTokenAddress).wadToRay();
 | |
|     uint256 usageRatio = totalDebt == 0 ? 0 : totalDebt.rayDiv(availableLiquidity.add(totalDebt));
 | |
| 
 | |
|     //if the liquidity rate is below REBALANCE_UP_THRESHOLD of the max variable APR at 95% usage,
 | |
|     //then we allow rebalancing of the stable rate positions.
 | |
| 
 | |
|     uint256 currentLiquidityRate = reserve.currentLiquidityRate;
 | |
|     uint256 maxVariableBorrowRate =
 | |
|       IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).getMaxVariableBorrowRate();
 | |
| 
 | |
|     require(
 | |
|       usageRatio >= REBALANCE_UP_USAGE_RATIO_THRESHOLD &&
 | |
|         currentLiquidityRate <=
 | |
|         maxVariableBorrowRate.percentMul(REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD),
 | |
|       Errors.LP_INTEREST_RATE_REBALANCE_CONDITIONS_NOT_MET
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates the action of setting an asset as collateral
 | |
|    * @param reserve The state of the reserve that the user is enabling or disabling as collateral
 | |
|    * @param reserveAddress The address of the reserve
 | |
|    * @param reservesData The data of all the reserves
 | |
|    * @param userConfig The state of the user for the specific reserve
 | |
|    * @param reserves The addresses of all the active reserves
 | |
|    * @param oracle The price oracle
 | |
|    */
 | |
|   function validateSetUseReserveAsCollateral(
 | |
|     DataTypes.ReserveData storage reserve,
 | |
|     address reserveAddress,
 | |
|     bool useAsCollateral,
 | |
|     mapping(address => DataTypes.ReserveData) storage reservesData,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     mapping(uint256 => address) storage reserves,
 | |
|     uint256 reservesCount,
 | |
|     address oracle
 | |
|   ) external view {
 | |
|     uint256 underlyingBalance = IERC20(reserve.aTokenAddress).balanceOf(msg.sender);
 | |
| 
 | |
|     require(underlyingBalance > 0, Errors.VL_UNDERLYING_BALANCE_NOT_GREATER_THAN_0);
 | |
| 
 | |
|     require(
 | |
|       useAsCollateral ||
 | |
|         GenericLogic.balanceDecreaseAllowed(
 | |
|           reserveAddress,
 | |
|           msg.sender,
 | |
|           underlyingBalance,
 | |
|           reservesData,
 | |
|           userConfig,
 | |
|           reserves,
 | |
|           reservesCount,
 | |
|           oracle
 | |
|         ),
 | |
|       Errors.VL_DEPOSIT_ALREADY_IN_USE
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates a flashloan action
 | |
|    * @param assets The assets being flashborrowed
 | |
|    * @param amounts The amounts for each asset being borrowed
 | |
|    **/
 | |
|   function validateFlashloan(address[] memory assets, uint256[] memory amounts) internal pure {
 | |
|     require(assets.length == amounts.length, Errors.VL_INCONSISTENT_FLASHLOAN_PARAMS);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates the liquidation action
 | |
|    * @param collateralReserve The reserve data of the collateral
 | |
|    * @param principalReserve The reserve data of the principal
 | |
|    * @param userConfig The user configuration
 | |
|    * @param userHealthFactor The user's health factor
 | |
|    * @param userStableDebt Total stable debt balance of the user
 | |
|    * @param userVariableDebt Total variable debt balance of the user
 | |
|    **/
 | |
|   function validateLiquidationCall(
 | |
|     DataTypes.ReserveData storage collateralReserve,
 | |
|     DataTypes.ReserveData storage principalReserve,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     uint256 userHealthFactor,
 | |
|     uint256 userStableDebt,
 | |
|     uint256 userVariableDebt
 | |
|   ) internal view returns (uint256, string memory) {
 | |
|     if (
 | |
|       !collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()
 | |
|     ) {
 | |
|       return (
 | |
|         uint256(Errors.CollateralManagerErrors.NO_ACTIVE_RESERVE),
 | |
|         Errors.VL_NO_ACTIVE_RESERVE
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) {
 | |
|       return (
 | |
|         uint256(Errors.CollateralManagerErrors.HEALTH_FACTOR_ABOVE_THRESHOLD),
 | |
|         Errors.LPCM_HEALTH_FACTOR_NOT_BELOW_THRESHOLD
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     bool isCollateralEnabled =
 | |
|       collateralReserve.configuration.getLiquidationThreshold() > 0 &&
 | |
|         userConfig.isUsingAsCollateral(collateralReserve.id);
 | |
| 
 | |
|     //if collateral isn't enabled as collateral by user, it cannot be liquidated
 | |
|     if (!isCollateralEnabled) {
 | |
|       return (
 | |
|         uint256(Errors.CollateralManagerErrors.COLLATERAL_CANNOT_BE_LIQUIDATED),
 | |
|         Errors.LPCM_COLLATERAL_CANNOT_BE_LIQUIDATED
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (userStableDebt == 0 && userVariableDebt == 0) {
 | |
|       return (
 | |
|         uint256(Errors.CollateralManagerErrors.CURRRENCY_NOT_BORROWED),
 | |
|         Errors.LPCM_SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.LPCM_NO_ERRORS);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @dev Validates an aToken transfer
 | |
|    * @param from The user from which the aTokens are being transferred
 | |
|    * @param reservesData The state of all the reserves
 | |
|    * @param userConfig The state of the user for the specific reserve
 | |
|    * @param reserves The addresses of all the active reserves
 | |
|    * @param oracle The price oracle
 | |
|    */
 | |
|   function validateTransfer(
 | |
|     address from,
 | |
|     mapping(address => DataTypes.ReserveData) storage reservesData,
 | |
|     DataTypes.UserConfigurationMap storage userConfig,
 | |
|     mapping(uint256 => address) storage reserves,
 | |
|     uint256 reservesCount,
 | |
|     address oracle
 | |
|   ) internal view {
 | |
|     (, , , , uint256 healthFactor) =
 | |
|       GenericLogic.calculateUserAccountData(
 | |
|         from,
 | |
|         reservesData,
 | |
|         userConfig,
 | |
|         reserves,
 | |
|         reservesCount,
 | |
|         oracle
 | |
|       );
 | |
| 
 | |
|     require(
 | |
|       healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
 | |
|       Errors.VL_TRANSFER_NOT_ALLOWED
 | |
|     );
 | |
|   }
 | |
| }
 | 
