// SPDX-License-Identifier: agpl-3.0
pragma solidity ^0.6.8;
pragma experimental ABIEncoderV2;

import {SafeMath} from '@openzeppelin/contracts/math/SafeMath.sol';
import {
  VersionedInitializable
} from '../libraries/openzeppelin-upgradeability/VersionedInitializable.sol';
import {
  InitializableAdminUpgradeabilityProxy
} from '../libraries/openzeppelin-upgradeability/InitializableAdminUpgradeabilityProxy.sol';
import {ReserveConfiguration} from '../libraries/configuration/ReserveConfiguration.sol';
import {ILendingPoolAddressesProvider} from '../interfaces/ILendingPoolAddressesProvider.sol';
import {ILendingPool} from '../interfaces/ILendingPool.sol';
import {IERC20Detailed} from '../interfaces/IERC20Detailed.sol';
import {Errors} from '../libraries/helpers/Errors.sol';

/**
 * @title LendingPoolConfigurator contract
 * @author Aave
 * @notice Executes configuration methods on the LendingPoolCore contract. Allows to enable/disable reserves
 * and set different protocol parameters.
 **/

contract LendingPoolConfigurator is VersionedInitializable {
  using SafeMath for uint256;
  using ReserveConfiguration for ReserveConfiguration.Map;

  /**
   * @dev emitted when a reserve is initialized.
   * @param asset the address of the reserve
   * @param aToken the address of the overlying aToken contract
   * @param stableDebtToken the address of the associated stable rate debt token
   * @param variableDebtToken the address of the associated variable rate debt token
   * @param interestRateStrategyAddress the address of the interest rate strategy for the reserve
   **/
  event ReserveInitialized(
    address indexed asset,
    address indexed aToken,
    address stableDebtToken,
    address variableDebtToken,
    address interestRateStrategyAddress
  );

  /**
   * @dev emitted when borrowing is enabled on a reserve
   * @param asset the address of the reserve
   * @param stableRateEnabled true if stable rate borrowing is enabled, false otherwise
   **/
  event BorrowingEnabledOnReserve(address asset, bool stableRateEnabled);

  /**
   * @dev emitted when borrowing is disabled on a reserve
   * @param asset the address of the reserve
   **/
  event BorrowingDisabledOnReserve(address indexed asset);

  /**
   * @dev emitted when a reserve is enabled as collateral.
   * @param asset the address of the reserve
   * @param ltv the loan to value of the asset when used as collateral
   * @param liquidationThreshold the threshold at which loans using this asset as collateral will be considered undercollateralized
   * @param liquidationBonus the bonus liquidators receive to liquidate this asset
   **/
  event ReserveEnabledAsCollateral(
    address indexed asset,
    uint256 ltv,
    uint256 liquidationThreshold,
    uint256 liquidationBonus
  );

  /**
   * @dev emitted when a reserve is disabled as collateral
   * @param asset the address of the reserve
   **/
  event ReserveDisabledAsCollateral(address indexed asset);

  /**
   * @dev emitted when stable rate borrowing is enabled on a reserve
   * @param asset the address of the reserve
   **/
  event StableRateEnabledOnReserve(address indexed asset);

  /**
   * @dev emitted when stable rate borrowing is disabled on a reserve
   * @param asset the address of the reserve
   **/
  event StableRateDisabledOnReserve(address indexed asset);

  /**
   * @dev emitted when a reserve is activated
   * @param asset the address of the reserve
   **/
  event ReserveActivated(address indexed asset);

  /**
   * @dev emitted when a reserve is deactivated
   * @param asset the address of the reserve
   **/
  event ReserveDeactivated(address indexed asset);

  /**
   * @dev emitted when a reserve is freezed
   * @param asset the address of the reserve
   **/
  event ReserveFreezed(address indexed asset);

  /**
   * @dev emitted when a reserve is unfreezed
   * @param asset the address of the reserve
   **/
  event ReserveUnfreezed(address indexed asset);

  /**
   * @dev emitted when a reserve loan to value is updated
   * @param asset the address of the reserve
   * @param ltv the new value for the loan to value
   **/
  event ReserveBaseLtvChanged(address asset, uint256 ltv);

  /**
   * @dev emitted when a reserve liquidation threshold is updated
   * @param asset the address of the reserve
   * @param threshold the new value for the liquidation threshold
   **/
  event ReserveLiquidationThresholdChanged(address asset, uint256 threshold);

  /**
   * @dev emitted when a reserve liquidation bonus is updated
   * @param asset the address of the reserve
   * @param bonus the new value for the liquidation bonus
   **/
  event ReserveLiquidationBonusChanged(address asset, uint256 bonus);

  /**
   * @dev emitted when the reserve decimals are updated
   * @param asset the address of the reserve
   * @param decimals the new decimals
   **/
  event ReserveDecimalsChanged(address asset, uint256 decimals);

  /**
   * @dev emitted when a reserve interest strategy contract is updated
   * @param asset the address of the reserve
   * @param strategy the new address of the interest strategy contract
   **/
  event ReserveInterestRateStrategyChanged(address asset, address strategy);

  /**
   * @dev emitted when an aToken implementation is upgraded
   * @param asset the address of the reserve
   * @param proxy the aToken proxy address
   * @param implementation the new aToken implementation
   **/
  event ATokenUpgraded(address asset, address proxy, address implementation);

  /**
   * @dev emitted when the implementation of a stable debt token is upgraded
   * @param asset the address of the reserve
   * @param proxy the stable debt token proxy address
   * @param implementation the new aToken implementation
   **/
  event StableDebtTokenUpgraded(address asset, address proxy, address implementation);

  /**
   * @dev emitted when the implementation of a variable debt token is upgraded
   * @param asset the address of the reserve
   * @param proxy the variable debt token proxy address
   * @param implementation the new aToken implementation
   **/
  event VariableDebtTokenUpgraded(address asset, address proxy, address implementation);

  ILendingPoolAddressesProvider internal addressesProvider;
  ILendingPool internal pool;

  /**
   * @dev only the lending pool manager can call functions affected by this modifier
   **/
  modifier onlyLendingPoolManager {
    require(
      addressesProvider.getLendingPoolManager() == msg.sender,
      Errors.CALLER_NOT_LENDING_POOL_MANAGER
    );
    _;
  }

  uint256 public constant CONFIGURATOR_REVISION = 0x3;

  function getRevision() internal override pure returns (uint256) {
    return CONFIGURATOR_REVISION;
  }

  function initialize(ILendingPoolAddressesProvider provider) public initializer {
    addressesProvider = provider;
    pool = ILendingPool(addressesProvider.getLendingPool());
  }

  /**
   * @dev initializes a reserve
   * @param asset the address of the reserve to be initialized
   * @param aTokenImpl  the address of the aToken contract implementation
   * @param stableDebtTokenImpl the address of the stable debt token contract
   * @param variableDebtTokenImpl the address of the variable debt token contract
   * @param underlyingAssetDecimals the decimals of the reserve underlying asset
   * @param interestRateStrategyAddress the address of the interest rate strategy contract for this reserve
   **/
  function initReserve(
    address asset,
    address aTokenImpl,
    address stableDebtTokenImpl,
    address variableDebtTokenImpl,
    uint8 underlyingAssetDecimals,
    address interestRateStrategyAddress
  ) public onlyLendingPoolManager {
    address aTokenProxyAddress = _initTokenWithProxy(aTokenImpl, underlyingAssetDecimals);

    address stableDebtTokenProxyAddress = _initTokenWithProxy(
      stableDebtTokenImpl,
      underlyingAssetDecimals
    );

    address variableDebtTokenProxyAddress = _initTokenWithProxy(
      variableDebtTokenImpl,
      underlyingAssetDecimals
    );

    pool.initReserve(
      asset,
      aTokenProxyAddress,
      stableDebtTokenProxyAddress,
      variableDebtTokenProxyAddress,
      interestRateStrategyAddress
    );

    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setDecimals(underlyingAssetDecimals);

    currentConfig.setActive(true);
    currentConfig.setFrozen(false);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveInitialized(
      asset,
      aTokenProxyAddress,
      stableDebtTokenProxyAddress,
      variableDebtTokenProxyAddress,
      interestRateStrategyAddress
    );
  }

  /**
   * @dev updates the aToken implementation for the asset
   * @param asset the address of the reserve to be updated
   * @param implementation the address of the new aToken implementation
   **/
  function updateAToken(address asset, address implementation) external onlyLendingPoolManager {
    (address aTokenAddress, , ) = pool.getReserveTokensAddresses(asset);

    _upgradeTokenImplementation(asset, aTokenAddress, implementation);

    emit ATokenUpgraded(asset, aTokenAddress, implementation);
  }

  /**
   * @dev updates the stable debt token implementation for the asset
   * @param asset the address of the reserve to be updated
   * @param implementation the address of the new aToken implementation
   **/
  function updateStableDebtToken(address asset, address implementation)
    external
    onlyLendingPoolManager
  {
    (, address stableDebtToken, ) = pool.getReserveTokensAddresses(asset);

    _upgradeTokenImplementation(asset, stableDebtToken, implementation);

    emit StableDebtTokenUpgraded(asset, stableDebtToken, implementation);
  }

  /**
   * @dev updates the variable debt token implementation for the asset
   * @param asset the address of the reserve to be updated
   * @param implementation the address of the new aToken implementation
   **/
  function updateVariableDebtToken(address asset, address implementation)
    external
    onlyLendingPoolManager
  {
    (, , address variableDebtToken) = pool.getReserveTokensAddresses(asset);

    _upgradeTokenImplementation(asset, variableDebtToken, implementation);

    emit VariableDebtTokenUpgraded(asset, variableDebtToken, implementation);
  }

  /**
   * @dev enables borrowing on a reserve
   * @param asset the address of the reserve
   * @param stableBorrowRateEnabled true if stable borrow rate needs to be enabled by default on this reserve
   **/
  function enableBorrowingOnReserve(address asset, bool stableBorrowRateEnabled)
    external
    onlyLendingPoolManager
  {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setBorrowingEnabled(true);
    currentConfig.setStableRateBorrowingEnabled(stableBorrowRateEnabled);

    pool.setConfiguration(asset, currentConfig.data);

    emit BorrowingEnabledOnReserve(asset, stableBorrowRateEnabled);
  }

  /**
   * @dev disables borrowing on a reserve
   * @param asset the address of the reserve
   **/
  function disableBorrowingOnReserve(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setBorrowingEnabled(false);

    pool.setConfiguration(asset, currentConfig.data);
    emit BorrowingDisabledOnReserve(asset);
  }

  /**
   * @dev enables a reserve to be used as collateral
   * @param asset the address of the reserve
   * @param ltv the loan to value of the asset when used as collateral
   * @param liquidationThreshold the threshold at which loans using this asset as collateral will be considered undercollateralized
   * @param liquidationBonus the bonus liquidators receive to liquidate this asset
   **/
  function enableReserveAsCollateral(
    address asset,
    uint256 ltv,
    uint256 liquidationThreshold,
    uint256 liquidationBonus
  ) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setLtv(ltv);
    currentConfig.setLiquidationThreshold(liquidationThreshold);
    currentConfig.setLiquidationBonus(liquidationBonus);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveEnabledAsCollateral(asset, ltv, liquidationThreshold, liquidationBonus);
  }

  /**
   * @dev disables a reserve as collateral
   * @param asset the address of the reserve
   **/
  function disableReserveAsCollateral(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setLtv(0);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveDisabledAsCollateral(asset);
  }

  /**
   * @dev enable stable rate borrowing on a reserve
   * @param asset the address of the reserve
   **/
  function enableReserveStableRate(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setStableRateBorrowingEnabled(true);

    pool.setConfiguration(asset, currentConfig.data);

    emit StableRateEnabledOnReserve(asset);
  }

  /**
   * @dev disable stable rate borrowing on a reserve
   * @param asset the address of the reserve
   **/
  function disableReserveStableRate(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setStableRateBorrowingEnabled(false);

    pool.setConfiguration(asset, currentConfig.data);

    emit StableRateDisabledOnReserve(asset);
  }

  /**
   * @dev activates a reserve
   * @param asset the address of the reserve
   **/
  function activateReserve(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setActive(true);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveActivated(asset);
  }

  /**
   * @dev deactivates a reserve
   * @param asset the address of the reserve
   **/
  function deactivateReserve(address asset) external onlyLendingPoolManager {
    (
      uint256 availableLiquidity,
      uint256 totalBorrowsStable,
      uint256 totalBorrowsVariable,
      ,
      ,
      ,
      ,
      ,
      ,

    ) = pool.getReserveData(asset);
    require(
      availableLiquidity == 0 && totalBorrowsStable == 0 && totalBorrowsVariable == 0,
      Errors.RESERVE_LIQUIDITY_NOT_0
    );

    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setActive(false);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveDeactivated(asset);
  }

  /**
   * @dev freezes a reserve. A freezed reserve doesn't accept any new deposit, borrow or rate swap, but can accept repayments, liquidations, rate rebalances and redeems
   * @param asset the address of the reserve
   **/
  function freezeReserve(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setFrozen(true);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveFreezed(asset);
  }

  /**
   * @dev unfreezes a reserve
   * @param asset the address of the reserve
   **/
  function unfreezeReserve(address asset) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setFrozen(false);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveUnfreezed(asset);
  }

  /**
   * @dev emitted when a reserve loan to value is updated
   * @param asset the address of the reserve
   * @param ltv the new value for the loan to value
   **/
  function setLtv(address asset, uint256 ltv) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setLtv(ltv);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveBaseLtvChanged(asset, ltv);
  }

  /**
   * @dev updates the liquidation threshold of a reserve.
   * @param asset the address of the reserve
   * @param threshold the new value for the liquidation threshold
   **/
  function setLiquidationThreshold(address asset, uint256 threshold)
    external
    onlyLendingPoolManager
  {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setLiquidationThreshold(threshold);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveLiquidationThresholdChanged(asset, threshold);
  }

  /**
   * @dev updates the liquidation bonus of a reserve
   * @param asset the address of the reserve
   * @param bonus the new value for the liquidation bonus
   **/
  function setLiquidationBonus(address asset, uint256 bonus) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setLiquidationBonus(bonus);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveLiquidationBonusChanged(asset, bonus);
  }

  /**
   * @dev updates the reserve decimals
   * @param asset the address of the reserve
   * @param decimals the new number of decimals
   **/
  function setReserveDecimals(address asset, uint256 decimals) external onlyLendingPoolManager {
    ReserveConfiguration.Map memory currentConfig = pool.getConfiguration(asset);

    currentConfig.setDecimals(decimals);

    pool.setConfiguration(asset, currentConfig.data);

    emit ReserveDecimalsChanged(asset, decimals);
  }

  /**
   * @dev sets the interest rate strategy of a reserve
   * @param asset the address of the reserve
   * @param rateStrategyAddress the new address of the interest strategy contract
   **/
  function setReserveInterestRateStrategyAddress(address asset, address rateStrategyAddress)
    external
    onlyLendingPoolManager
  {
    pool.setReserveInterestRateStrategyAddress(asset, rateStrategyAddress);
    emit ReserveInterestRateStrategyChanged(asset, rateStrategyAddress);
  }

  /**
   * @dev initializes a token with a proxy and a specific implementation
   * @param implementation the address of the implementation
   * @param decimals the decimals of the token
   **/
  function _initTokenWithProxy(address implementation, uint8 decimals) internal returns (address) {
    InitializableAdminUpgradeabilityProxy proxy = new InitializableAdminUpgradeabilityProxy();

    bytes memory params = abi.encodeWithSignature(
      'initialize(uint8,string,string)',
      decimals,
      IERC20Detailed(implementation).name(),
      IERC20Detailed(implementation).symbol()
    );

    proxy.initialize(implementation, address(this), params);

    return address(proxy);
  }

  function _upgradeTokenImplementation(
    address asset,
    address proxyAddress,
    address implementation
  ) internal {
    InitializableAdminUpgradeabilityProxy proxy = InitializableAdminUpgradeabilityProxy(
      payable(proxyAddress)
    );

    (uint256 decimals, , , , , , , , , ) = pool.getReserveConfigurationData(asset);

    bytes memory params = abi.encodeWithSignature(
      'initialize(uint8,string,string)',
      uint8(decimals),
      IERC20Detailed(implementation).name(),
      IERC20Detailed(implementation).symbol()
    );

    proxy.upgradeToAndCall(implementation, params);
  }
}