contract StakeableToken is GlobalsAndUtility {	
    /**	
     * @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	
    {	
        GlobalsCache memory g;	
        GlobalsCache memory gSnapshot;	
        _globalsLoad(g, gSnapshot);	
        /* Enforce the minimum stake time */	
        require(newStakedDays >= MIN_STAKE_DAYS, "CSNE: 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);	
        /* require() is more informative than the default assert() */	
        require(stakeLists[stakerAddr].length != 0, "CSNE: Empty stake list");	
        require(stakeIndex < stakeLists[stakerAddr].length, "CSNE: 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 + st._stakedDays, "CSNE: Stake not fully served");	
        /* Stake must still be locked */	
        require(st._unlockedDay == 0, "CSNE: 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 dividends, 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 += 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, "CSNE: Empty stake list");	
        require(stakeIndex < stakeListRef.length, "CSNE: stakeIndex invalid");	
        /* Get stake copy */	
        StakeCache memory st;	
        _stakeLoad(stakeListRef[stakeIndex], stakeIdParam, st);	
        /* Check if log data needs to be updated */	
        _dailyDataUpdateAuto(g);	
        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 {	
                _stakeUnlock(g, st);	
                servedDays = g._currentDay - 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 -= st._stakeShares;	
            stakeReturn = st._stakedSuns;	
        }	
        emit StakeEnd(	
            stakeIdParam, 	
            prevUnlocked ? 1 : 0,	
            msg.sender,	
            st._lockedDay,	
            servedDays, 	
            st._stakedSuns, 	
            st._stakeShares, 	
            dividends,	
            payout, 	
            penalty,	
            stakeReturn	
        );	
        if (cappedPenalty != 0 && !prevUnlocked) {	
            /* Split penalty proceeds only if not previously unlocked by stakeGoodAccounting() */	
            g._stakePenaltyTotal += 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 -= st._stakedSuns;	
        _stakeRemove(stakeListRef, stakeIndex);	
        _globalsSync(g, gSnapshot);	
    }	
    /**	
     * @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, "CSNE: newStakedDays higher than maximum");	
        uint256 bonusSuns = _stakeStartBonusSuns(newStakedSuns, newStakedDays);	
        uint256 newStakeShares = (newStakedSuns + bonusSuns) * SHARE_RATE_SCALE / g._shareRate;	
        /* Ensure newStakedSuns is enough for at least one stake share */	
        require(newStakeShares != 0, "CSNE: 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 + 1;	
        /* Create Stake */	
        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 += newStakeShares;	
        /* Track total staked Suns for inflation calculations */	
        g._lockedSunsTotal += newStakedSuns;	
    }	
    /**	
     * @dev Calculates total stake payout including rewards for a multi-day range	
     * @param g Cache of stored globals	
     * @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(	
        GlobalsCache memory g,	
        uint256 stakeSharesParam,	
        uint256 beginDay,	
        uint256 endDay	
    )	
        private	
        view	
        returns (uint256 payout)	
    {	
        uint256 counter;	
        for (uint256 day = beginDay; day < endDay; day++) {	
            uint256 dayPayout;	
            dayPayout = dailyData[day].dayPayoutTotal * stakeSharesParam	
                / dailyData[day].dayStakeSharesTotal;	
            if (counter < 4) {	
                counter++;	
            } 	
            /* Eligible to receive bonus */	
            else {	
                dayPayout = (dailyData[day].dayPayoutTotal * stakeSharesParam	
                / dailyData[day].dayStakeSharesTotal) * BONUS_DAY_SCALE;	
                counter = 0;	
            }	
            payout += dayPayout;	
        }	
        return payout;	
    }	
    /**	
     * @dev Calculates user dividends	
     * @param g Cache of stored globals	
     * @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(	
        GlobalsCache memory g,	
        uint256 stakeSharesParam,	
        uint256 beginDay,	
        uint256 endDay	
    )	
        private	
        view	
        returns (uint256 payout)	
    {	
        for (uint256 day = beginDay; day < endDay; day++) {	
            uint256 dayPayout;	
            /* user's share of 95% of the day's dividends */	
            dayPayout += ((dailyData[day].dayDividends * 95) / 100) * stakeSharesParam	
            / dailyData[day].dayStakeSharesTotal;	
            payout += dayPayout;	
        }	
        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)	
    {	
       
        uint256 cappedExtraDays = 0;	
        /* Must be more than 1 day for Longer-Pays-Better */	
        if (newStakedDays > 1) {	
            cappedExtraDays = newStakedDays <= LPB_MAX_DAYS ? newStakedDays - 1 : LPB_MAX_DAYS;	
        }	
        uint256 cappedStakedSuns = newStakedSuns <= BPB_MAX_SUNS	
            ? newStakedSuns	
            : BPB_MAX_SUNS;	
        bonusSuns = cappedExtraDays * BPB + cappedStakedSuns * LPB;	
        bonusSuns = newStakedSuns * bonusSuns / (LPB * BPB);	
        return bonusSuns;	
    }	
    function _stakeUnlock(GlobalsCache memory g, StakeCache memory st)	
        private	
        pure	
    {	
        g._stakeSharesTotal -= 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 + payout;	
            dividends = _calcPayoutDividendsReward(	
                g,	
                st._stakeShares,	
                st._lockedDay,	
                st._lockedDay + servedDays	
            );	
        } else {	
            // servedDays must == stakedDays here	
            payout = _calcPayoutRewards(	
                g,	
                st._stakeShares,	
                st._lockedDay,	
                st._lockedDay + servedDays	
            );	
            dividends = _calcPayoutDividendsReward(	
                g,	
                st._stakeShares,	
                st._lockedDay,	
                st._lockedDay + servedDays	
            );	
            stakeReturn = st._stakedSuns + 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 -= cappedPenalty;	
            }	
        }	
        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 + servedDays;	
        /* 50% of stakedDays (rounded up) with a minimum applied */	
        uint256 penaltyDays = (stakedDaysParam + 1) / 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, lockedDayParam);	
            penalty = expected * 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 + penaltyDays;	
            penalty = _calcPayoutRewards(g, stakeSharesParam, lockedDayParam, penaltyEndDay);	
            uint256 delta = _calcPayoutRewards(g, stakeSharesParam, penaltyEndDay, servedEndDay);	
            payout = penalty + delta;	
            return (payout, penalty);	
        }	
        /* penaltyDays >= servedDays  */	
        payout = _calcPayoutRewards(g, 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 * penaltyDays / 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 + stakedDaysParam + LATE_PENALTY_GRACE_DAYS;	
        if (unlockedDayParam <= maxUnlockedDay) {	
            return 0;	
        }	
        /* Calculate penalty as a percentage of stake return based on time */	
        return rawStakeReturn * (unlockedDayParam - maxUnlockedDay) / 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 + bonusSuns) * SHARE_RATE_SCALE / 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 stakeCount(address stakerAddr)	
    external	
    view	
    returns (uint256)	
{	
    return stakeLists[stakerAddr].length;	
}	

/**	
 * @dev PUBLIC FACING: Return the current stake count for a staker address	
 * @param stakerAddr Address of staker	
 */