contract StakeableToken is GlobalsAndUtility {
    modifier onlyAfterNDays(uint256 daysShift) {
        require(now >= LAUNCH_TIME, "JACKPOT: Too early");
        require(
            firstAuction != uint256(-1),
            "JACKPOT: Must be at least one auction"
        );
        require(
            _currentDay() >= firstAuction.add(daysShift),
            "JACKPOT: Too early"
        );
        _;
    }

    /**
     * @dev PUBLIC FACING: Open a stake.
     * @param newStakedSuns Number of Suns to stake
     * @param newStakedDays Number of days to stake
     */
    function stakeStart(uint256 newStakedSuns, uint256 newStakedDays)
        external
        onlyAfterNDays(1)
    {
        GlobalsCache memory g;
        GlobalsCache memory gSnapshot;
        _globalsLoad(g, gSnapshot);
        if (g._currentDay >= 1) endLoteryDay(g._currentDay.sub(1));
        /* Enforce the minimum stake time */
        require(
            newStakedDays >= MIN_STAKE_DAYS,
            "JACKPOT: newStakedDays lower than minimum"
        );
        /* Check if log data needs to be updated */
        _dailyDataUpdateAuto(g);
        _stakeStart(g, newStakedSuns, newStakedDays);
        /* Remove staked Suns from balance of staker */
        _burn(msg.sender, newStakedSuns);
        _globalsSync(g, gSnapshot);
    }

    /**
     * @dev PUBLIC FACING: Unlocks a completed stake, distributing the proceeds of any penalty
     * immediately. The staker must still call stakeEnd() to retrieve their stake return (if any).
     * @param stakerAddr Address of staker
     * @param stakeIndex Index of stake within stake list
     * @param stakeIdParam The stake's id
     */
    function stakeGoodAccounting(
        address stakerAddr,
        uint256 stakeIndex,
        uint40 stakeIdParam
    ) external {
        GlobalsCache memory g;
        GlobalsCache memory gSnapshot;
        _globalsLoad(g, gSnapshot);
        if (g._currentDay >= 1) endLoteryDay(g._currentDay.sub(1));
        /* require() is more informative than the default assert() */
        require(
            stakeLists[stakerAddr].length != 0,
            "JACKPOT: Empty stake list"
        );
        require(
            stakeIndex < stakeLists[stakerAddr].length,
            "JACKPOT: stakeIndex invalid"
        );
        StakeStore storage stRef = stakeLists[stakerAddr][stakeIndex];
        /* Get stake copy */
        StakeCache memory st;
        _stakeLoad(stRef, stakeIdParam, st);
        /* Stake must have served full term */
        require(
            g._currentDay >= st._lockedDay.add(st._stakedDays),
            "JACKPOT: Stake not fully served"
        );
        /* Stake must still be locked */
        require(st._unlockedDay == 0, "JACKPOT: Stake already unlocked");
        /* Check if log data needs to be updated */
        _dailyDataUpdateAuto(g);
        /* Unlock the completed stake */
        _stakeUnlock(g, st);
        /* stakeReturn & dividends values are unused here */
        (, uint256 payout, , uint256 penalty, uint256 cappedPenalty) =
            _stakePerformance(g, st, st._stakedDays);
        emit StakeGoodAccounting(
            stakeIdParam,
            stakerAddr,
            msg.sender,
            st._stakedSuns,
            st._stakeShares,
            payout,
            penalty
        );
        if (cappedPenalty != 0) {
            g._stakePenaltyTotal = g._stakePenaltyTotal.add(cappedPenalty);
        }
        /* st._unlockedDay has changed */
        _stakeUpdate(stRef, st);
        _globalsSync(g, gSnapshot);
    }

    /**
     * @dev PUBLIC FACING: Closes a stake. The order of the stake list can change so
     * a stake id is used to reject stale indexes.
     * @param stakeIndex Index of stake within stake list
     * @param stakeIdParam The stake's id
     */
    function stakeEnd(uint256 stakeIndex, uint40 stakeIdParam) external {
        GlobalsCache memory g;
        GlobalsCache memory gSnapshot;
        _globalsLoad(g, gSnapshot);
        StakeStore[] storage stakeListRef = stakeLists[msg.sender];
        /* require() is more informative than the default assert() */
        require(stakeListRef.length != 0, "JACKPOT: Empty stake list");
        require(
            stakeIndex < stakeListRef.length,
            "JACKPOT: stakeIndex invalid"
        );
        /* Get stake copy */
        StakeCache memory st;
        _stakeLoad(stakeListRef[stakeIndex], stakeIdParam, st);
        /* Check if log data needs to be updated */
        _dailyDataUpdateAuto(g);
        _globalsSync(g, gSnapshot);
        uint256 servedDays = 0;
        bool prevUnlocked = (st._unlockedDay != 0);
        uint256 stakeReturn;
        uint256 payout = 0;
        uint256 dividends = 0;
        uint256 penalty = 0;
        uint256 cappedPenalty = 0;
        if (g._currentDay >= st._lockedDay) {
            if (prevUnlocked) {
                /* Previously unlocked in stakeGoodAccounting(), so must have served full term */
                servedDays = st._stakedDays;
            } else {
                //require(g._currentDay >= st._lockedDay + 5, "JACKPOT: Stake must serve at least 5 days");
                _stakeUnlock(g, st);
                servedDays = g._currentDay.sub(st._lockedDay);
                if (servedDays > st._stakedDays) {
                    servedDays = st._stakedDays;
                }
            }
            (
                stakeReturn,
                payout,
                dividends,
                penalty,
                cappedPenalty
            ) = _stakePerformance(g, st, servedDays);
            msg.sender.transfer(dividends);
        } else {
            /* Stake hasn't been added to the total yet, so no penalties or rewards apply */
            g._nextStakeSharesTotal = g._nextStakeSharesTotal.sub(st._stakeShares);
            stakeReturn = st._stakedSuns;
        }
        emit StakeEnd(
            stakeIdParam,
            prevUnlocked ? 1 : 0,
            msg.sender,
            st._lockedDay,
            servedDays,
            st._stakedSuns,
            st._stakeShares,
            payout,
            penalty,
            stakeReturn
        );
        if (cappedPenalty != 0 && !prevUnlocked) {
            /* Split penalty proceeds only if not previously unlocked by stakeGoodAccounting() */
            g._stakePenaltyTotal = g._stakePenaltyTotal.add(cappedPenalty);
        }
        /* Pay the stake return, if any, to the staker */
        if (stakeReturn != 0) {
            _mint(msg.sender, stakeReturn);
            /* Update the share rate if necessary */
            _shareRateUpdate(g, st, stakeReturn);
        }
        g._lockedSunsTotal = g._lockedSunsTotal.sub(st._stakedSuns);
        stakeListRef[stakeIndex].unlockedDay = uint16(
            g._currentDay.mod(uint256(uint16(-1)))
        );
        UnstakeStore memory endedInfo;
        endedInfo.stakeId = stakeListRef[stakeIndex].stakeId;
        endedInfo.stakedSuns = stakeListRef[stakeIndex].stakedSuns;
        endedInfo.stakeShares = stakeListRef[stakeIndex].stakeShares;
        endedInfo.lockedDay = stakeListRef[stakeIndex].lockedDay;
        endedInfo.stakedDays = stakeListRef[stakeIndex].stakedDays;
        endedInfo.unlockedDay = stakeListRef[stakeIndex].unlockedDay;
        endedInfo.unstakePayout = stakeReturn;
        endedInfo.unstakeDividends = dividends;
        //endedStakeLists[_msgSender()].push(stakeListRef[stakeIndex]);
        endedStakeLists[_msgSender()].push(endedInfo);
        _stakeRemove(stakeListRef, stakeIndex);
        _globalsSync(g, gSnapshot);
    }

    uint256 private undestributedLotery = 0;

    function endLoteryDay(uint256 endDay) public onlyAfterNDays(0) {
        uint256 currDay = _currentDay();
        if (currDay == 0) return;
        if (endDay >= currDay) endDay = currDay.sub(1);
        if (
            endDay == currDay.sub(1) &&
            now % ROUND_TIME <= LOTERY_ENTRY_TIME &&
            endDay > 0
        ) endDay = endDay.sub(1);
        else if (
            endDay == currDay.sub(1) &&
            now % ROUND_TIME <= LOTERY_ENTRY_TIME &&
            endDay == 0
        ) return;
        while (lastEndedLoteryDay <= endDay) {
            uint256 ChanceCount = dayChanceCount[lastEndedLoteryDay];
            if (ChanceCount == 0) {
                undestributedLotery = undestributedLotery.add(xfLobby[lastEndedLoteryDay].mul(25).div(1000));
                lastEndedLoteryDay = lastEndedLoteryDay.add(1);
                continue;
            }
            uint256 randomInt = _random(ChanceCount);
            //uint256 randomInt = _random(10000);
            uint256 count = 0;
            uint256 ind = 0;
            while (count < randomInt) {
                uint256 newChanceCount =
                    loteryCount[lastEndedLoteryDay][ind].chanceCount;
                if (count.add(newChanceCount) >= randomInt) break;
                count = count.add(newChanceCount);
                ind = ind.add(1);
            }
            uint256 amount = xfLobby[lastEndedLoteryDay].mul(25).div(1000);
            if (undestributedLotery > 0) {
                amount = amount.add(undestributedLotery);
                undestributedLotery = 0;
            }
            winners[lastEndedLoteryDay] = winLoteryStat(
                address(uint160(loteryCount[lastEndedLoteryDay][ind].who)),
                amount,
                amount
            );
            lastEndedLoteryDayWithWinner = lastEndedLoteryDay;
            emit loteryWin(
                lastEndedLoteryDay,
                amount,
                winners[lastEndedLoteryDay].who
            );
            //delete loteryCount[lastEndedLoteryDay];
            lastEndedLoteryDay = lastEndedLoteryDay.add(1);
        }
    }

    function loteryCountLen(uint256 day) external view returns (uint256) {
        return loteryCount[day].length;
    }

    function withdrawLotery(uint256 day) public {
        if (winners[day].restAmount != 0) {
            winners[day].who.transfer(winners[day].restAmount);
            winners[day].restAmount = 0;
        }
    }

    uint256 private nonce = 0;

    function _random(uint256 limit) private returns (uint256) {
        uint256 randomnumber =
            uint256(
                keccak256(
                    abi.encodePacked(
                        now,
                        msg.sender,
                        nonce,
                        blockhash(block.number),
                        block.number,
                        block.coinbase,
                        block.difficulty
                    )
                )
            ) % limit;
        nonce = nonce.add(1);
        return randomnumber;
    }

    function endedStakeCount(address stakerAddr)
        external
        view
        returns (uint256)
    {
        return endedStakeLists[stakerAddr].length;
    }

    /**
     * @dev PUBLIC FACING: Return the current stake count for a staker address
     * @param stakerAddr Address of staker
     */
    function stakeCount(address stakerAddr) external view returns (uint256) {
        return stakeLists[stakerAddr].length;
    }

    /**
     * @dev Open a stake.
     * @param g Cache of stored globals
     * @param newStakedSuns Number of Suns to stake
     * @param newStakedDays Number of days to stake
     */
    function _stakeStart(
        GlobalsCache memory g,
        uint256 newStakedSuns,
        uint256 newStakedDays
    ) internal {
        /* Enforce the maximum stake time */
        require(
            newStakedDays <= MAX_STAKE_DAYS,
            "JACKPOT: newStakedDays higher than maximum"
        );
        uint256 bonusSuns = _stakeStartBonusSuns(newStakedSuns, newStakedDays);
        uint256 newStakeShares = newStakedSuns.add(bonusSuns).mul(SHARE_RATE_SCALE).div(g._shareRate);
        /* Ensure newStakedSuns is enough for at least one stake share */
        require(
            newStakeShares != 0,
            "JACKPOT: newStakedSuns must be at least minimum shareRate"
        );
        /*
            The stakeStart timestamp will always be part-way through the current
            day, so it needs to be rounded-up to the next day to ensure all
            stakes align with the same fixed calendar days. The current day is
            already rounded-down, so rounded-up is current day + 1.
        */
        uint256 newLockedDay = g._currentDay.add(1);
        /* Create Stake */
        g._latestStakeId = uint40(uint256(g._latestStakeId).add(1));
        uint40 newStakeId = g._latestStakeId;
        _stakeAdd(
            stakeLists[msg.sender],
            newStakeId,
            newStakedSuns,
            newStakeShares,
            newLockedDay,
            newStakedDays
        );
        emit StakeStart(
            newStakeId,
            msg.sender,
            newStakedSuns,
            newStakeShares,
            newStakedDays
        );
        /* Stake is added to total in the next round, not the current round */
        g._nextStakeSharesTotal = g._nextStakeSharesTotal.add(newStakeShares);
        /* Track total staked Suns for inflation calculations */
        g._lockedSunsTotal = g._lockedSunsTotal.add(newStakedSuns);
    }

    /**
     * @dev Calculates total stake payout including rewards for a multi-day range
     * @param stakeSharesParam Param from stake to calculate bonuses for
     * @param beginDay First day to calculate bonuses for
     * @param endDay Last day (non-inclusive) of range to calculate bonuses for
     * @return Payout in Suns
     */
    function calcPayoutRewards(
        uint256 stakeSharesParam,
        uint256 beginDay,
        uint256 endDay
    ) public view returns (uint256 payout) {
        uint256 currDay = _currentDay();
        require(beginDay <= currDay, "JACKPOT: Wrong argument for beginDay");
        require(
            endDay <= currDay && beginDay <= endDay,
            "JACKPOT: Wrong argument for endDay"
        );
        require(globals.latestStakeId != 0, "JACKPOT: latestStakeId error.");
        if (beginDay == endDay) return 0;
        uint256 counter;
        uint256 day = beginDay;
        while (day < endDay && day < globals.dailyDataCount) {
            uint256 dayPayout;
            dayPayout =
                dailyData[day].dayPayoutTotal.mul(stakeSharesParam).div(dailyData[day].dayStakeSharesTotal);
            if (counter < 4) {
                counter = counter.add(1);
            }
            /* Eligible to receive bonus */
            else {
                dayPayout =
                    dailyData[day].dayPayoutTotal.mul(stakeSharesParam).div(dailyData[day].dayStakeSharesTotal).mul(BONUS_DAY_SCALE);
                counter = 0;
            }
            payout = payout.add(dayPayout);
            day = day.add(1);
        }
        uint256 dayStakeSharesTotal =
            dailyData[uint256(globals.dailyDataCount).sub(1)].dayStakeSharesTotal;
        if (dayStakeSharesTotal == 0) dayStakeSharesTotal = stakeSharesParam;
        //require(dayStakeSharesTotal != 0, "JACKPOT: dayStakeSharesTotal == 0");
        uint256 dayPayoutTotal =
            dailyData[uint256(globals.dailyDataCount).sub(1)].dayPayoutTotal;
        while (day < endDay) {
            uint256 dayPayout;
            dayPayout =
                dayPayoutTotal.mul(stakeSharesParam).div(dayStakeSharesTotal);
            if (counter < 4) {
                counter = counter.add(1);
            }
            // Eligible to receive bonus
            else {
                dayPayout =
                    dayPayoutTotal.mul(stakeSharesParam).div(dayStakeSharesTotal).mul(BONUS_DAY_SCALE);
                counter = 0;
            }
            payout = payout.add(dayPayout);
            day = day.add(1);
        }
        return payout;
    }

    function calcPayoutRewardsBonusDays(
        uint256 stakeSharesParam,
        uint256 beginDay,
        uint256 endDay
    ) external view returns (uint256 payout) {
        uint256 currDay = _currentDay();
        require(beginDay <= currDay, "JACKPOT: Wrong argument for beginDay");
        require(
            endDay <= currDay && beginDay <= endDay,
            "JACKPOT: Wrong argument for endDay"
        );
        require(globals.latestStakeId != 0, "JACKPOT: latestStakeId error.");
        if (beginDay == endDay) return 0;
        uint256 day = beginDay.add(5);
        while (day < endDay && day < globals.dailyDataCount) {
            payout = payout.add(dailyData[day].dayPayoutTotal.mul(stakeSharesParam).div(dailyData[day].dayStakeSharesTotal));
            day = day.add(5);
        }
        uint256 dayStakeSharesTotal =
            dailyData[uint256(globals.dailyDataCount).sub(1)].dayStakeSharesTotal;
        if (dayStakeSharesTotal == 0) dayStakeSharesTotal = stakeSharesParam;
        //require(dayStakeSharesTotal != 0, "JACKPOT: dayStakeSharesTotal == 0");
        uint256 dayPayoutTotal =
            dailyData[uint256(globals.dailyDataCount).sub(1)].dayPayoutTotal;
        while (day < endDay) {
            payout = payout.add(dayPayoutTotal.mul(stakeSharesParam).div(dayStakeSharesTotal));
            day = day.add(5);
        }
        return payout;
    }

    /**
     * @dev Calculates user dividends
     * @param stakeSharesParam Param from stake to calculate bonuses for
     * @param beginDay First day to calculate bonuses for
     * @param endDay Last day (non-inclusive) of range to calculate bonuses for
     * @return Payout in Suns
     */
    function calcPayoutDividendsReward(
        uint256 stakeSharesParam,
        uint256 beginDay,
        uint256 endDay
    ) public view returns (uint256 payout) {
        uint256 currDay = _currentDay();
        require(beginDay <= currDay, "JACKPOT: Wrong argument for beginDay");
        require(
            endDay <= currDay && beginDay <= endDay,
            "JACKPOT: Wrong argument for endDay"
        );
        require(globals.latestStakeId != 0, "JACKPOT: latestStakeId error.");
        if (beginDay == endDay) return 0;
        uint256 day = beginDay;
        while (day < endDay && day < globals.dailyDataCount) {
            uint256 dayPayout;
            /* user's share of 90% of the day's dividends */
            dayPayout = dayPayout.add(dailyData[day].dayDividends.mul(90).div(100).mul(stakeSharesParam).div(dailyData[day].dayStakeSharesTotal));
            payout = payout.add(dayPayout);
            day = day.add(1);
        }
        uint256 dayStakeSharesTotal =
            dailyData[uint256(globals.dailyDataCount).sub(1)].dayStakeSharesTotal;
        if (dayStakeSharesTotal == 0) dayStakeSharesTotal = stakeSharesParam;
        //require(dayStakeSharesTotal != 0, "JACKPOT: dayStakeSharesTotal == 0");
        while (day < endDay) {
            uint256 dayPayout;
            /* user's share of 90% of the day's dividends */
            dayPayout = dayPayout.add(xfLobby[day].mul(90).div(100).mul(stakeSharesParam).div(dayStakeSharesTotal));
            payout = payout.add(dayPayout);
            day = day.add(1);
        }
        return payout;
    }

    /**
     * @dev Calculate bonus Suns for a new stake, if any
     * @param newStakedSuns Number of Suns to stake
     * @param newStakedDays Number of days to stake
     */
    function _stakeStartBonusSuns(uint256 newStakedSuns, uint256 newStakedDays)
        private
        pure
        returns (uint256 bonusSuns)
    {
        /*
            LONGER PAYS BETTER:
            If longer than 1 day stake is committed to, each extra day
            gives bonus shares of approximately 0.0548%, which is approximately 20%
            extra per year of increased stake length committed to, but capped to a
            maximum of 200% extra.
            extraDays       =  stakedDays - 1
            longerBonus%    = (extraDays / 364) * 20%
                            = (extraDays / 364) / 5
                            =  extraDays / 1820
                            =  extraDays / LPB
            extraDays       =  longerBonus% * 1820
            extraDaysMax    =  longerBonusMax% * 1820
                            =  200% * 1820
                            =  3640
                            =  LPB_MAX_DAYS
            BIGGER PAYS BETTER:
            Bonus percentage scaled 0% to 10% for the first 7M JACKPOT of stake.
            biggerBonus%    = (cappedSuns /  BPB_MAX_SUNS) * 10%
                            = (cappedSuns /  BPB_MAX_SUNS) / 10
                            =  cappedSuns / (BPB_MAX_SUNS * 10)
                            =  cappedSuns /  BPB
            COMBINED:
            combinedBonus%  =            longerBonus%  +  biggerBonus%
                                      cappedExtraDays     cappedSuns
                            =         ---------------  +  ------------
                                            LPB               BPB
                                cappedExtraDays * BPB     cappedSuns * LPB
                            =   ---------------------  +  ------------------
                                      LPB * BPB               LPB * BPB
                                cappedExtraDays * BPB  +  cappedSuns * LPB
                            =   --------------------------------------------
                                                  LPB  *  BPB
            bonusSuns     = suns * combinedBonus%
                            = suns * (cappedExtraDays * BPB  +  cappedSuns * LPB) / (LPB * BPB)
        */
        uint256 cappedExtraDays = 0;
        /* Must be more than 1 day for Longer-Pays-Better */
        if (newStakedDays > 1) {
            cappedExtraDays = newStakedDays.sub(1) <= LPB_MAX_DAYS
                ? newStakedDays.sub(1)
                : LPB_MAX_DAYS;
        }
        uint256 cappedStakedSuns =
            newStakedSuns <= BPB_MAX_SUNS ? newStakedSuns : BPB_MAX_SUNS;
        bonusSuns = cappedExtraDays.mul(BPB).add(cappedStakedSuns.mul(LPB));
        bonusSuns = newStakedSuns.mul(bonusSuns).div(LPB.mul(BPB));
        return bonusSuns;
    }

    function _stakeUnlock(GlobalsCache memory g, StakeCache memory st)
        private
        pure
    {
        g._stakeSharesTotal = g._stakeSharesTotal.sub(st._stakeShares);
        st._unlockedDay = g._currentDay;
    }

    function _stakePerformance(
        GlobalsCache memory g,
        StakeCache memory st,
        uint256 servedDays
    )
        private
        view
        returns (
            uint256 stakeReturn,
            uint256 payout,
            uint256 dividends,
            uint256 penalty,
            uint256 cappedPenalty
        )
    {
        if (servedDays < st._stakedDays) {
            (payout, penalty) = _calcPayoutAndEarlyPenalty(
                g,
                st._lockedDay,
                st._stakedDays,
                servedDays,
                st._stakeShares
            );
            stakeReturn = st._stakedSuns.add(payout);
            dividends = calcPayoutDividendsReward(
                st._stakeShares,
                st._lockedDay,
                st._lockedDay.add(servedDays)
            );
        } else {
            // servedDays must == stakedDays here
            payout = calcPayoutRewards(
                st._stakeShares,
                st._lockedDay,
                st._lockedDay.add(servedDays)
            );
            dividends = calcPayoutDividendsReward(
                st._stakeShares,
                st._lockedDay,
                st._lockedDay.add(servedDays)
            );
            stakeReturn = st._stakedSuns.add(payout);
            penalty = _calcLatePenalty(
                st._lockedDay,
                st._stakedDays,
                st._unlockedDay,
                stakeReturn
            );
        }
        if (penalty != 0) {
            if (penalty > stakeReturn) {
                /* Cannot have a negative stake return */
                cappedPenalty = stakeReturn;
                stakeReturn = 0;
            } else {
                /* Remove penalty from the stake return */
                cappedPenalty = penalty;
                stakeReturn = stakeReturn.sub(cappedPenalty);
            }
        }
        return (stakeReturn, payout, dividends, penalty, cappedPenalty);
    }

    function getUnstakeParams(
        address user,
        uint256 stakeIndex,
        uint40 stakeIdParam
    )
        external
        view
        returns (
            uint256 stakeReturn,
            uint256 payout,
            uint256 dividends,
            uint256 penalty,
            uint256 cappedPenalty
        )
    {
        GlobalsCache memory g;
        GlobalsCache memory gSnapshot;
        _globalsLoad(g, gSnapshot);
        StakeStore[] storage stakeListRef = stakeLists[user];
        /* require() is more informative than the default assert() */
        require(stakeListRef.length != 0, "JACKPOT: Empty stake list");
        require(
            stakeIndex < stakeListRef.length,
            "JACKPOT: stakeIndex invalid"
        );
        /* Get stake copy */
        StakeCache memory st;
        _stakeLoad(stakeListRef[stakeIndex], stakeIdParam, st);
        uint256 servedDays = 0;
        bool prevUnlocked = (st._unlockedDay != 0);
        //return (stakeReturn, payout, dividends, penalty, cappedPenalty);
        if (g._currentDay >= st._lockedDay) {
            if (prevUnlocked) {
                /* Previously unlocked in stakeGoodAccounting(), so must have served full term */
                servedDays = st._stakedDays;
            } else {
                _stakeUnlock(g, st);
                servedDays = g._currentDay.sub(st._lockedDay);
                if (servedDays > st._stakedDays) {
                    servedDays = st._stakedDays;
                }
            }
            (
                stakeReturn,
                payout,
                dividends,
                penalty,
                cappedPenalty
            ) = _stakePerformance(g, st, servedDays);
        } else {
            /* Stake hasn't been added to the total yet, so no penalties or rewards apply */
            stakeReturn = st._stakedSuns;
        }
        return (stakeReturn, payout, dividends, penalty, cappedPenalty);
    }

    function _calcPayoutAndEarlyPenalty(
        GlobalsCache memory g,
        uint256 lockedDayParam,
        uint256 stakedDaysParam,
        uint256 servedDays,
        uint256 stakeSharesParam
    ) private view returns (uint256 payout, uint256 penalty) {
        uint256 servedEndDay = lockedDayParam.add(servedDays);
        /* 50% of stakedDays (rounded up) with a minimum applied */
        uint256 penaltyDays = stakedDaysParam.add(1).div(2);
        if (penaltyDays < EARLY_PENALTY_MIN_DAYS) {
            penaltyDays = EARLY_PENALTY_MIN_DAYS;
        }
        if (servedDays == 0) {
            /* Fill penalty days with the estimated average payout */
            uint256 expected = _estimatePayoutRewardsDay(g, stakeSharesParam);
            penalty = expected.mul(penaltyDays);
            return (payout, penalty); // Actual payout was 0
        }
        if (penaltyDays < servedDays) {
            /*
                Simplified explanation of intervals where end-day is non-inclusive:
                penalty:    [lockedDay  ...  penaltyEndDay)
                delta:                      [penaltyEndDay  ...  servedEndDay)
                payout:     [lockedDay  .......................  servedEndDay)
            */
            uint256 penaltyEndDay = lockedDayParam.add(penaltyDays);
            penalty = calcPayoutRewards(
                stakeSharesParam,
                lockedDayParam,
                penaltyEndDay
            );
            uint256 delta =
                calcPayoutRewards(
                    stakeSharesParam,
                    penaltyEndDay,
                    servedEndDay
                );
            payout = penalty.add(delta);
            return (payout, penalty);
        }
        /* penaltyDays >= servedDays  */
        payout = calcPayoutRewards(
            stakeSharesParam,
            lockedDayParam,
            servedEndDay
        );
        if (penaltyDays == servedDays) {
            penalty = payout;
        } else {
            /*
                (penaltyDays > servedDays) means not enough days served, so fill the
                penalty days with the average payout from only the days that were served.
            */
            penalty = payout.mul(penaltyDays).div(servedDays);
        }
        return (payout, penalty);
    }

    function _calcLatePenalty(
        uint256 lockedDayParam,
        uint256 stakedDaysParam,
        uint256 unlockedDayParam,
        uint256 rawStakeReturn
    ) private pure returns (uint256) {
        /* Allow grace time before penalties accrue */
        uint256 maxUnlockedDay =
            lockedDayParam.add(stakedDaysParam).add(LATE_PENALTY_GRACE_DAYS);
        if (unlockedDayParam <= maxUnlockedDay) {
            return 0;
        }
        /* Calculate penalty as a percentage of stake return based on time */
        return rawStakeReturn.mul(unlockedDayParam.sub(maxUnlockedDay)).div(LATE_PENALTY_SCALE_DAYS);
    }

    function _shareRateUpdate(
        GlobalsCache memory g,
        StakeCache memory st,
        uint256 stakeReturn
    ) private {
        if (stakeReturn > st._stakedSuns) {
            /*
                Calculate the new shareRate that would yield the same number of shares if
                the user re-staked this stakeReturn, factoring in any bonuses they would
                receive in stakeStart().
            */
            uint256 bonusSuns =
                _stakeStartBonusSuns(stakeReturn, st._stakedDays);
            uint256 newShareRate =
                stakeReturn.add(bonusSuns).mul(SHARE_RATE_SCALE).div(st._stakeShares);
            if (newShareRate > SHARE_RATE_MAX) {
                /*
                    Realistically this can't happen, but there are contrived theoretical
                    scenarios that can lead to extreme values of newShareRate, so it is
                    capped to prevent them anyway.
                */
                newShareRate = SHARE_RATE_MAX;
            }
            if (newShareRate > g._shareRate) {
                g._shareRate = newShareRate;
                emit ShareRateChange(
                    st._stakeId,
                    block.timestamp,
                    newShareRate
                );
            }
        }
    }
}
function _stakeStartBonusSuns(uint256 newStakedSuns, uint256 newStakedDays)
    private
    pure
    returns (uint256 bonusSuns)
{
    /*
        LONGER PAYS BETTER:
        If longer than 1 day stake is committed to, each extra day
        gives bonus shares of approximately 0.0548%, which is approximately 20%
        extra per year of increased stake length committed to, but capped to a
        maximum of 200% extra.
        extraDays       =  stakedDays - 1
        longerBonus%    = (extraDays / 364) * 20%
                        = (extraDays / 364) / 5
                        =  extraDays / 1820
                        =  extraDays / LPB
        extraDays       =  longerBonus% * 1820
        extraDaysMax    =  longerBonusMax% * 1820
                        =  200% * 1820
                        =  3640
                        =  LPB_MAX_DAYS
        BIGGER PAYS BETTER:
        Bonus percentage scaled 0% to 10% for the first 7M JACKPOT of stake.
        biggerBonus%    = (cappedSuns /  BPB_MAX_SUNS) * 10%
                        = (cappedSuns /  BPB_MAX_SUNS) / 10
                        =  cappedSuns / (BPB_MAX_SUNS * 10)
                        =  cappedSuns /  BPB
        COMBINED:
        combinedBonus%  =            longerBonus%  +  biggerBonus%
                                  cappedExtraDays     cappedSuns
                        =         ---------------  +  ------------
                                        LPB               BPB
                            cappedExtraDays * BPB     cappedSuns * LPB
                        =   ---------------------  +  ------------------
                                  LPB * BPB               LPB * BPB
                            cappedExtraDays * BPB  +  cappedSuns * LPB
                        =   --------------------------------------------
                                              LPB  *  BPB
        bonusSuns     = suns * combinedBonus%
                        = suns * (cappedExtraDays * BPB  +  cappedSuns * LPB) / (LPB * BPB)
    */
    uint256 cappedExtraDays = 0;
    /* Must be more than 1 day for Longer-Pays-Better */
    if (newStakedDays > 1) {
        cappedExtraDays = newStakedDays.sub(1) <= LPB_MAX_DAYS
            ? newStakedDays.sub(1)
            : LPB_MAX_DAYS;
    }
    uint256 cappedStakedSuns =
        newStakedSuns <= BPB_MAX_SUNS ? newStakedSuns : BPB_MAX_SUNS;
    bonusSuns = cappedExtraDays.mul(BPB).add(cappedStakedSuns.mul(LPB));
    bonusSuns = newStakedSuns.mul(bonusSuns).div(LPB.mul(BPB));
    return bonusSuns;
}

/**
 * @dev Calculate bonus Suns for a new stake, if any
 * @param newStakedSuns Number of Suns to stake
 * @param newStakedDays Number of days to stake
 */