// File: src/BondDepositoryV3.sol



// SPDX-License-Identifier: MIT

import {IKotoV3} from "./interfaces/IKotoV3.sol";
import {IUniswapV2Router02} from "./interfaces/IUniswapV2Router02.sol";
import {IUniswapV2Factory} from "./interfaces/IUniswapv2Factory.sol";

pragma solidity 0.8.23;

interface IERC20Minimal {
    function transfer(address to, uint256 value) external returns (bool);
    function balanceOf(address) external view returns (uint256);
}

contract BondDepositoryV3 {
    IUniswapV2Factory public constant FACTORY = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
    address public constant OWNER = 0x946eF43867225695E29241813A8F41519634B36b;
    address public constant UNISWAP_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
    address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    uint256 public constant TIMELOCK_INTERVAL = 86400 * 3; // 3 day timelock
    uint256 public execution = type(uint256).max;
    ///@dev set timelock to max so that it can not be called
    address public koto;

    modifier onlyOwner() {
        if (msg.sender != OWNER) revert OnlyOwner();
        _;
    }

    constructor() {}

    ///@notice bond ETH held within the depository for Koto Tokens
    ///@param _value the ether value to send to the Koto Contract.
    function bond(uint256 _value) external onlyOwner {
        uint256 amountOut = IKotoV3(koto).bond{value: _value}();
        emit DepositoryBond(_value, amountOut);
    }

    ///@notice redeem Koto tokens held within the depository for ETH
    ///@param _amount the amount of Koto tokens to redeem
    function redeem(uint256 _amount) external onlyOwner {
        uint256 ethAmount = IKotoV3(koto).redeem(_amount);
        emit DepositoryRedeem(_amount, ethAmount);
    }

    ///@notice burn additional Koto tokens held within the depository without redeeming them
    ///@param _amount the amount of Koto tokens to burn.
    function burn(uint256 _amount) external onlyOwner {
        IKotoV3(koto).burn(_amount);
        emit DepositoryBurn(_amount);
    }

    ///@notice swap eth for koto or koto for eth
    ///@param _value the amount of either eth or koto to swap
    ///@param zeroForOne if you are swaping eth or koto
    ///@dev when zeroForOne is true koto is being swapped to eth
    function swap(uint256 _value, bool zeroForOne, uint256 _minAmount) external onlyOwner {
        if (zeroForOne) {
            address[] memory path = new address[](2);
            path[0] = koto;
            path[1] = WETH;
            IUniswapV2Router02(UNISWAP_ROUTER).swapExactTokensForETHSupportingFeeOnTransferTokens(
                _value, _minAmount, path, address(this), block.timestamp
            );
            emit DepositorySell(_value);
        } else {
            address[] memory path = new address[](2);
            path[0] = WETH;
            path[1] = koto;
            IUniswapV2Router02(UNISWAP_ROUTER).swapExactETHForTokensSupportingFeeOnTransferTokens{value: _value}(
                _minAmount, path, address(this), block.timestamp
            );
            emit DepositoryBuy(_value);
        }
    }

    ///@notice send reward tokens to the Voter contract in order to being reward distribution.
    function reward(uint256 amount, address _to) external onlyOwner {
        if (msg.sender != OWNER) revert OnlyOwner();
        if (amount > IKotoV3(koto).allowance(address(this), _to)) {
            IKotoV3(koto).approve(_to, type(uint256).max);
        }
        assembly {
            let ptr := mload(0x40)
            ///@dev notifyRewardAmount function sig.
            mstore(ptr, 0x3c6b16ab00000000000000000000000000000000000000000000000000000000)
            mstore(add(ptr, 4), amount)
            let success := call(gas(), _to, 0, ptr, 36, 0, 0)
            if iszero(success) { revert(0, 0) }
        }
    }

    ///@notice set the koto contract address
    function set(address _koto) external onlyOwner {
        koto = _koto;
        IKotoV3(_koto).approve(_koto, type(uint256).max);
        IKotoV3(_koto).approve(UNISWAP_ROUTER, type(uint256).max);
        emit Set(_koto);
    }

    /// @notice create the markets for the eth and lp bonds
    /// @param ethBondAmount the amount of koto tokens to set aside for Eth bonds
    /// @param lpBondAmount the amount of koto tokens to set aside for LP Bonds
    function deposit(uint256 ethBondAmount, uint256 lpBondAmount) external onlyOwner {
        address _koto = koto;
        IKotoV3(_koto).create(ethBondAmount, lpBondAmount);
        emit DepositoryDeposit(_koto, ethBondAmount + lpBondAmount, ethBondAmount, lpBondAmount);
    }

    ///@notice begin timelock function if migrating liquidity is needed
    function start() external onlyOwner {
        execution = block.timestamp + TIMELOCK_INTERVAL;
        emit TimelockStart(block.timestamp, execution);
    }

    ///@notice in a emergency withdraw the liquidity pool tokens after the timelock has passed
    ///@param to the address to send the LP tokens to.
    function emergencyWithdraw(address to) external onlyOwner {
        if (block.timestamp < execution) revert Timelock();
        address _pair = FACTORY.getPair(koto, WETH);
        uint256 _balance = IERC20Minimal(_pair).balanceOf(address(this));
        IERC20Minimal(_pair).transfer(to, _balance);
        execution = type(uint256).max;
        emit LiquidityMigration(to, _balance);
    }

    event DepositoryBond(uint256 ethAmountIn, uint256 kotoAmountOut);
    event DepositoryBurn(uint256 kotoBurnAmount);
    event DepositoryBuy(uint256 ethAmountIn);
    event DepositoryDeposit(address indexed token, uint256 kotoIn, uint256 ethCapacity, uint256 lpCapacity);
    event DepositoryRedeem(uint256 kotoAmountIn, uint256 ethAmountOut);
    event DepositorySell(uint256 kotoAmountOut);
    event LiquidityMigration(address indexed receiver, uint256 amount);
    event Set(address indexed token);
    event TimelockStart(uint256 initializedTime, uint256 executionTime);

    error OnlyOwner();
    error Timelock();
    error TransferFailed();

    receive() external payable {}
}

// File: src/interfaces/IKotoV3.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import {PricingLibrary} from "../PricingLibrary.sol";

interface IKotoV3 {
    // ==================== EXTERNAL FUNCTIONS ===================== \\

    function transfer(address _to, uint256 _value) external returns (bool success);

    function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);

    function approve(address _spender, uint256 _value) external returns (bool success);

    ///@notice exchange ETH for Koto tokens at the current bonding price
    ///@dev bonds are set on 1 day intervals with 4 hour deposit intervals and 30 minute tune intervals.
    function bond() external payable returns (uint256 payout);

    function bondLp(uint256 _lpAmount) external returns (uint256 payout);

    ///@notice burn Koto tokens in exchange for a piece of the underlying reserves
    ///@param amount The amount of Koto tokens to redeem
    ///@return payout The amount of ETH received in exchange for the Koto tokens
    function redeem(uint256 amount) external returns (uint256 payout);

    ///@notice burn Koto tokens, without redemption
    ///@param amount the amount of Koto to burn
    function burn(uint256 amount) external returns (bool success);

    // ==================== EXTERNAL VIEW FUNCTIONS ===================== \\

    ///@notice get the tokens name
    function name() external pure returns (string memory);

    ///@notice get the tokens symbol
    function symbol() external pure returns (string memory);

    ///@notice get the tokens decimals
    function decimals() external pure returns (uint8);

    ///@notice get the tokens total supply
    function totalSupply() external view returns (uint256);

    ///@notice get the current balance of a user
    ///@param _owner the user whos balance you want to check
    function balanceOf(address _owner) external view returns (uint256);

    ///@notice get current approved amount for transfer from another party
    ///@param owner the current owner of the tokens
    ///@param spender the user who has approval (or not) to spend the owners tokens
    function allowance(address owner, address spender) external view returns (uint256);

    ///@notice return the Uniswap V2 Pair address
    function pool() external view returns (address);

    ///@notice get the owner of the contract
    ///@dev ownership is nontransferable and limited to opening trade, exclusion / inclusion,s and increasing liquidity
    function ownership() external pure returns (address);

    ///@notice the current price a bond
    function bondPrice() external view returns (uint256);

    function bondPriceLp() external view returns (uint256);

    ///@notice return the current redemption price for 1 uint of Koto.
    function redemptionPrice() external view returns (uint256);

    function marketInfo()
        external
        view
        returns (PricingLibrary.Market memory, PricingLibrary.Term memory, PricingLibrary.Data memory);

    function lpMarketInfo()
        external
        view
        returns (PricingLibrary.Market memory, PricingLibrary.Term memory, PricingLibrary.Data memory);

    function depository() external pure returns (address);

    function create(uint256 ethBondAmount, uint256 lpBondAmount) external;

    // ========================= EVENTS ========================= \\

    event AmmAdded(address poolAdded);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
    event Bond(address indexed buyer, uint256 amount, uint256 bondPrice);
    event CreateMarket(uint256 bonds, uint256 start, uint48 end);
    event IncreaseLiquidity(uint256 kotoAdded, uint256 ethAdded);
    event Launched(uint256 time);
    event LimitsRemoved(uint256 time);
    event OpenBondMarket(uint256 openingTime);
    event Redeem(address indexed sender, uint256 burned, uint256 payout, uint256 floorPrice);
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event UserExcluded(address indexed userToExclude);

    // ========================= ERRORS ========================= \\

    error AlreadyLaunched();
    error BondFailed();
    error InsufficentAllowance();
    error InsufficentBalance();
    error InsufficentBondsAvailable();
    error InvalidSender();
    error InvalidTransfer();
    error LimitsReached();
    error MarketClosed();
    error MaxPayout();
    error OngoingBonds();
    error OnlyOwner();
    error RedeemFailed();
    error Reentrancy();
}

// File: src/interfaces/IUniswapV2Router02.sol



//SPDX-License-Identifier: UNLICENSED

pragma solidity >=0.6.2;

interface IUniswapV2Router02 {
    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external returns (uint256 amountETH);
    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
        address token,
        uint256 liquidity,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline,
        bool approveMax,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external returns (uint256 amountETH);

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;
    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external payable;
    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external;

    function addLiquidityETH(
        address token,
        uint256 amountTokenDesired,
        uint256 amountTokenMin,
        uint256 amountETHMin,
        address to,
        uint256 deadline
    ) external payable returns (uint256 amountToken, uint256 amountETH, uint256 liquidity);
}

// File: src/interfaces/IUniswapv2Factory.sol



//SPDX-License-Identifier: MIT

pragma solidity >=0.6.2;

interface IUniswapV2Factory {
    function getPair(address tokenA, address tokenB) external view returns (address pair);
}

// File: src/PricingLibrary.sol



// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

library PricingLibrary {
    // 1 Slot
    struct Data {
        uint48 lastTune;
        uint48 lastDecay; // last timestamp when market was created and debt was decayed
        uint48 length; // time from creation to conclusion. used as speed to decay debt.
        uint48 depositInterval; // target frequency of deposits
        uint48 tuneInterval; // frequency of tuning
    }

    // 2 Storage slots
    struct Market {
        uint96 capacity; // capacity remaining
        uint96 totalDebt; // total debt from market
        uint96 maxPayout; // max tokens in/out
        uint96 sold; // Koto out
        uint96 purchased; // Eth in
    }

    // 1 Storage Slot
    struct Adjustment {
        uint128 change;
        uint48 lastAdjustment;
        uint48 timeToAdjusted;
        bool active;
    }

    // 2 Storage slots
    struct Term {
        uint48 conclusion; // timestamp when the current market will end
        uint256 controlVariable; // scaling variable for price
    }

    function decay(Data memory data, Market memory market, Term memory terms, Adjustment memory adjustments)
        internal
        view
        returns (Market memory, Data memory, Term memory, Adjustment memory)
    {
        uint48 time = uint48(block.timestamp);
        market.totalDebt -= debtDecay(data, market);
        data.lastDecay = time;

        if (adjustments.active) {
            (uint128 adjustby, uint48 dt, bool stillActive) = controlDecay(adjustments);
            terms.controlVariable -= adjustby;
            if (stillActive) {
                adjustments.change -= adjustby;
                adjustments.timeToAdjusted -= dt;
                adjustments.lastAdjustment = time;
            } else {
                adjustments.active = false;
            }
        }
        return (market, data, terms, adjustments);
    }

    function controlDecay(Adjustment memory info) internal view returns (uint128, uint48, bool) {
        if (!info.active) return (0, 0, false);

        uint48 secondsSince = uint48(block.timestamp) - info.lastAdjustment;
        bool active = secondsSince < info.timeToAdjusted;
        uint128 _decay = active ? (info.change * secondsSince) / info.timeToAdjusted : info.change;
        return (_decay, secondsSince, active);
    }

    function marketPrice(uint256 _controlVariable, uint256 _totalDebt, uint256 _totalSupply)
        internal
        pure
        returns (uint256)
    {
        return ((_controlVariable * debtRatio(_totalDebt, _totalSupply)) / 1e18);
    }

    function debtRatio(uint256 _totalDebt, uint256 _totalSupply) internal pure returns (uint256) {
        return ((_totalDebt * 1e18) / _totalSupply);
    }

    function debtDecay(Data memory data, Market memory market) internal view returns (uint64) {
        uint256 secondsSince = block.timestamp - data.lastDecay;
        return uint64((market.totalDebt * secondsSince) / data.length);
    }

    struct TuneCache {
        uint256 remaining;
        uint256 price;
        uint256 capacity;
        uint256 targetDebt;
        uint256 ncv;
    }

    function tune(
        uint48 time,
        Market memory market,
        Term memory term,
        Data memory data,
        Adjustment memory adjustment,
        uint256 _totalSupply
    ) internal pure returns (Market memory, Term memory, Data memory, Adjustment memory) {
        TuneCache memory cache;
        if (time >= data.lastTune + data.tuneInterval) {
            cache.remaining = term.conclusion - time;
            cache.price = marketPrice(term.controlVariable, market.totalDebt, _totalSupply);
            cache.capacity = market.capacity;
            market.maxPayout = uint96((cache.capacity * data.depositInterval / cache.remaining));
            cache.targetDebt = cache.capacity * data.length / cache.remaining;
            cache.ncv = (cache.price * _totalSupply) / cache.targetDebt;

            if (cache.ncv < term.controlVariable) {
                uint128 change = uint128(term.controlVariable - cache.ncv);
                adjustment = Adjustment(change, time, data.tuneInterval, true);
            } else {
                term.controlVariable = cache.ncv;
            }
            data.lastTune = time;
        }
        return (market, term, data, adjustment);
    }
}