// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/interfaces/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

import "./interfaces/ITreasury.sol";
import "./interfaces/IPegStabilityModule.sol";
import "./interfaces/IERC20BurnableMintable.sol";

/// @title PegStabilityModule
/// @author Bluejay Core Team
/// @notice PegStabilityModule allow stablecoins pegged to bluStables to be used as collaterals to mint
/// bluStables. It's purpose is to create a 0 slippage swap between the external stablecoin and bluStable.
contract PegStabilityModule is Ownable, ReentrancyGuard, IPegStabilityModule {
  using SafeERC20 for IERC20BurnableMintable;
  using SafeERC20 for IERC20;

  uint256 private constant WAD = 10**18;

  /// @notice Contract address of the treasury
  ITreasury public immutable override treasury;

  /// @notice External stablecoin that is pegged and can be swapped to the bluStablecoin
  IERC20 public immutable override externalStablecoin;

  /// @notice BluStablecoin that can be issued and redeemed against the externalStablecoin
  IERC20BurnableMintable public immutable override bluStablecoin;

  /// @notice Fixed ratio of the number of externalStablecoin per bluStablecoin
  uint256 public immutable override ratio;

  /// @notice Fees when swapping from external stablecoin to target bluStablecoin, in WAD
  uint256 public override feesIn;

  /// @notice Fees when swapping from target bluStablecoin to external stablecoin, in WAD
  uint256 public override feesOut;

  /// @notice Total amount of bluStables that is allowed to be minted by this PSM
  uint256 public override debtCeiling;

  /// @notice Total amount of bluStables that is currently minted by this PSM
  uint256 public override totalDebt;

  /// @notice Constructor to initialize the contract
  /// @param _treasury Contract address of the treasury
  /// @param _externalStablecoin Contract address of external stablecoin that is pegged
  /// @param _bluStablecoin Contract address of bluStablecoin that can be issued and redeemed
  /// @param _feesIn Fees when swapping from external stablecoin to target bluStablecoin, in WAD
  /// @param _feesOut Fees when swapping from target bluStablecoin to external stablecoin, in WAD
  /// @param _ratio Fixed ratio of the number of externalStablecoin per bluStablecoin, in WAD
  /// @param _debtCeiling Total amount of bluStables that is allowed to be minted by this PSM
  /// @dev Use 1e6 for ratio if externalStablecoin is using 6 decimals and 1e18 if externalStablecoin is using 18 decimals
  /// This assumes that bluStablecoin has 18 decimals
  constructor(
    address _treasury,
    address _externalStablecoin,
    address _bluStablecoin,
    uint256 _feesIn,
    uint256 _feesOut,
    uint256 _ratio,
    uint256 _debtCeiling
  ) {
    treasury = ITreasury(_treasury);
    externalStablecoin = IERC20(_externalStablecoin);
    bluStablecoin = IERC20BurnableMintable(_bluStablecoin);
    feesIn = _feesIn;
    feesOut = _feesOut;
    ratio = _ratio;
    debtCeiling = _debtCeiling;
  }

  // =============================== PUBLIC FUNCTIONS =================================

  /// @notice Swap between external stablecoin and bluStablecoin
  /// @dev This low-level function should be called from a contract which performs important safety checks
  /// @param recipient Address to send output to
  /// @param toBluStablecoin If the swap direction is from external stablecoin to bluStable
  function swap(address recipient, bool toBluStablecoin)
    public
    override
    nonReentrant
    returns (uint256 amountOut)
  {
    if (toBluStablecoin) {
      uint256 input = externalStablecoin.balanceOf(address(this));
      amountOut = getBluStablecoinsOut(input);
      totalDebt += amountOut;
      require(totalDebt <= debtCeiling, "Exceeded debt ceiling");
      externalStablecoin.safeTransfer(address(treasury), input);
      bluStablecoin.mint(recipient, amountOut);
      emit Swap(recipient, toBluStablecoin, input, amountOut);
    } else {
      uint256 input = bluStablecoin.balanceOf(address(this));
      amountOut = getExternalStablecoinsOut(input);
      require(input <= totalDebt, "Insufficient external stablecoin");
      unchecked {
        totalDebt -= input;
      }
      treasury.withdraw(address(externalStablecoin), recipient, amountOut);
      bluStablecoin.burn(input);
      emit Swap(recipient, toBluStablecoin, input, amountOut);
    }
  }

  // =============================== ADMIN FUNCTIONS =================================

  /// @notice Set fees when swapping from external stablecoin to target bluStablecoin
  /// @param _feesIn Fees, in WAD
  function setFeesIn(uint256 _feesIn) public override onlyOwner {
    feesIn = _feesIn;
    emit UpdatedFeesIn(_feesIn);
  }

  /// @notice Set fees when swapping from target bluStablecoin to external stablecoin
  /// @param _feesOut Fees, in WAD
  function setFeesOut(uint256 _feesOut) public override onlyOwner {
    feesOut = _feesOut;
    emit UpdatedFeesOut(_feesOut);
  }

  /// @notice Allow other tokens sent erroneously to this contract to be withdrawn
  /// @param token Address of the token to withdraw
  /// @param amount Amount of token to withdraw
  function rescueFunds(IERC20 token, uint256 amount) public override onlyOwner {
    token.safeTransfer(msg.sender, amount);
  }

  /// @notice Increase the amount of bluStables that can be minted by this PSM
  /// @param amount Amount to increase, in WAD
  function increaseDebtCeiling(uint256 amount) public override onlyOwner {
    debtCeiling += amount;
    emit UpdatedDebtCeiling(debtCeiling);
  }

  /// @notice Decrease the amount of bluStables that can be minted by this PSM
  /// @dev Cannot be used to decrease debtCeiling below totalDebt
  /// @param amount Amount to decrease, in WAD
  function decreaseDebtCeiling(uint256 amount) public override onlyOwner {
    uint256 allowance = debtCeiling - totalDebt;
    debtCeiling -= (allowance > amount) ? amount : allowance;
    emit UpdatedDebtCeiling(debtCeiling);
  }

  /// @notice Set the amount of bluStables that can be minted by this PSM
  /// @dev Cannot be used to set debtCeiling below totalDebt
  /// @param amount Amount to set, in WAD
  function setDebtCeiling(uint256 amount) public override onlyOwner {
    require(totalDebt <= amount, "Less than totalDebt");
    debtCeiling = amount;
    emit UpdatedDebtCeiling(debtCeiling);
  }

  // =============================== VIEW FUNCTIONS =================================

  /// @notice Calculate amount of bluStablecoins given an exact input of external stablecoin
  /// @param amountIn Amount of external stablecoin to swap
  function getBluStablecoinsOut(uint256 amountIn)
    public
    view
    override
    returns (uint256 amountOut)
  {
    amountOut = ((amountIn * (WAD - feesIn)) / ratio);
  }

  /// @notice Calculate amount of external stablecoins required given an exact output of bluStablecoin
  /// @param amountOut Amount of bluStablecoin output expected
  function getExternalStablecoinsIn(uint256 amountOut)
    public
    view
    override
    returns (uint256 amountIn)
  {
    amountIn = (amountOut * ratio) / (WAD - feesIn) + 1;
  }

  /// @notice Calculate amount of external stablecoins given an exact input of bluStablecoin
  /// @param amountIn Amount of bluStablecoin to swap
  function getExternalStablecoinsOut(uint256 amountIn)
    public
    view
    override
    returns (uint256 amountOut)
  {
    amountOut = ((amountIn * (WAD - feesOut) * ratio) / (WAD * WAD));
  }

  /// @notice Calculate amount of bluStablecoins required given an exact output of external stablecoin
  /// @param amountOut Amount of external stablecoin output expected
  function getBluStablecoinsIn(uint256 amountOut)
    public
    view
    override
    returns (uint256 amountIn)
  {
    amountIn = (amountOut * WAD * WAD) / ((WAD - feesOut) * ratio) + 1;
  }
}
[{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_externalStablecoin","type":"address"},{"internalType":"address","name":"_bluStablecoin","type":"address"},{"internalType":"uint256","name":"_feesIn","type":"uint256"},{"internalType":"uint256","name":"_feesOut","type":"uint256"},{"internalType":"uint256","name":"_ratio","type":"uint256"},{"internalType":"uint256","name":"_debtCeiling","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"bool","name":"toBluStablecoin","type":"bool"},{"indexed":false,"internalType":"uint256","name":"amountIn","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"Swap","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"debtCeiling","type":"uint256"}],"name":"UpdatedDebtCeiling","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"feesIn","type":"uint256"}],"name":"UpdatedFeesIn","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"feesOut","type":"uint256"}],"name":"UpdatedFeesOut","type":"event"},{"inputs":[],"name":"bluStablecoin","outputs":[{"internalType":"contract IERC20BurnableMintable","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"debtCeiling","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"decreaseDebtCeiling","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"externalStablecoin","outputs":[{"internalType":"contract IERC20","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feesIn","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feesOut","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"getBluStablecoinsIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"getBluStablecoinsOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"getExternalStablecoinsIn","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"getExternalStablecoinsOut","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"increaseDebtCeiling","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"ratio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"rescueFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"setDebtCeiling","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_feesIn","type":"uint256"}],"name":"setFeesIn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_feesOut","type":"uint256"}],"name":"setFeesOut","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"bool","name":"toBluStablecoin","type":"bool"}],"name":"swap","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"totalDebt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"treasury","outputs":[{"internalType":"contract ITreasury","name":"","type":"address"}],"stateMutability":"view","type":"function"}]