contract ElasticPool is Ownable, ReentrancyGuard, ERC20 {
	using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // Contract address where extra profit will be sent
    address public workerAddress;
    // Interface of the Token contract accepts
    IERC20 public inToken;
    // Interface of Balancer Factory used to check pool authenticy
    IBFactory public bFactory;
    // Interface of main Balancer Pool used for adding liquidity with In Token
    IBPool public bPool;
    // Interface of Balancer BAL Token
    IERC20 public balToken;
    // Interface of Balancer Pool one used for Exchange BAL to In Token
    IBPool public bPoolOne;
    // Interface of Balancer Pool two used for Exchange BAL to In Token
    IBPool public bPoolTwo;
    // Interface of intermediate token for BAL to In Token Exchange
    IERC20 public bIntermediateToken;
    // Address where fees will be sent
    address public feeAddress;
    // Fee from profit coefficient in ppm cannot be higher 20 000 (2%)
    uint256 public feeCoefficient;
    // 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;
    // Hold time for withdrawal in seconds
    uint256 public holdTime;

    // 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 (
        uint8 _decimals,
        address _inToken,
        address _bFactory,
        address _bPool,
        address _balToken,
        address _feeAddress,
        uint256 _feeCoefficient,
    	uint256 _lossCoefficient,
    	uint256 _profitCoefficient,
        uint256 _holdDays
    )
    	public
        ERC20('elascticPoolToken', 'EPT')
    {
        _setupDecimals(_decimals);
        inToken = IERC20(_inToken);

        bFactory = IBFactory(_bFactory);
        require(bFactory.isBPool(_bPool), "balancer pool address is wrong");
        bPool = IBPool(_bPool);
        require(bPool.isBound(_inToken), "in token is not bound to pool");
        balToken = IERC20(_balToken);
        
    	feeAddress = _feeAddress;
        setFeeCoefficient(_feeCoefficient);
    	setLossCoefficient(_lossCoefficient);
    	setProfitCoefficient(_profitCoefficient);
        setHoldTime(_holdDays);
    }

	/***************************************
                    ADMIN
    ****************************************/

    /** 
    * @dev Sets Balancer Pools for Exchange BAL to In Tokens
	* @param _bPoolOne - first Balancer Pool address
    * @param _bPoolOne - second Balancer Pool adress
    * @param _bIntermediateToken - intermediate token address
	*/
    function setExchangePools (
        address _bPoolOne,
        address _bPoolTwo,
        address _bIntermediateToken
    )
        external
        onlyOwner
    {
        require(bFactory.isBPool(_bPoolOne), "balancer pool one address is wrong");
        bPoolOne = IBPool(_bPoolOne);
        require(bPoolOne.isBound(address(balToken)) && bPoolOne.isBound(_bIntermediateToken), 
            "BAL or intermediate token is not bound to pool one");
        bIntermediateToken = IERC20(_bIntermediateToken);
        if(address(inToken) != _bIntermediateToken){
            require(bFactory.isBPool(_bPoolTwo), "balancer pool two address is wrong");
            bPoolTwo = IBPool(_bPoolTwo);
            require(bPoolTwo.isBound(address(inToken)) && bPoolTwo.isBound(_bIntermediateToken), 
                "In token or intermediate token is not bound to pool two");
        }
    }

    /** 
    * @dev Sets Worker contract address, can be called only once
    * @param _workerAddress - New worker contract address
    */
    function setWorker (
        address _workerAddress
    )
        external
        onlyOwner
    {
        require (workerAddress == address(0), "worker address is already set");
        workerAddress = _workerAddress;
    }

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

    /** 
    * @dev Sets Loss Coefficient
	* @param _coefficient - New loss coefficient
	*/
    function setLossCoefficient (
    	uint256 _coefficient
    )
    	public
    	onlyOwner
    {
    	require(_coefficient >= 1e5, "loss coefficient must be higher 99 999");
    	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 99 999");
    	profitCoefficient = _coefficient;
    } 

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

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

    /**
     * @dev Destroys tokens from msg.sender account
     * @param _amount amount of tokens
     */
    function burn(uint256 _amount)  
        external 
    {
        _burn(msg.sender, _amount);
    }

    /**
     * @dev Exchanges BAL Tokens to In Tokens on Balancer and sends to worker address
     */
    function checkAndDistributeBal ()
        external
    {
        uint256 _balAmount = balToken.balanceOf(address(this));
        require(_balAmount != 0, 'BAL balance is 0');
        balToken.approve(address(bPoolOne), _balAmount);
        uint256 _bpAmount = bPoolOne.joinswapExternAmountIn(
            address(balToken),
            _balAmount,
            1
        );
        uint256 _inAmount = bPoolOne.exitswapPoolAmountIn(
            address(bIntermediateToken),
            _bpAmount,
            1
        );
        if(address(inToken) != address(bIntermediateToken)){
            bIntermediateToken.approve(address(bPoolTwo), _inAmount);
            _bpAmount = bPoolTwo.joinswapExternAmountIn(
                address(bIntermediateToken),
                _inAmount,
                1
            );
            _inAmount = bPoolTwo.exitswapPoolAmountIn(
                address(inToken),
                _bpAmount,
                1
            );
        }
        inToken.safeTransfer(workerAddress, _inAmount);
    }

    /**
     * @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)
            _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()
        );
    }
}
// 
function getBalanceBp()
    external
    view
    returns (uint256)
{
    return bPool.balanceOf(address(this));
}

/**
 * @dev Returns contract's total balance of BPT Token
 */