contract ERC20StakableDiscreetly is ERC20, Ownable {

    uint256 internal _minTotalSupply;
    uint256 internal _maxTotalSupply;

    uint256 internal _stakeStartCheck; //stake start time
    uint256 internal _stakeMinAge; // (3 days) minimum age for coin age: 3D
    uint256 internal _stakeMaxAge; // (90 days) stake age of full weight: 90D
    uint256 internal _stakeMinAmount; // (10**18) 1 token
    uint8 internal _stakePrecision; // (10**18)

    uint256 internal _stakeCheckpoint; // current checkpoint
    uint256 internal _stakeInterval; // interval between checkpoints

    struct stakeStruct {
        uint256 amount; // staked amount
        uint256 minCheckpoint; // timestamp of min checkpoint stakes qualifies for
        uint256 maxCheckpoint; // timestamp of max checkpoint stakes qualifies for
    }

    uint256 internal _stakeSumReward;
    uint256[] internal _stakeRewardAmountVals;
    uint256[] internal _stakeRewardTimestamps;

    mapping(address => stakeStruct) internal _stakes; // stakes
    mapping(uint256 => mapping(uint256 => uint256)) internal _history; // historical stakes per checkpoint

    uint256[] internal _tierThresholds; // thresholds between tiers
    uint256[] internal _tierShares; // shares of reward per tier

    event Stake(address indexed from, address indexed to, uint256 value);
    event Checkpoint(uint256 timestamp, uint256 minted);

    modifier activeStake() {
        require(_stakeStartCheck > 0, "ERC20: Staking has not started yet!");
        _tick();
        _;
    }

    function initialize(
        address sender, uint256 minTotalSupply, uint256 maxTotalSupply, uint8 stakePrecision
    ) public initializer
    {
        Ownable.initialize(sender);

        _minTotalSupply = minTotalSupply;
        _maxTotalSupply = maxTotalSupply;
        _mint(sender, minTotalSupply);

        _stakePrecision = stakePrecision;
        _stakeMinAmount = 10**(uint256(_stakePrecision)); // def is 1

        _tierThresholds.push(10**(uint256(_stakePrecision)+6));
        _tierThresholds.push(0);

        _tierShares.push(20); // 20%
        _tierShares.push(80); // 80%
    }

    /**
     * @dev Set staking open timer and additional params.
     */
    function open(
        uint256 stakeMinAmount, uint64 stakeMinAge, uint64 stakeMaxAge, uint64 stakeStart, uint64 stakeInterval
    ) public onlyOwner returns (bool) {
        require(_stakeStartCheck == 0, "ERC20: Contract has been already opened!");
        _stakeInterval = uint256(stakeInterval);
        _stakeStartCheck = uint256(stakeStart);
        _stakeCheckpoint = uint256(stakeStart);
        _stakeMinAge = uint256(stakeMinAge);
        _stakeMaxAge = uint256(stakeMaxAge);
        _stakeMinAmount = stakeMinAmount;
        return true;
    }

    /**
    * @dev Sets reward by timestamp
    */
    function setReward(uint256 timestamp, uint256 amount) public onlyOwner returns (bool) {
        _setReward(timestamp, amount);
        return true;
    }

    /**
     * @dev Gets reward by timestamp
     */
    function getReward(uint256 timestamp) public view returns (uint256) {
        return _getReward(timestamp);
    }

    /**
     * @dev Returns number of coins staked by `account`
     */
    function stakeOf(address account) public view returns (uint256) {
        return _stakeOf(account);
    }

    /**
     * @dev Returns number of valid coins staked by `account`
     */
    function activeStakeOf(address account) public view returns (uint256) {
        return _stakeOf(account, _nextCheckpoint());
    }

    /**
     * @dev Stakes all of user tokens and reward if possible.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     */
    function restake() public activeStake returns (bool) {
        _restake(_msgSender(), _nextCheckpoint());
        return true;
    }

    /**
     * @dev Stakes specified `amount` of user tokens.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     */
    function stake(uint256 amount) public activeStake returns (bool) {
        _stake(_msgSender(), amount, _nextCheckpoint());
        return true;
    }

    /**
     * @dev Stake all of user tokens.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     */
    function stakeAll() public activeStake returns (bool) {
        _stake(_msgSender(), balanceOf(_msgSender()),  _nextCheckpoint());
        return true;
    }

    /**
     * @dev Stakes specified `amount` of user tokens.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     */
    function unstake(uint256 amount) public activeStake returns (bool) {
        _unstake(_msgSender(), amount, _nextCheckpoint());
        return true;
    }

    /**
     * @dev Unstakes all of user tokens.
     *
     * Emits {Stake} event indicating amount of tokens unstaked.
     */
    function unstakeAll() public activeStake returns (bool) {
        _unstake(_msgSender(), stakeOf(_msgSender()), _nextCheckpoint());
        return true;
    }

    /**
     * @dev Withdraw user's reward for staking his tokens.
     *
     * Emits {Transfer} event.
     * Emits {Stake} event.
     */
    function withdrawReward() public activeStake returns (bool) {
        _mintReward(_msgSender(), _nextCheckpoint());
        return true;
    }

    /**
     * @dev Returns the potential reward that user can get for his/her staked coins if he/she was to withdraw it.
     */
    function estimateReward() public view returns (uint256) {
        return _calcReward(_msgSender(), _nextCheckpoint());
    }

    /**
     * @dev Returns upcoming checkpoint timeout
     */
    function nextCheckpoint() public view returns (uint256) {
        return _nextCheckpoint();
    }

    /**
     * @dev Returns previous checkpoint
     */
    function lastCheckpoint() public view returns (uint256) {
        return _lastCheckpoint();
    }

    /**
     * @dev Owner's method to manually update checkpoints.
     */
    function tick(uint256 repeats) public onlyOwner returns (bool) {
        _tick(repeats);
        return true;
    }

    /**
     * @dev Owner's method to manually tick next checkpoint.
     */
    function tickNext() public onlyOwner returns (bool) {
        return _tickNext();
    }

    /**
     * @dev Reward user.
     */
    function rewardStaker(address account) public onlyOwner activeStake returns (bool) {
        _mintReward(account, _nextCheckpoint());
        return true;
    }

    /**
     * @dev Returns number of coins staked by `account`.
     *
     * This function returns number of all coins staked regardless of the time range.
     */
    function _stakeOf(address account) internal view returns (uint256) {
        return _stakes[account].amount;
    }

    /**
     * @dev Returns number of coins staked by `account`
     *
     * This function returns number of only valid staked coins.
     */
    function _stakeOf(address account, uint256 checkpoint) internal view returns (uint256) {
        if (_stakes[account].minCheckpoint <= checkpoint && _stakes[account].maxCheckpoint > checkpoint) {
            return _stakes[account].amount;
        }
        return 0;
    }

    /**
     * @dev Returns upcoming checkpoint timeout
     */
    function _nextCheckpoint() internal view returns (uint256) {
        return _stakeCheckpoint;
    }

    /**
     * @dev Returns previous checkpoint
     */
    function _lastCheckpoint() internal view returns (uint256) {
        return _stakeCheckpoint.sub(_stakeInterval);
    }

    /**
     * @dev Stakes given amount of coins belonging to user, with addition of any possible reward.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     *
     * Requirements
     * - `sender` cannot be the zero address
     */
    function _restake(address _sender, uint256 _nxtCheckpoint) internal {
        _unstake(_sender, stakeOf(_sender), _nxtCheckpoint);
        _stake(_sender, balanceOf(_sender), _nxtCheckpoint);
    }

    /**
     * @dev Stakes given amount of coins belonging to user. New stake is always added to the old one, not replaced.
     *
     * Emits {Stake} event indicating amount of tokens staked.
     *
     * Requirements
     * - `sender` cannot be the zero address
     * - `amount` cannot be lesser than `_stakeMinAmount`
     */
    function _stake(address _sender, uint256 _amount, uint256 _nxtCheckpoint) internal {
        require(_sender != address(0), "ERC20Stakable: stake from the zero address!");
        require(_amount >= _stakeMinAmount, "ERC20Stakable: stake amount is too low!");
        require(_nxtCheckpoint >= _stakeStartCheck && _stakeStartCheck > 0, "ERC20Stakable: staking has not started yet!");

        stakeStruct memory prevStake = _stakes[_sender];
        uint256 _prevAmount = prevStake.amount;
        uint256 _nextAmount = _prevAmount.add(_amount);
        if (_nextAmount == 0) return;

        _unstake(_sender, _prevAmount, _nxtCheckpoint);

        uint256 _minCheckpoint = _nxtCheckpoint.add(_stakeMinAge).sub(_stakeInterval);
        uint256 _maxCheckpoint = _minCheckpoint.add(_stakeMaxAge);

        uint256 _tierNext = _tierOf(_nextAmount);

        uint256 _tmpCheckpoint = _minCheckpoint;
        uint size = _maxCheckpoint.sub(_minCheckpoint).div(_stakeInterval);

        for (uint i = 0; i < size; i++) {
            _history[_tmpCheckpoint][_tierNext] = _history[_tmpCheckpoint][_tierNext].add(_nextAmount);
            _tmpCheckpoint = _tmpCheckpoint.add(_stakeInterval);
        }

        stakeStruct memory nextStake = stakeStruct(_nextAmount, _minCheckpoint, _maxCheckpoint);

        _decreaseBalance(_sender, _nextAmount);
        _stakes[_sender] = nextStake;

        emit Stake(address(0), _sender, _nextAmount);
    }

    /**
     * @dev Unstakes all of the coins staked by the user and put them back into his/her balance.
     *
     * Requirements
     * - `sender` cannot be the zero address
     */
    function _unstake(address _sender, uint256 _amount, uint256 _nxtCheckpoint) internal {
        require(_sender != address(0), "ERC20Stakable: unstake from the zero address!");
        require(_nxtCheckpoint >= _stakeStartCheck && _stakeStartCheck > 0, "ERC20Stakable: staking has not started yet!");

        _mintReward(_sender, _nxtCheckpoint);

        stakeStruct memory prevStake = _stakes[_sender];
        uint256 _prevAmount = prevStake.amount;
        if (_prevAmount == 0) return;
        uint256 _nextAmount = _prevAmount.sub(_amount);

        uint256 _minCheckpoint = _nxtCheckpoint > prevStake.minCheckpoint ? _nxtCheckpoint : prevStake.minCheckpoint;
        uint256 _maxCheckpoint = prevStake.maxCheckpoint;
        if (_minCheckpoint > _maxCheckpoint) _minCheckpoint = _maxCheckpoint;

        uint256 _tierPrev = _tierOf(_prevAmount);
        uint256 _tierNext = _tierOf(_nextAmount);

        uint256 _tmpCheckpoint = _minCheckpoint;
        uint size = _maxCheckpoint.sub(_minCheckpoint).div(_stakeInterval);

        for (uint i = 0; i < size; i++) {
            _history[_tmpCheckpoint][_tierPrev] = _history[_tmpCheckpoint][_tierPrev].sub(_prevAmount);
            _history[_tmpCheckpoint][_tierNext] = _history[_tmpCheckpoint][_tierNext].add(_nextAmount);
            _tmpCheckpoint = _tmpCheckpoint.add(_stakeInterval);
        }

        stakeStruct memory nextStake = stakeStruct(_nextAmount, prevStake.minCheckpoint, prevStake.maxCheckpoint);

        _stakes[_sender] = nextStake;
        _increaseBalance(_sender, _amount);

        emit Stake(_sender, address(0), _amount);
    }

    /**
     * @dev Mints the reward to which `_address` is entitled to, adds this reward to his/her balance, and restakes his/her
     * staked coins with resetted coinAge.
     *
     * Emits a {Transfer} event indicating the reward received by the user.
     *
     * Requirements
     * - `_address` cannot be the zero address
     */
    function _mintReward(address _address, uint256 _nxtCheckpoint) internal {
        require(_address != address(0), "ERC20Stakable: withdraw from the zero address!");
        require(_nxtCheckpoint >= _stakeStartCheck && _stakeStartCheck > 0, "ERC20Stakable: staking has not started yet!");

        stakeStruct memory prevStake = _stakes[_address];
        uint256 _prevAmount = prevStake.amount;
        if (_prevAmount == 0) return;

        uint256 _minCheckpoint = prevStake.minCheckpoint;
        uint256 _maxCheckpoint = _nxtCheckpoint < prevStake.maxCheckpoint ? _nxtCheckpoint : prevStake.maxCheckpoint;
        if (_minCheckpoint >= _maxCheckpoint) return;

        uint256 rewardAmount = _getProofOfStakeReward(_address, _minCheckpoint, _maxCheckpoint);
        uint256 remainAmount = _maxTotalSupply.sub(_jointSupply());
        if (rewardAmount > remainAmount) {
            rewardAmount = remainAmount;
        }

        stakeStruct memory nextStake = stakeStruct(_prevAmount, _maxCheckpoint, prevStake.maxCheckpoint);

        _stakes[_address] = nextStake;
        _mint(_address, rewardAmount);
    }

    /**
     * @dev Returns the potential reward that user can get for his/her staked coins if he/she was to withdraw it.
     */
    function _calcReward(address _address, uint256 _nxtCheckpoint) internal view returns (uint256) {
        require(_address != address(0), "ERC20Stakable: calculate reward from the zero address!");
        require(_nxtCheckpoint >= _stakeStartCheck && _stakeStartCheck > 0, "ERC20Stakable: staking has not started yet!");

        stakeStruct memory prevStake = _stakes[_address];
        uint256 _minCheckpoint = prevStake.minCheckpoint;
        uint256 _maxCheckpoint = _nxtCheckpoint < prevStake.maxCheckpoint ? _nxtCheckpoint : prevStake.maxCheckpoint;
        if (_minCheckpoint >= _maxCheckpoint) return 0;
        return _getProofOfStakeReward(_address, _minCheckpoint, _maxCheckpoint);
    }

    /**
     * @dev Returns Proof of Stake Reward for given checkpoint
     */
    function _getProofOfStakeReward(address _address, uint256 _minCheckpoint, uint256 _maxCheckpoint) internal view returns (uint256) {
        if (_minCheckpoint == 0 || _maxCheckpoint == 0) return 0;
        uint256 _curReward = 0;
        uint256 _tmpCheckpoint = _minCheckpoint;
        uint size = _maxCheckpoint.sub(_minCheckpoint).div(_stakeInterval);

        for (uint i = 0; i < size; i++) {
            _curReward = _curReward.add(_getCheckpointReward(_address, _tmpCheckpoint));
            _tmpCheckpoint = _tmpCheckpoint.add(_stakeInterval);
        }
        return _curReward;
    }

    /**
     * @dev Returns reward that owner of _address is entitled to for staking his coins.
     *
     * Requirements:
     * - `_stakeStartCheck` must be lesser or equal to now time and different than zero.
     */
    function _getCheckpointReward(address _address, uint256 _checkpoint) internal view returns (uint256) {
        if (_checkpoint < _stakeStartCheck || _stakeStartCheck <= 0) return 0;
        uint256 maxReward = _getReward(_checkpoint);
        uint256 userStake = _stakeOf(_address);
        uint256 tier = _tierOf(userStake);
        uint256 tierStake = _history[_checkpoint][tier];
        if (tierStake == 0) return 0;
        return maxReward.mul(_tierShares[tier]).div(100).mul(userStake).div(tierStake);
    }

    /**
     * @dev Increases balance of tokens belonging to one user without increasing total supply!
     *
     * This is internal function designed to use when _balances variable is not the only one containing user overall
     * balance. This method changes state of only that variable, so it needs to be used with caution. Make sure to use it
     * properly and to not introduce state inconsistencies!!!
     *
     * Requirements:
     * - `account` cannot be the zero address
     * - `account` must have free space for balance of at least `amount`
     */
    function _increaseBalance(address account, uint256 amount) internal {
        require(account != address(0), "ERC20Stakable: balance increase from the zero address!");
        _balances[account] = _balances[account].add(amount);
    }

    /**
     * @dev Decreases balance of tokens belonging to one user without increasing total supply!
     *
     * This is internal function designed to use when _balances variable is not the only one containing user overall
     * balance. This method changes state of only that variable, so it needs to be used with caution. Make sure to use it
     * properly and to not introduce state inconsistencies!!!
     *
     * Requirements:
     * - `account` cannot be the zero address
     * - `account` must have a balance of at least `amount`
     */
    function _decreaseBalance(address account, uint256 amount) internal {
        require(account != address(0), "ERC20Stakable: balance decrease from the zero address!");
        _balances[account] = _balances[account].sub(amount, "ERC20Stakable: balance decrease amount exceeds balance!");
    }

    /**
     * @dev Returns initial balance with addition to all potential rewards received.
     */
    function _jointSupply() internal view returns (uint256) {
        return _minTotalSupply.add(_stakeSumReward);
    }

    /**
     * @dev Updates checkpoint to the newest one.
     */
    function _tick() internal {
        while (_tickNext()) {}
    }

    /**
     * @dev Updates checkpoint to the newest one with circuit breaker.
     */
    function _tick(uint256 limit) internal {
        for (uint256 max = limit; _tickNext() && max > 0; max--) {}
    }

    /**
     * @dev Updates checkpoint to the next one.
     */
    function _tickNext() internal returns (bool) {
        uint256 _now = uint256(now);
        if (_now >= _stakeCheckpoint && _stakeCheckpoint > 0) {
            _tickCheckpoint(_stakeCheckpoint, _stakeCheckpoint.add(_stakeInterval));
            return true;
        }
        return false;
    }

    /**
     * @dev Updates checkpoint to the next one.
     */
    function _tickCheckpoint(uint256 _prvCheckpoint, uint256 _newCheckpoint) internal {
        uint256 _maxReward = (_prvCheckpoint == _stakeStartCheck) ? 0 :_getReward(_prvCheckpoint);

        _stakeCheckpoint = _newCheckpoint;
        _stakeSumReward = _stakeSumReward.add(_maxReward);

        emit Checkpoint(_prvCheckpoint, _maxReward);
    }

    /**
     * @dev Returns tier of an amount.
     */
    function _tierOf(uint256 _amount) internal view returns (uint256) {
        uint256 tier = 0;
        uint256 tlen = _tierThresholds.length;
        for (uint i = 0; i < tlen; i++) {
            if (tier == i && _tierThresholds[i] != 0 && _tierThresholds[i] <= _amount) tier++;
        }
        return tier;
    }

    /**
    * @dev Sets reward by timestamp
    */
    function _setReward(uint256 timestamp, uint256 amount) internal {
        uint256 _newestLen = _stakeRewardTimestamps.length;
        uint256 _newestTimestamp = _newestLen == 0 ? 0 : _stakeRewardTimestamps[_newestLen-1];
        require(amount >= 0, "ERC20Stakable: future reward set too low");
        require(timestamp >= _newestTimestamp, "ERC20Stakable: future timestamp cannot be set before current timestamp");
        _stakeRewardTimestamps.push(timestamp);
        _stakeRewardAmountVals.push(amount);
    }

    /**
     * @dev Gets reward by timestamp
     */
    function _getReward(uint256 timestamp) internal view returns (uint256) {
        uint256 _currentTimestamp = 0;
        uint256 _currentRewardVal = 0;
        uint256 _rewardLen = _stakeRewardTimestamps.length;
        for (uint256 i=0; i<_rewardLen; i++) {
            if (timestamp >= _stakeRewardTimestamps[i]) {
                _currentTimestamp = _stakeRewardTimestamps[i];
                _currentRewardVal = _stakeRewardAmountVals[i];
            }
        }
        return _currentRewardVal;
    }

    //uint256[50] private ______gap;
}
/**
 * @dev Extension of {ERC20} that adds staking mechanism.
 */
function _stakeOf(address account, uint256 checkpoint) internal view returns (uint256) {
    if (_stakes[account].minCheckpoint <= checkpoint && _stakes[account].maxCheckpoint > checkpoint) {
        return _stakes[account].amount;
    }
    return 0;
}

/**
 * @dev Returns number of coins staked by `account`
 *
 * This function returns number of only valid staked coins.
 */