contract VoxVault is ERC20 {
    using SafeERC20 for IERC20;
    using Address for address;
    using SafeMath for uint256;

    IERC20 public token;
    IERC20 public vox;

    uint256 public min = 9500;
    uint256 public constant max = 10000;

    uint256 public burnFee = 5000;
    uint256 public constant burnFeeMax = 7500;
    uint256 public constant burnFeeMin = 2500;
    uint256 public constant burnFeeBase = 10000;

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

    bool public isActive = true;

    address public governance;
    address public treasury;
    address public timelock;
    address public strategy;
    address public burn = 0x000000000000000000000000000000000000dEaD;

    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 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 _token, address _vox, address _governance, address _treasury, address _timelock)
        public
        ERC20(
            string(abi.encodePacked("voxie ", ERC20(_token).name())),
            string(abi.encodePacked("v", ERC20(_token).symbol()))
        )
    {
        _setupDecimals(ERC20(_token).decimals());
        token = IERC20(_token);
        vox = IERC20(_vox);
        governance = _governance;
        treasury = _treasury;
        timelock = _timelock;
    }

    // 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) public {
        require(msg.sender == governance, "!governance");
        isActive = _isActive;
    }

    // Set the minimum percentage of tokens that can be deposited to earn 
    function setMin(uint256 _min) external {
        require(msg.sender == governance, "!governance");
        require(_min <= max, "numerator cannot be greater than denominator");
        min = _min;
    }

    // Set a new governance address, can only be triggered by the old address
    function setGovernance(address _governance) public {
        require(msg.sender == governance, "!governance");
        governance = _governance;
    }

    // Set the timelock address, can only be triggered by the old address
    function setTimelock(address _timelock) public {
        require(msg.sender == timelock, "!timelock");
        timelock = _timelock;
    }

    // Set a new strategy address, can only be triggered by the timelock
    function setStrategy(address _strategy) public {
        require(msg.sender == timelock, "!timelock");
        require(IStrategy(_strategy).underlying() == address(token), 'strategy does not support this underlying');
        strategy = _strategy;
    }

    // Set the burn fee for multipliers
    function setBurnFee(uint256 _burnFee) public {
        require(msg.sender == timelock, "!timelock");
        require(_burnFee <= burnFeeMax, 'burn fee can not be more than 75,0 %');
        require(_burnFee >= burnFeeMin, 'burn fee can not be less than 25,0 %');
        burnFee = _burnFee;
    }

    // Set withdrawal fee for the vault
    function setWithdrawalFee(uint256 _withdrawalFee) external {
        require(msg.sender == timelock, "!timelock");
        withdrawalFee = _withdrawalFee;
    }

    // Add a new multplier with the selected cost
    function addMultiplier(uint256 _cost) public returns (uint256 index) {
        require(msg.sender == timelock, "!timelock");
        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 {
        require(msg.sender == timelock, "!timelock");
        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, 'vault is not active');
        require(strategy != address(0), 'strategy is not set');
        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 {
        require(isActive, 'vault is not active');
        require(strategy != address(0), 'strategy is not yet set');
        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) {
            if (tiers[msg.sender] > 0) {
                uint256 userMultiplier = tiers[msg.sender].add(tierBase);
                shares = _amount.mul(userMultiplier).div(tierBase);
            } else {
                shares = _amount;
            }
        } else {
            if (tiers[msg.sender] > 0) {
                uint256 userMultiplier = tiers[msg.sender].add(tierBase);
                shares = (_amount.mul(userMultiplier).div(tierBase).mul(totalSupply())).div(_pool);
            } else {
                shares = (_amount.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 {
        require(block.number > depositBlocks[msg.sender], 'withdraw: not the same block as deposits');
        require(_amount > 0, 'withdraw: positive amount');
        require(_amount <= deposits[msg.sender], 'withdraw: more than deposited');
        require(issued[msg.sender] > 0, 'withdraw: you need to first make a 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, "withdraw: not enough shares in balance");
        // 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) {
        // Calculate amount of rewards the user has gained
        uint256 rewards = balance().sub(totalDeposited);
        uint256 shares = balanceOf(account);
        if (rewards > 0) {
            return (rewards.mul(shares)).div(totalSupply());
        }
    }

    // Purchase a multiplier tier for the user
    function purchaseMultiplier(uint256 _tiers) external returns (uint256 newTier) {
        require(isActive, 'vault is not active');
        require(strategy != address(0), 'strategy is not yet set');
        require(_tiers > 0, 'you need to purchase at least one multiplier');
        uint256 multipliersLength = multiplierCosts.length;
        require(tiers[msg.sender].add(_tiers) <= multipliersLength, 'you can not purchase so many tiers');

        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(vox).balanceOf(msg.sender) >= totalCost, 'you do not have enough VOX to purchase the multiplier tiers');
        vox.safeTransferFrom(msg.sender, address(this), totalCost);
        newTier = tiers[msg.sender].add(_tiers);
        tiers[msg.sender] = newTier;
        emit MultiplierPurchased(msg.sender, _tiers, totalCost);
    }

    // Distribute the VOX tokens collected by the multiplier purchases
    function distribute() external {
        require(msg.sender == governance, "!governance");
        uint256 b = vox.balanceOf(address(this));
        if (b > 0) {
            uint256 toBurn = b.mul(burnFee).div(burnFeeBase);
            uint256 leftover = b.sub(toBurn);
            vox.safeTransfer(burn, toBurn);
            vox.safeTransfer(treasury, leftover);
        }
    }

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

    // Helper function when migrating to new version
    function setMultiplier(address account, uint256 multiplier) external {
        require(msg.sender == timelock, "!timelock");
        require(multiplier <= multiplierCosts.length, 'multiplier value too high');
        tiers[account] = multiplier;
    }

    // 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 tiers have already been purchased');
        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());
    }
}
function setGovernance(address _governance) public {
        require(msg.sender == governance, "!governance");
        governance = _governance;
    }
// Set a new governance address, can only be triggered by the old address