contract Borrow is Ownable, ReentrancyGuard {
	using SafeMath for uint256;
    using SafeERC20 for IERC20;
    using Address for address;

    // Interface of the Balancer Pool
    IBPool public bPool;
    // Interface of Balancer BAL Token
    IERC20 public balToken; // mainnet 0xba100000625a3754423978a60c9317c58a424e3D;
    // Interface of the Token contract accepts
    IERC20 public inToken;
    // Contract address of the Token contract mints and sends out to compensate losses
    IToken public outToken;
    // Address where fees will be sent
    address public feeAddress;
    // Contract address where extra profit will be sent
    address public workerAddress;
    // Interface of Exchange contract address where BAL tokens will be sent for exchange
    IExchange public exchange;
    // Hold time for withdrawal in seconds
    uint256 public holdTime;
    // Max loss coefficient contract will compensate to user in ppm, cannot be lower 100 000 (10%)
    uint256 public lossCoefficient;
    // Max profit coefficient contract will pay to user in ppm, cannot be lower 100 000 (10%)
    uint256 public profitCoefficient;
    // Fee from profit coefficient in ppm cannot be higher 20 000 (10%)
    uint256 public feeCoefficient;

    // Struct to store user balances and time last deposited
    struct UserData {
        //In Token Balance
        uint256 inBalance;
        //BPT Token Balance
        uint256 bpBalance;
        //Last time deposited
        uint256 lastTime;
    }
    mapping (address => UserData) private userData;
    // Total In Token balance locked in contract
    uint256 private inBalance;

    event Received(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event FeePaid(address indexed user, uint256 amount);
    
    constructor (
        address _bPool,
        address _balToken,
    	address _inToken,
    	address _outToken,
        address _feeAddress,        
    	address _workerAddress,
        address _exchangeAddress,
    	uint256 _holdDays,
    	uint256 _lossCoefficient,
    	uint256 _profitCoefficient,
    	uint256 _feeCoefficient
    )
    	public
    {
    	setBPool(_bPool);
        setBalToken(_balToken);
        setInToken(_inToken);
    	outToken = IToken(_outToken);
    	feeAddress = _feeAddress;
        setWorker(_workerAddress);
        setExchange(_exchangeAddress);
    	setHoldTime(_holdDays);
    	setLossCoefficient(_lossCoefficient);
    	setProfitCoefficient(_profitCoefficient);
    	setFeeCoefficient(_feeCoefficient);
    }

	/***************************************
                    ADMIN
    ****************************************/
    
    /** 
    * @dev Sets Balancer pool contract address
    * @param _bPool - Pool contract address
    */
    function setBPool (
        address _bPool
    )
        internal
    {
        require(_bPool.isContract(), "bPool must be a contract");
        bPool = IBPool(_bPool);
        require(bPool.isFinalized(), "pool is not finalized");

    }

    /** 
    * @dev Sets BAL Token contract address
    * @param _balToken - BAL Token contract address
    */
    function setBalToken (
        address _balToken
    )
        internal
    {
        require(_balToken.isContract(), "balToken must be a contract");
        balToken = IERC20(_balToken);
    }

    /** 
    * @dev Sets In Token contract address, checks it is binded to Balancer Pool
    * @param _inToken - Pool contract address
    */
    function setInToken (
        address _inToken
    )
        internal
    {
        require(_inToken.isContract(), "inToken must be a contract");
        require(bPool.isBound(_inToken), "token is not bound to pool");
        inToken = IERC20(_inToken);
    }

    /** 
    * @dev Sets Worker contract address
	* @param _workerAddress - New worker contract address
	*/
    function setWorker (
    	address _workerAddress
    )
    	public
    	onlyOwner
    {
    	require(_workerAddress.isContract(), "workerAddress must be a contract");
    	workerAddress = _workerAddress;
    }

    /** 
    * @dev Sets Exchange contract address
    * @param _exchangeAddress - New exchange contract address
    */
    function setExchange (
        address _exchangeAddress
    )
        public
        onlyOwner
    {
        require(_exchangeAddress.isContract(), "exchangeAddress address must be a contract");
        exchange = IExchange(_exchangeAddress);
    }

    /** 
    * @dev Sets Hold time
    * @param _holdDays - Hold time in days
    */
    function setHoldTime (
        uint256 _holdDays
    )
        public
        onlyOwner
    {
        holdTime = _holdDays.mul(1 days);
    }

    /** 
    * @dev Sets Loss Coefficient
	* @param _coefficient - New loss coefficient
	*/
    function setLossCoefficient (
    	uint256 _coefficient
    )
    	public
    	onlyOwner
    {
    	require(_coefficient >= 1e5, "loss coefficient must be higher 100 000");
    	lossCoefficient = _coefficient;
    }    

    /** 
    * @dev Sets Profit Coefficient
	* @param _coefficient - New profit coefficient
	*/
    function setProfitCoefficient (
    	uint256 _coefficient
    )
    	public
    	onlyOwner
    {
    	require(_coefficient >= 1e5, "profit coefficient must be higher 100 000");
    	profitCoefficient = _coefficient;
    } 

    /** 
    * @dev Sets Fee Coefficient
	* @param _coefficient - New fee coefficient
	*/
    function setFeeCoefficient (
    	uint256 _coefficient
    )
    	public
    	onlyOwner
    {
    	require(_coefficient <= 2e4, "fee coefficient must be lower 20 000");
    	feeCoefficient = _coefficient;
    } 

    /***************************************
                    ACTIONS
    ****************************************/

    /**
     * @dev Transfers BAL Tokens to exchange contract
     */
    function withdrawBal ()
        external
    {
        uint256 _amount = balToken.balanceOf(address(this));
        require(_amount != 0, 'BAL balance is 0');
        balToken.approve(address(exchange), _amount);
        exchange.exchangeBal(_amount);
    }

    
    /**
     * @dev Transfers In Tokens
     * @param _inAmount of In Tokens
     */
    function receiveInToken (uint256 _inAmount)
        external
        nonReentrant
        returns (uint256 bpAmount)
    {
        require(_inAmount != 0, "in amount is 0");
        address _address = msg.sender;
        UserData storage _user = userData[_address];
        inToken.safeTransferFrom(_address, address(this), _inAmount);
        inToken.approve(address(bPool), _inAmount);
        bpAmount = bPool.joinswapExternAmountIn(
            address(inToken),
            _inAmount,
            1
        );
        inBalance = inBalance.add(_inAmount);
        _user.inBalance = _user.inBalance.add(_inAmount);
        _user.bpBalance = _user.bpBalance.add(bpAmount);
        _user.lastTime = block.timestamp;
        emit Received(_address, _inAmount);
        return bpAmount;
    }

    /**
     * @dev Withdraw
     * @param _bpAmount of BPT Tokens
     */
     function withdraw(uint256 _bpAmount)
        external
        nonReentrant
        returns (uint256 amount, uint256 _compensatedAmount)
    {
        require(_bpAmount != 0, "in amount is 0");
        address _address = msg.sender;
        UserData storage _user = userData[_address];
        require(_user.bpBalance >= _bpAmount, "not enough balance");
        require(block.timestamp.sub(_user.lastTime) >= holdTime, 'cannot withdraw, tokens on hold');
        uint256 _inAmount = _user.inBalance.mul(_bpAmount).div(_user.bpBalance);
        inBalance = inBalance.sub(_inAmount);
        _user.inBalance = _user.inBalance.sub(_inAmount);
        _user.bpBalance = _user.bpBalance.sub(_bpAmount); 
        amount = bPool.exitswapPoolAmountIn(
            address(inToken),
            _bpAmount,
            1
        );
        uint256 _fee; uint256 _feeWorker;
        (
            amount, 
            _compensatedAmount, 
            _fee, 
            _feeWorker
        ) = calcChange(_inAmount, amount);
        inToken.safeTransfer(_address, amount);
        if(_compensatedAmount != 0)
            outToken.mint(_address, _compensatedAmount);
        if(_fee != 0){
            inToken.safeTransfer(feeAddress, _fee);
            emit FeePaid(feeAddress, _fee);
        }
        if(_feeWorker != 0){
            inToken.safeTransfer(workerAddress, _feeWorker);
            emit FeePaid(workerAddress, _feeWorker);
        }
        emit Withdrawn(_address, _inAmount);
        return (amount, _compensatedAmount);
    }
    

    /***************************************
                    GETTERS
    ****************************************/
    
    /**
     * @dev Returns contract's total balance of BAL Token
     */
    function getBalanceBal()
        external
        view
        returns (uint256)
    {
        return balToken.balanceOf(address(this));
    }

    /**
     * @dev Returns total balance of In Token locked into contract
     */
    function getBalanceIn()
        external
        view
        returns (uint256)
    {
        return inBalance;
    }

    /**
     * @dev Returns contract's total balance of BPT Token
     */
    function getBalanceBp()
        external
        view
        returns (uint256)
    {
        return bPool.balanceOf(address(this));
    }

    /**
     * @dev Returns balance of In Token of given user address
     * @param _address address of the user
     */
    function getBalanceInOf(address _address)
        external
        view
        returns (uint256)
    {
        return userData[_address].inBalance;
    }

    /**
     * @dev Returns balance of BPT Token of given user address
     * @param _address address of the user
     */
    function getBalanceBpOf(address _address)
        external
        view
        returns (uint256)
    {
        return userData[_address].bpBalance;
    }

    /**
     * @dev Returns true if hold is active for given user address
     * @param _address address of the user
     */
    function isOnHold(address _address)
        external
        view
        returns (bool)
    {
        if(userData[_address].lastTime == 0) return false;
        return block.timestamp.sub(userData[_address].lastTime) <= holdTime;
    }

    /**
     * @dev Calculates returned In, Compensation Tokens values and fees
     * @param _inAmount initial investment of In Tokens
     * @param _amount returned amount from Balancer
     */
    function calcChange(uint256 _inAmount, uint256 _amount)
        public
        view
        returns (
            uint256 returnAmount, 
            uint256 compensatedAmount, 
            uint256 fee, 
            uint256 feeWorker
        )
    {
        uint256 _change; uint256 _maxChange;
        if(_inAmount > _amount){
            _change = _inAmount.sub(_amount);
            _maxChange = _inAmount.mul(lossCoefficient).div(1e6);
            if(_change > _maxChange){
                compensatedAmount = _maxChange;
            }else{
                compensatedAmount = _change;
            }
            returnAmount = _amount;
        }else{
            _change = _amount.sub(_inAmount);
            _maxChange = _change.mul(profitCoefficient).div(1e6);
            returnAmount = _inAmount.add(_maxChange);
            fee = _change.sub(_maxChange).mul(feeCoefficient).div(1e6);
            feeWorker = _change.sub(_maxChange).sub(fee);
        }
        return (returnAmount, compensatedAmount, fee, feeWorker);
    }

    /**
     * @dev Calculates returned In, Compensation Tokens values for user
     * @param _address User address
     * @param _bpAmount Amount of BPT Tokens
     */
    function calcReturn(address _address, uint256 _bpAmount)
        external
        view
        returns (
            uint256 returnAmount, 
            uint256 compensatedAmount
        )
    {
        UserData memory _user = userData[_address];
        require(_bpAmount != 0, "in amount is 0");
        require(_user.bpBalance >= _bpAmount, "not enough balance");
        uint256 _inAmount = _user.inBalance.mul(_bpAmount).div(_user.bpBalance);
        uint256 _amount = calcInPoolBp(_inAmount);
        (
            returnAmount, 
            compensatedAmount, 
            ,
        ) = calcChange(_inAmount, _amount);
        return (returnAmount, compensatedAmount);
    }

    /**
     * @dev Calculates returned BPT Tokens from Pool given In Token amount
     * @param _inAmount Amount of In Tokens
     */
    function calcBpPoolIn(uint256 _inAmount)
        external
        view
        returns (uint256)
    {
        return bPool.calcPoolOutGivenSingleIn(
            bPool.getBalance(address(inToken)),
            bPool.getDenormalizedWeight(address(inToken)),
            bPool.totalSupply(),
            bPool.getTotalDenormalizedWeight(),
            _inAmount,
            bPool.getSwapFee()
        );
    }

    /**
     * @dev Calculates returned In Tokens from Pool given BPT Token amount
     * @param _bpAmount Amount of BPT Tokens
     */
    function calcInPoolBp(uint256 _bpAmount)
        public
        view
        returns (uint256)
    {
        return bPool.calcSingleOutGivenPoolIn(
            bPool.getBalance(address(inToken)),
            bPool.getDenormalizedWeight(address(inToken)),
            bPool.totalSupply(),
            bPool.getTotalDenormalizedWeight(),
            _bpAmount,
            bPool.getSwapFee()
        );
    }
}
// 
// Libs
// Used contracts
//Balancer
// Internal
function getBalanceInOf(address _address)
    external
    view
    returns (uint256)
{
    return userData[_address].inBalance;
}

/**
 * @dev Returns balance of In Token of given user address
 * @param _address address of the user
 */