contract YvsVault is ERC20, ReentrancyGuard {
    using SafeERC20 for IERC20;
    using Address for address;
    using SafeMath for uint256;

    IERC20 internal token;
    IERC20 internal yvs;

    // Underlying token address
    address public underlying;

    // Address of controller
    address public controller;

    // Minimum/maximum allowed to be invested
    uint256 public min = 9500;
    uint256 public constant max = 10000;

    // Burn fee on purchases
    uint256 public burnFee = 5000;
    uint256 public constant burnFeeMax = 7500;
    uint256 public constant burnFeeMin = 2500;
    uint256 public constant burnFeeBase = 10000;

    // Withdrawal fee
    uint256 public withdrawalFee = 25;
    uint256 public constant withdrawalFeeMax = 25;
    uint256 public constant withdrawalFeeBase = 10000;

    // Minimum deposit period
    uint256 public minDepositPeriod = 7 days;

    // Is the strategy active (inactive on deploy)
    bool public isActive = false;

    // Addresses
    address public governance;
    address public treasury;
    address public timelock;
    address public strategy;

    mapping(address => uint256) public depositBlocks;
    mapping(address => uint256) public deposits;
    mapping(address => uint256) public issued;
    mapping(address => uint256) public tiers;
    uint256[] public multiplierCosts;
    uint256 internal constant tierMultiplier = 5;
    uint256 internal constant tierBase = 100;
    uint256 public totalDeposited = 0;

    // EVENTS
    event Deposit(address indexed user, uint256 amount);
    event Withdraw(address indexed user, uint256 amount);
    event SharesIssued(address indexed user, uint256 amount);
    event SharesPurged(address indexed user, uint256 amount);
    event ClaimRewards(address indexed user, uint256 amount);
    event MultiplierPurchased(address indexed user, uint256 tiers, uint256 totalCost);

    constructor(address _underlying, address _yvs, address _governance, address _treasury, address _timelock)
        public
        ERC20(
            string(abi.encodePacked("yvsie ", ERC20(_underlying).name())),
            string(abi.encodePacked("yvs", ERC20(_underlying).symbol()))
        )
    {
        require(address(_underlying) != address(_yvs), "!underlying");

        _setupDecimals(ERC20(_underlying).decimals());
        token = IERC20(_underlying);
        yvs = IERC20(_yvs);
        underlying = _underlying;
        governance = _governance;
        treasury = _treasury;
        timelock = _timelock;

        // multiplier costs from tier 1 to 5
        multiplierCosts.push(5000000000000000000); // 5 $yvs
        multiplierCosts.push(10000000000000000000); // 10 $yvs
        multiplierCosts.push(20000000000000000000); // 20 $yvs
        multiplierCosts.push(40000000000000000000); // 40 $yvs
        multiplierCosts.push(80000000000000000000); // 80 $yvs
    }

    // Check the total underyling token balance to see if we should earn();
    function balance() public view returns (uint256) {
        return
            token.balanceOf(address(this)).add(
                IStrategy(strategy).balanceOf()
            );
    }

    // Sets whether deposits are accepted by the vault
    function setActive(bool _isActive) external isGovernance {
        isActive = _isActive;
    }

    // Set the minimum percentage of tokens that can be deposited to earn 
    function setMin(uint256 _min) external isGovernance {
        require(_min <= max, "min>max");
        min = _min;
    }

    // Set a new governance address, can only be triggered by the old address
    function setGovernance(address _governance) external isGovernance {
        governance = _governance;
    }

    // Set a new treasury address, can only be triggered by the governance
    function setTreasury(address _treasury) external isGovernance {
        treasury = _treasury;
    }

    // Set the timelock address, can only be triggered by the old address
    function setTimelock(address _timelock) external isTimelock {
        timelock = _timelock;
    }

    // Set a new strategy address, can only be triggered by the timelock
    function setStrategy(address _strategy) external isTimelock {
        require(IStrategy(_strategy).underlying() == address(token), '!underlying');
        strategy = _strategy;
    }

    // Set the controller address, can only be set once after deployment
    function setController(address _controller) external isGovernance {
        require(controller == address(0), "!controller");
        controller = _controller;
    }

    // Set the burn fee for multipliers
    function setBurnFee(uint256 _burnFee) public isTimelock {
        require(_burnFee <= burnFeeMax, 'max');
        require(_burnFee >= burnFeeMin, 'min');
        burnFee = _burnFee;
    }

    // Set withdrawal fee for the vault
    function setWithdrawalFee(uint256 _withdrawalFee) external isTimelock {
        require(_withdrawalFee <= withdrawalFeeMax, "!max");
        withdrawalFee = _withdrawalFee;
    }

    // Add a new multplier with the selected cost
    function addMultiplier(uint256 _cost) public isTimelock returns (uint256 index) {
        multiplierCosts.push(_cost);
        index = multiplierCosts.length - 1;
    }

    // Set new cost for multiplier, can only be triggered by the timelock
    function setMultiplier(uint256 index, uint256 _cost) public isTimelock {
        multiplierCosts[index] = _cost;
    }

    // Custom logic in here for how much of the underlying asset can be deposited
    // Sets the minimum required on-hand to keep small withdrawals cheap
    function available() public view returns (uint256) {
        return token.balanceOf(address(this)).mul(min).div(max);
    }

    // Deposits collected underlying assets into the strategy and starts earning
    function earn() public {
        require(isActive, 'earn: !active');
        require(strategy != address(0), 'earn: !strategy');
        uint256 _bal = available();
        token.safeTransfer(strategy, _bal);
        IStrategy(strategy).deposit();
    }

    // Deposits underlying assets from the user into the vault contract
    function deposit(uint256 _amount) public nonReentrant {
        require(!address(msg.sender).isContract() && msg.sender == tx.origin, "deposit: !contract");
        require(isActive, 'deposit: !vault');
        require(strategy != address(0), 'deposit: !strategy');
        
        uint256 _pool = balance();
        uint256 _before = token.balanceOf(address(this));
        token.safeTransferFrom(msg.sender, address(this), _amount);
        uint256 _after = token.balanceOf(address(this));
        _amount = _after.sub(_before); // Additional check for deflationary tokens
        deposits[msg.sender] = deposits[msg.sender].add(_amount);
        totalDeposited = totalDeposited.add(_amount);
        uint256 shares = 0;
        if (totalSupply() == 0) {
            uint256 userMultiplier = tiers[msg.sender].mul(tierMultiplier).add(tierBase); // 5 %, 10 %, 15 %, 20 %, 25 %
            shares = _amount.mul(userMultiplier).div(tierBase);
        } else {
            uint256 userMultiplier = tiers[msg.sender].mul(tierMultiplier).add(tierBase); // 5 %, 10 %, 15 %, 20 %, 25 %
            shares = (_amount.mul(userMultiplier).div(tierBase).mul(totalSupply())).div(_pool);
        }

        _mint(msg.sender, shares);
        issued[msg.sender] = issued[msg.sender].add(shares);
        depositBlocks[msg.sender] = block.number;
        emit Deposit(msg.sender, _amount);
        emit SharesIssued(msg.sender, shares);
    }

    // Deposits all the funds of the user
    function depositAll() external {
        deposit(token.balanceOf(msg.sender));
    }

    // No rebalance implementation for lower fees and faster swaps
    function withdraw(uint256 _amount) public nonReentrant {
        require(!address(msg.sender).isContract() && msg.sender == tx.origin, "withdraw: !no contract");
        require(block.number >= depositBlocks[msg.sender].add(minDepositPeriod), 'withdraw: !minDepositPeriod');
        require(_amount > 0, '!positive');
        require(_amount <= deposits[msg.sender], '>deposit');
        require(issued[msg.sender] > 0, '!deposit');

        // Get the amount of user shares
        uint256 shares = issued[msg.sender];
        // Calculate percentage of principal being withdrawn
        uint256 p = (_amount.mul(1e18).div(deposits[msg.sender]));
        // Calculate amount of shares to be burned
        uint256 r = shares.mul(p).div(1e18);

        // Make sure the user has the required amount in his balance
        require(balanceOf(msg.sender) >= r, "!shares");
        // Burn the proportion of shares that are being withdrawn
        _burn(msg.sender, r);
        // Reduce the amount from user's issued amount
        issued[msg.sender] = issued[msg.sender].sub(r);

        // Calculate amount of rewards the user has gained
        uint256 rewards = balance().sub(totalDeposited);
        uint256 userRewards = 0;
        if (rewards > 0) {
            userRewards = (rewards.mul(shares)).div(totalSupply());
        }

        // Receive the correct proportion of the rewards
        if (userRewards > 0) {
            userRewards = userRewards.mul(p).div(1e18);
        }

        // Calculate the withdrawal amount as _amount + user rewards
        uint256 withdrawAmount = _amount.add(userRewards);

        // Check balance
        uint256 b = token.balanceOf(address(this));
        if (b < withdrawAmount) {
            uint256 _withdraw = withdrawAmount.sub(b);
            IStrategy(strategy).withdraw(_withdraw);
            uint256 _after = token.balanceOf(address(this));
            uint256 _diff = _after.sub(b);
            if (_diff < _withdraw) {
                withdrawAmount = b.add(_diff);
            }
        }

        // Remove the withdrawn principal from total and user deposits
        deposits[msg.sender] = deposits[msg.sender].sub(_amount);
        totalDeposited = totalDeposited.sub(_amount);

        // Calculate withdrawal fee and deduct from amount
        uint256 _withdrawalFee = _amount.mul(withdrawalFee).div(withdrawalFeeBase);
        token.safeTransfer(treasury, _withdrawalFee);
        token.safeTransfer(msg.sender, withdrawAmount.sub(_withdrawalFee));

        // Emit events
        emit Withdraw(msg.sender, _amount);
        emit SharesPurged(msg.sender, r);
        emit ClaimRewards(msg.sender, userRewards);
    }

    // Withdraws all underlying assets belonging to the user
    function withdrawAll() external {
        withdraw(deposits[msg.sender]);
    }

    function pendingRewards(address account) external view returns (uint256 pending) {
        // Calculate amount of rewards the user has gained
        uint256 rewards = balance().sub(totalDeposited);
        uint256 shares = issued[account];
        if (rewards > 0) {
            pending = (rewards.mul(shares)).div(totalSupply());
        }
    }

    // Purchase a multiplier tier for the user
    function purchaseMultiplier(uint256 _tiers) external returns (uint256 newTier) {
        require(isActive, '!active');
        require(strategy != address(0), '!strategy');
        require(_tiers > 0, '!tiers');
        uint256 multipliersLength = multiplierCosts.length;
        require(tiers[msg.sender].add(_tiers) <= multipliersLength, '!max');

        uint256 totalCost = 0;
        uint256 lastMultiplier = tiers[msg.sender].add(_tiers);
        for (uint256 i = tiers[msg.sender]; i < multipliersLength; i++) {
            if (i == lastMultiplier) {
                break;
            }
            totalCost = totalCost.add(multiplierCosts[i]);
        }

        require(IERC20(yvs).balanceOf(msg.sender) >= totalCost, '!yvs');
        yvs.safeTransferFrom(msg.sender, address(this), totalCost);
        newTier = tiers[msg.sender].add(_tiers);
        tiers[msg.sender] = newTier;
        emit MultiplierPurchased(msg.sender, _tiers, totalCost);
    }

    // Distribute the YVS tokens collected by the multiplier purchases
    function distribute() external restricted {
        uint256 b = yvs.balanceOf(address(this));
        if (b > 0) {
            uint256 toBurn = b.mul(burnFee).div(burnFeeBase);
            uint256 leftover = b.sub(toBurn);
            Burnable(address(yvs)).burn(toBurn);
            yvs.safeTransfer(treasury, leftover);
        }
    }

    // Used to salvage any non-underlying assets to governance
    function salvage(address reserve, uint256 amount) external isGovernance {
        require(reserve != address(token), "!token");
        require(reserve != address(yvs), "!yvs");
        IERC20(reserve).safeTransfer(treasury, amount);
    }

    // Returns the current multiplier tier for the user
    function getMultiplier() external view returns (uint256) {
        return tiers[msg.sender];
    }

    // Returns the next multiplier tier cost for the user
    function getNextMultiplierCost() external view returns (uint256) {
        require(tiers[msg.sender] < multiplierCosts.length, '!all');
        return multiplierCosts[tiers[msg.sender]];
    }

    // Returns the total number of multipliers
    function getCountOfMultipliers() external view returns (uint256) {
        return multiplierCosts.length;
    }

    // Returns the current ratio between earned assets and deposited assets
    function getRatio() public view returns (uint256) {
        return (balance().sub(totalDeposited)).mul(1e18).div(totalSupply());
    }

    // **** Modifiers **** //

    modifier restricted {
        require(
            (msg.sender == tx.origin && !address(msg.sender).isContract()) ||
                msg.sender == governance ||
                msg.sender == controller
        );

        _;
    }

    modifier isTimelock {
        require(
            msg.sender == timelock, 
            "!timelock"
        );

        _;
    }

    modifier isGovernance {
        require(
            msg.sender == governance, 
            "!governance"
        );

        _;
    }
}
function balance() public view returns (uint256) {
    return
        token.balanceOf(address(this)).add(
            IStrategy(strategy).balanceOf()
        );
}

// Check the total underyling token balance to see if we should earn();