diff --git a/contracts/GlobalsAndUtility.sol b/contracts/GlobalsAndUtility.sol index 67a75af..7579d53 100644 --- a/contracts/GlobalsAndUtility.sol +++ b/contracts/GlobalsAndUtility.sol @@ -151,13 +151,21 @@ contract GlobalsAndUtility is ERC20 { uint8 internal constant BTC_ADDR_TYPE_P2WPKH_IN_P2SH = 2; uint8 internal constant BTC_ADDR_TYPE_COUNT = 3; + /* Starting Share Price */ + uint80 internal constant INITIAL_SHARES_PER_HEART = 1e18; + /* Globals expanded for memory (except _latestStakeId) and compact for storage */ struct GlobalsCache { // 1 uint256 _daysStored; + uint256 _stakedHeartsTotal; uint256 _stakeSharesTotal; uint256 _nextStakeSharesTotal; uint48 _latestStakeId; + // share price + uint80 _sharesPerHeart; + // total "paper" payouts for share price + uint256 _pendingPayoutTotal; // 2 uint256 _stakePenaltyPool; uint256 _unclaimedSatoshisTotal; @@ -170,9 +178,12 @@ contract GlobalsAndUtility is ERC20 { struct GlobalsStore { // 1 uint16 daysStored; - uint80 stakeSharesTotal; - uint80 nextStakeSharesTotal; + uint80 stakedHeartsTotal; + uint256 stakeSharesTotal; + uint256 nextStakeSharesTotal; uint48 latestStakeId; + uint80 sharesPerHeart; + uint80 pendingPayoutTotal; // 2 uint80 stakePenaltyPool; uint64 unclaimedSatoshisTotal; @@ -188,7 +199,7 @@ contract GlobalsAndUtility is ERC20 { /* Period data */ struct DailyDataStore { uint80 dayPayoutTotal; - uint80 dayStakeSharesTotal; + uint256 dayStakeSharesTotal; } mapping(uint256 => DailyDataStore) public dailyData; @@ -272,13 +283,15 @@ contract GlobalsAndUtility is ERC20 { function getGlobalInfo() external view - returns (uint256[11] memory) + returns (uint256[13] memory) { return [ globals.daysStored, + globals.stakedHeartsTotal, globals.stakeSharesTotal, globals.nextStakeSharesTotal, globals.latestStakeId, + globals.sharesPerHeart, globals.stakePenaltyPool, globals.unclaimedSatoshisTotal, globals.claimedSatoshisTotal, @@ -345,9 +358,13 @@ contract GlobalsAndUtility is ERC20 { { // 1 g._daysStored = globals.daysStored; + g._stakedHeartsTotal = globals.stakedHeartsTotal; g._stakeSharesTotal = globals.stakeSharesTotal; g._nextStakeSharesTotal = globals.nextStakeSharesTotal; g._latestStakeId = globals.latestStakeId; + g._sharesPerHeart = globals.sharesPerHeart; + + g._pendingPayoutTotal = globals.pendingPayoutTotal; // 2 g._stakePenaltyPool = globals.stakePenaltyPool; g._unclaimedSatoshisTotal = globals.unclaimedSatoshisTotal; @@ -363,9 +380,12 @@ contract GlobalsAndUtility is ERC20 { { // 1 gSnapshot._daysStored = g._daysStored; + gSnapshot._stakedHeartsTotal = g._stakedHeartsTotal; gSnapshot._stakeSharesTotal = g._stakeSharesTotal; gSnapshot._nextStakeSharesTotal = g._nextStakeSharesTotal; gSnapshot._latestStakeId = g._latestStakeId; + gSnapshot._sharesPerHeart = g._sharesPerHeart; + gSnapshot._pendingPayoutTotal = g._pendingPayoutTotal; // 2 gSnapshot._stakePenaltyPool = g._stakePenaltyPool; gSnapshot._unclaimedSatoshisTotal = g._unclaimedSatoshisTotal; @@ -377,18 +397,24 @@ contract GlobalsAndUtility is ERC20 { internal { globals.daysStored = uint16(g._daysStored); - globals.stakeSharesTotal = uint80(g._stakeSharesTotal); - globals.nextStakeSharesTotal = uint80(g._nextStakeSharesTotal); + globals.stakedHeartsTotal = uint80(g._stakedHeartsTotal); + globals.stakeSharesTotal = uint256(g._stakeSharesTotal); + globals.nextStakeSharesTotal = uint256(g._nextStakeSharesTotal); globals.latestStakeId = g._latestStakeId; + globals.sharesPerHeart = uint80(g._sharesPerHeart); + globals.pendingPayoutTotal = uint80(g._pendingPayoutTotal); } function _syncGlobals1(GlobalsCache memory g, GlobalsCache memory gSnapshot) internal { if (g._daysStored == gSnapshot._daysStored + && g._stakedHeartsTotal == gSnapshot._stakedHeartsTotal && g._stakeSharesTotal == gSnapshot._stakeSharesTotal && g._nextStakeSharesTotal == gSnapshot._nextStakeSharesTotal - && g._latestStakeId == gSnapshot._latestStakeId) { + && g._latestStakeId == gSnapshot._latestStakeId + && g._sharesPerHeart == gSnapshot._sharesPerHeart + && g._pendingPayoutTotal == gSnapshot._pendingPayoutTotal) { return; } _saveGlobals1(g); @@ -518,11 +544,15 @@ contract GlobalsAndUtility is ERC20 { rs._totalSupplyCached = totalSupply(); uint256 day = g._daysStored; + uint256 totalPendingPayout = g._pendingPayoutTotal; do { if (g._stakeSharesTotal != 0) { _calcDailyRound(g, rs, day); dailyData[day].dayPayoutTotal = uint80(rs._payoutTotal); - dailyData[day].dayStakeSharesTotal = uint80(g._stakeSharesTotal); + dailyData[day].dayStakeSharesTotal = g._stakeSharesTotal; + /* capture additional pending payout */ + totalPendingPayout += rs._payoutTotal; + } else { if (day == CLAIM_REWARD_DAYS && g._unclaimedSatoshisTotal != 0) { /* @@ -551,7 +581,24 @@ contract GlobalsAndUtility is ERC20 { uint16(day), msg.sender ); + + /* share price = (stakedHearts + payoutTotal) / sharesTotal + * to keep precision we always assume >1 shares per heart + * so our shares per hearts is 1 / price + * i.e. sharesTotal/(stakedHearts + payoutTotal) + */ + if(g._stakeSharesTotal == 0){ + // no stakes, leave price unchanged + } + else if(totalPendingPayout + g._stakedHeartsTotal == 0){ + // div by 0, instead reset to original price + g._sharesPerHeart = INITIAL_SHARES_PER_HEART; + } else { + g._sharesPerHeart = uint80(g._stakeSharesTotal / (totalPendingPayout + g._stakedHeartsTotal)); + } + g._daysStored = day; + g._pendingPayoutTotal = totalPendingPayout; if (rs._mintContractBatch != 0) { _mint(address(this), rs._mintContractBatch); diff --git a/contracts/HEX.sol b/contracts/HEX.sol index 7cb42fd..e0e25fc 100644 --- a/contracts/HEX.sol +++ b/contracts/HEX.sol @@ -9,6 +9,7 @@ contract HEX is StakeableToken { { /* Add all Satoshis from UTXO snapshot to contract */ globals.unclaimedSatoshisTotal = uint64(FULL_SATOSHIS_TOTAL); + globals.sharesPerHeart = INITIAL_SHARES_PER_HEART; _mint(address(this), FULL_SATOSHIS_TOTAL * HEARTS_PER_SATOSHI); } diff --git a/contracts/StakeableToken.sol b/contracts/StakeableToken.sol index ae95a5c..a6030e3 100644 --- a/contracts/StakeableToken.sol +++ b/contracts/StakeableToken.sol @@ -29,7 +29,7 @@ contract StakeableToken is UTXORedeemableToken { /* Check if log data needs to be updated */ _storeDailyDataBefore(g, g._currentDay); - uint256 newStakeShares = calcStakeShares(newStakedHearts, newStakedDays); + uint256 newStakeShares = calcStakeShares(newStakedHearts, newStakedDays, g._sharesPerHeart); /* The startStake timestamp will always be part-way through the current @@ -61,6 +61,9 @@ contract StakeableToken is UTXORedeemableToken { /* Stake is added to pool in next round, not current round */ g._nextStakeSharesTotal += newStakeShares; + /* capture total hearts staked for share price */ + g._stakedHeartsTotal += newStakedHearts; + /* Transfer staked Hearts to contract */ _transfer(msg.sender, address(this), newStakedHearts); @@ -113,6 +116,10 @@ contract StakeableToken is UTXORedeemableToken { st._stakedDays ); + /* remove hearts and pending payout from share price calculation */ + g._stakedHeartsTotal -= st._stakedHearts; + g._pendingPayoutTotal -= payout; + if (msg.sender == stakerAddr) { emit GoodAccountingBySelf( uint40(block.timestamp), @@ -177,11 +184,15 @@ contract StakeableToken is UTXORedeemableToken { uint256 payout = 0; uint256 penalty = 0; uint256 cappedPenalty = 0; + uint256 pendingValueRemoved = 0; + uint256 stakedHeartsRemoved = st._stakedHearts; if (g._currentDay >= st._pooledDay) { if (prevUnpooled) { /* Previously unpooled in goodAccounting(), so must have served full term */ servedDays = st._stakedDays; + //We were unpooled so we accounted for our staked hearts + stakedHeartsRemoved = 0; } else { _unpoolStake(g, st); @@ -192,6 +203,10 @@ contract StakeableToken is UTXORedeemableToken { } (stakeReturn, payout, penalty, cappedPenalty) = _calcStakeReturn(g, st, servedDays); + //Good accounting would remove our pending value so only count if we didn't do that + if(!prevUnpooled){ + pendingValueRemoved = payout; + } } else { /* Stake hasn't been added to the global pool yet, so no penalties or rewards apply */ g._nextStakeSharesTotal -= st._stakeShares; @@ -199,6 +214,11 @@ contract StakeableToken is UTXORedeemableToken { stakeReturn = st._stakedHearts; } + /* remove hearts from share price calculation */ + g._stakedHeartsTotal -= stakedHeartsRemoved; + /* remove pending value from share price calculation */ + g._pendingPayoutTotal -= pendingValueRemoved; + emit EndStake( uint40(block.timestamp), msg.sender, @@ -255,11 +275,12 @@ contract StakeableToken is UTXORedeemableToken { } /** - * @dev Calculate stakeShares for a new stake, including any bonus - * @param newStakedHearts Number of Hearts to stake + * @dev Apply LPB bonus for a new stake based on days + * @param newStakedHearts Number of hearts to stake * @param newStakedDays Number of days to stake + * @param sharesPerHeart Number of shares for each heart */ - function calcStakeShares(uint256 newStakedHearts, uint256 newStakedDays) + function calcStakeShares(uint256 newStakedHearts, uint256 newStakedDays, uint80 sharesPerHeart) private pure returns (uint256) @@ -321,6 +342,8 @@ contract StakeableToken is UTXORedeemableToken { stakeShares = hearts + combinedAmount */ + require(sharesPerHeart > 0, "HEX: share price undecidable"); + uint256 cappedExtraDays = 0; /* Must be more than 1 day for Longer-Pays-Better */ @@ -335,7 +358,7 @@ contract StakeableToken is UTXORedeemableToken { uint256 combinedAmount = cappedExtraDays * LPB_H + cappedStakedHearts * LPB_D; combinedAmount = newStakedHearts * combinedAmount / (LPB_D * LPB_H); - return newStakedHearts + combinedAmount; + return sharesPerHeart * (newStakedHearts + combinedAmount); } function _unpoolStake(GlobalsCache memory g, StakeCache memory st)