contract OlympusBondDepository is Ownable {

    using FixedPoint for *;
    using SafeERC20 for IERC20;
    using SafeMath for uint;

    struct Bond {
        uint valueRemaining; // value of LP given
        uint payoutRemaining; // OHM remaining to be paid
        uint lastBlock; // Last interaction
        uint vestingPeriod; // Blocks left to vest
        uint pricePaid; // In DAI, for front end viewing
    }
    mapping( address => Bond ) public bondInfo; 

    // Bond terms
    uint public controlVariable;
    uint public vestingTerm;
    uint public minimumPrice;
    uint public DAOShare; 
    uint public maxPayoutPercent; //  compared to circulating supply, in hundreths. i.e. 50 = 0.5%

    uint256 public totalDebt; // Total value of outstanding bonds

    address public immutable OHM;
    address public immutable LP;

    address public immutable treasury;
    address public immutable distributor;
    address public immutable DAO;

    address public immutable circulatingOHMContract; // calculates circulating supply
    address public immutable bondCalculator;

    constructor ( 
        address OHM_,
        address LP_,
        address treasury_, 
        address distributor_, 
        address DAO_, 
        address circulatingOHMContract_,
        address bondCalculator_
    ) {
        require( OHM_ != address(0) );
        OHM = OHM_;
        require( LP_ != address(0) );
        LP = LP_;
        require( treasury_ != address(0) );
        treasury = treasury_;
        require( distributor_ != address(0) );
        distributor = distributor_;
        require( DAO_ != address(0) );
        DAO = DAO_;
        require( circulatingOHMContract_ != address(0) );
        circulatingOHMContract = circulatingOHMContract_;
        require( bondCalculator_ != address(0) );
        bondCalculator = bondCalculator_;
    }

    /**
        @notice set parameters of new bonds
        @param controlVariable_ uint
        @param vestingTerm_ uint
        @param minPrice_ uint
        @param maxPayout_ uint
        @param DAOShare_ uint
        @return bool
     */
    function setBondTerm( 
        uint controlVariable_, 
        uint vestingTerm_, 
        uint minPrice_,
        uint maxPayout_,
        uint DAOShare_
    ) external onlyManager() returns ( bool ) {
        controlVariable = controlVariable_;
        vestingTerm = vestingTerm_;
        minimumPrice = minPrice_;
        maxPayoutPercent = maxPayout_;
        DAOShare = DAOShare_;
        return true;
    }

    /**
        @notice deposit bond
        @param amount_ uint
        @param maxPremium_ uint
        @param depositor_ address
        @return uint
     */
    function deposit( 
        uint amount_, 
        uint maxPremium_,
        address depositor_
    ) external returns ( uint ) {
        return _deposit( amount_, maxPremium_, depositor_ );
    }

    function _deposit( 
        uint amount_, 
        uint maxPrice_,
        address depositor_
    ) internal returns ( uint ) {
        require( depositor_ != address(0), "Invalid address" );
        uint price = bondPriceInDAI(); // DAI price of bond (for depositor info)

        require( maxPrice_ >= bondPrice(), "Slippage limit: more than max price" ); // slippage protection

        uint value = IBondCalculator( bondCalculator ).valuation( LP, amount_ );
        uint payout = payoutFor( value );

        require( payout >= 10000000, "Bond too small" ); // must be > 0.01 OHM
        require( payout <= maxPayout(), "Bond too large");

        // Transfer in LP
        IERC20( LP ).safeTransferFrom( msg.sender, address(this), amount_ );
        // Deposit LP to mint OHM
        IERC20( LP ).approve( address( treasury ), amount_ );
        ITreasury( treasury ).depositPrinciple( amount_ );
        
        // calculate profits
        uint daoProfit = payout.mul( DAOShare ).div( 10000 );
        // small rounding errors may occur due to improved bond calculator not used by vault
        uint padding = value.div( 10000000 ); // leaves dust to ensure against underflow
        uint profit = value.sub( payout ).sub( daoProfit ).sub( padding );
        // Transfer profits to staking distributor and dao 
        IERC20( OHM ).safeTransfer( distributor, profit );
        IERC20( OHM ).safeTransfer( DAO, daoProfit );

        totalDebt = totalDebt.add( value ); // increase total debt
        
        // Store depositor info
        bondInfo[ depositor_ ] = Bond({
            valueRemaining: bondInfo[ depositor_ ].valueRemaining.add( value ),
            payoutRemaining: bondInfo[ depositor_ ].payoutRemaining.add( payout ),
            lastBlock: block.number,
            vestingPeriod: vestingTerm,
            pricePaid: price
        });
        return payout;
    }

    /** 
        @notice redeem all unvested bonds
        @return payout_ uint
     */ 
    function redeem() external returns ( uint ) {        
        Bond memory info = bondInfo[ msg.sender ];
        uint percentVested = percentVestedFor( msg.sender );

        if ( percentVested >= 10000 ) { // if fully vested, pay full amount & clear info
            delete bondInfo[msg.sender];
            totalDebt = totalDebt.sub( info.valueRemaining );
            IERC20( OHM ).transfer( msg.sender, info.payoutRemaining );
            return info.payoutRemaining;
        } else {
            // calculate reductions from vesting
            uint value = info.valueRemaining.mul( percentVested ).div( 10000 );
            uint payout = info.payoutRemaining.mul( percentVested ).div( 10000 );
            uint blocksSinceLast = block.number.sub( info.lastBlock );

            // store updated deposit info
            bondInfo[ msg.sender ] = Bond({
                valueRemaining: info.valueRemaining.sub( value ),
                payoutRemaining: info.payoutRemaining.sub( payout ),
                lastBlock: block.number,
                vestingPeriod: info.vestingPeriod.sub( blocksSinceLast ),
                pricePaid: info.pricePaid
            });

            // reduce total debt by vested amount
            totalDebt = totalDebt.sub( value );
            // send payout
            IERC20( OHM ).transfer( msg.sender, payout );

            return payout;
        }
    }

    /**
        @notice use maxPayoutPercent to determine maximum bond size
        @return uint
     */
    function maxPayout() public view returns ( uint ) {
        uint circulatingOHM = ICirculatingOHM( circulatingOHMContract ).OHMCirculatingSupply();
        return circulatingOHM.mul( maxPayoutPercent ).div( 10000 );
    }

    /**
        @notice calculate how far into vesting a depositor is
        @param depositor_ address
        @return _percentVested uint
     */
    function percentVestedFor( address depositor_ ) public view returns ( uint _percentVested ) {
        Bond memory bond = bondInfo[ depositor_ ];
        uint blocksSinceLast = block.number.sub( bond.lastBlock );
        uint vestingPeriod = bond.vestingPeriod;

        if ( vestingPeriod > 0 ) {
            _percentVested = blocksSinceLast.mul( 10000 ).div( vestingPeriod );
        } else {
            _percentVested = 0;
        }
    }

    /**
        @notice calculate amount of OHM available for claim by depositor
        @param depositor_ address
        @return _pendingPayout uint
     */
    function pendingPayoutFor( address depositor_ ) external view returns ( uint _pendingPayout ) {
        uint percentVested = percentVestedFor( depositor_ );
        uint payoutRemaining = bondInfo[ depositor_ ].payoutRemaining;

        if ( percentVested >= 10000 ) {
            _pendingPayout = payoutRemaining;
        } else {
            _pendingPayout = payoutRemaining.mul( percentVested ).div( 10000 );
        }
    }

    /**
        @notice calculate interest due for new bond
        @param value_ uint
        @return _interestDue uint
     */
    function payoutFor( uint value_ ) public view returns ( uint ) {
        return FixedPoint.fraction( value_, bondPrice() ).decode112with18().div( 1e16 );
    }

    /**
        @notice calculate current bond premium
        @return _price uint
     */
    function bondPrice() public view returns ( uint _price ) {        
        _price = controlVariable.mul( _calcDebtRatio() ).add( 1000000000 ).div( 1e7 );
        if ( _price < minimumPrice ) {
            _price = minimumPrice;
        }
    }

    /**
        @notice calculate current bond premium without a minimum
        @return _price uint
     */
    function bondPriceWithoutFloor() external view returns ( uint _price ) {
        _price = controlVariable.mul( _calcDebtRatio() ).add( 1000000000 ).div( 1e7 );
    }

    /**
        @notice converts bond price to DAI value
        @return _price uint
     */
    function bondPriceInDAI() public view returns ( uint _price ) {
        _price = bondPrice().mul( IBondCalculator( bondCalculator ).markdown( LP ) ).div( 1e2 );
    }

    /**
        @notice calculate current debt ratio
        @return _debtRatio uint
     */
    function debtRatio() external view returns ( uint _debtRatio ) {
        _debtRatio = _calcDebtRatio();
    }

    function _calcDebtRatio() internal view returns ( uint _debtRatio ) {   
        uint supply = ICirculatingOHM( circulatingOHMContract ).OHMCirculatingSupply();
        _debtRatio = FixedPoint.fraction( 
            // Must move the decimal to the right by 9 places to avoid math underflow error
            totalDebt.mul( 1e9 ), 
            supply
        ).decode112with18().div( 1e18 );
    }

    /**
        @notice allow anyone to send lost tokens (excluding LP or OHM) to the DAO
        @return bool
     */
    function recoverLostToken( address token_ ) external returns ( bool ) {
        require( token_ != OHM );
        require( token_ != LP );
        IERC20( token_ ).safeTransfer( DAO, IERC20( token_ ).balanceOf( address(this) ) );
        return true;
    }
}
function recoverLostToken( address token_ ) external returns ( bool ) {
    require( token_ != OHM );
    require( token_ != LP );
    IERC20( token_ ).safeTransfer( DAO, IERC20( token_ ).balanceOf( address(this) ) );
    return true;
}

/**
 @notice allow anyone to send lost tokens (excluding LP or OHM) to the DAO
 @return bool
 */