From 11894fa8baa47fe1f315f66d9d6f4bc43c66154b Mon Sep 17 00:00:00 2001 From: Kyle Bahr Date: Fri, 12 Apr 2019 04:48:15 -0700 Subject: [PATCH 1/5] adds stake pricing as described `https://hastebin.com/yiyiwupose.txt` here by Greg The Magnificent --- contracts/GlobalsAndUtility.sol | 40 +++++++++++++++++++++++++++- contracts/HEX.sol | 1 + contracts/StakeableToken.sol | 46 ++++++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/contracts/GlobalsAndUtility.sol b/contracts/GlobalsAndUtility.sol index 25243dc..64ceeb7 100644 --- a/contracts/GlobalsAndUtility.sol +++ b/contracts/GlobalsAndUtility.sol @@ -139,13 +139,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 = 1e10; + /* 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 + uint256 _sharesPerHeart; + // total "paper" payouts for share price + uint256 _pendingPayoutTotal; // 2 uint256 _stakePenaltyPool; uint256 _unclaimedSatoshisTotal; @@ -158,9 +166,12 @@ contract GlobalsAndUtility is ERC20 { struct GlobalsStore { // 1 uint16 daysStored; + uint80 stakedHeartsTotal; uint80 stakeSharesTotal; uint80 nextStakeSharesTotal; uint48 latestStakeId; + uint80 sharesPerHeart; + uint80 pendingPayoutTotal; // 2 uint80 stakePenaltyPool; uint64 unclaimedSatoshisTotal; @@ -333,9 +344,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; @@ -351,9 +366,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; @@ -365,18 +383,24 @@ contract GlobalsAndUtility is ERC20 { internal { globals.daysStored = uint16(g._daysStored); + globals.stakedHeartsTotal = uint80(g._stakedHeartsTotal); globals.stakeSharesTotal = uint80(g._stakeSharesTotal); globals.nextStakeSharesTotal = uint80(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); @@ -506,11 +530,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); + /* capture additional pending payout */ + totalPendingPayout += rs._payoutTotal; + } else { if (day == CLAIM_REWARD_DAYS && g._unclaimedSatoshisTotal != 0) { /* @@ -539,7 +567,17 @@ 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) + */ + + g._sharesPerHeart = 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 20d5ef1..5866ee1 100644 --- a/contracts/StakeableToken.sol +++ b/contracts/StakeableToken.sol @@ -29,7 +29,8 @@ 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, g._sharesPerHeart); + newStakeShares = applyLongerPaysBetter(newStakeShares, newStakedDays); /* The startStake timestamp will always be part-way through the current @@ -61,6 +62,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 +117,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,6 +185,7 @@ contract StakeableToken is UTXORedeemableToken { uint256 payout = 0; uint256 penalty = 0; uint256 cappedPenalty = 0; + uint256 pendingValueRemoved = 0; if (g._currentDay >= st._pooledDay) { if (prevUnpooled) { @@ -192,13 +201,20 @@ contract StakeableToken is UTXORedeemableToken { } (stakeReturn, payout, penalty, cappedPenalty) = _calcStakeReturn(g, st, servedDays); + pendingValueRemoved = payout; } else { /* Stake hasn't been added to the global pool yet, so no penalties or rewards apply */ g._nextStakeSharesTotal -= st._stakeShares; stakeReturn = st._stakedHearts; + pendingValueRemoved = stakeReturn; } + /* remove hearts from share price calculation */ + g._stakedHeartsTotal -= st._stakedHearts; + /* remove pending value from share price calculation */ + g._pendingPayoutTotal -= pendingValueRemoved; + emit EndStake( uint40(block.timestamp), msg.sender, @@ -253,13 +269,31 @@ contract StakeableToken is UTXORedeemableToken { } return payout; } - + /** - * @dev Calculate stakeShares for a new stake, including any bonus + * @dev Calculate stakeShares for a new stake based on share price + * Price can slide between many shares per heart or many hearts per share depending on + * dynamic conditions. We manage this by tracking both, though it's only ever either one + * way or the other, i.e. one of the inputs is always 0 * @param newStakedHearts Number of Hearts to stake + * @param sharesPerHeart number of shares for each heart + */ + + function calcStakeShares(uint256 newStakedHearts, uint256 sharesPerHeart) + private + pure + returns (uint256) + { + require(sharesPerHeart > 0, "HEX: share price undecidable"); + return newStakedHearts * sharesPerHeart; + } + + /** + * @dev Apply LPB bonus for a new stake based on days + * @param newStakeShares Number of shares to stake * @param newStakedDays Number of days to stake */ - function calcStakeShares(uint256 newStakedHearts, uint256 newStakedDays) + function applyLongerPaysBetter(uint256 newStakeShares, uint256 newStakedDays) private pure returns (uint256) @@ -291,14 +325,14 @@ contract StakeableToken is UTXORedeemableToken { */ if (newStakedDays <= 1) { /* No bonus shares if there are no extra days */ - return newStakedHearts; + return newStakeShares; } uint256 extraDays = newStakedDays < 3641 ? newStakedDays - 1 : 3640; - return newStakedHearts + newStakedHearts * extraDays / 1820; + return newStakeShares + newStakeShares * extraDays / 1820; } function _unpoolStake(GlobalsCache memory g, StakeCache memory st) From 026fd64624b5e69654ac41ebdf2d3c972b65a944 Mon Sep 17 00:00:00 2001 From: Kyle Bahr Date: Fri, 12 Apr 2019 06:26:14 -0700 Subject: [PATCH 2/5] increasing resolution on shares --- contracts/GlobalsAndUtility.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/GlobalsAndUtility.sol b/contracts/GlobalsAndUtility.sol index 64ceeb7..6b599fe 100644 --- a/contracts/GlobalsAndUtility.sol +++ b/contracts/GlobalsAndUtility.sol @@ -140,7 +140,7 @@ contract GlobalsAndUtility is ERC20 { uint8 internal constant BTC_ADDR_TYPE_COUNT = 3; /* Starting Share Price */ - uint80 internal constant INITIAL_SHARES_PER_HEART = 1e10; + uint80 internal constant INITIAL_SHARES_PER_HEART = 1e18; /* Globals expanded for memory (except _latestStakeId) and compact for storage */ struct GlobalsCache { From 487aebf6f5f37eedcf3d345cd4d7b977a0d99349 Mon Sep 17 00:00:00 2001 From: Kyle Bahr Date: Fri, 12 Apr 2019 09:41:35 -0700 Subject: [PATCH 3/5] update types to be able to hold relevant info and noticed a data read function that needs new global info --- contracts/GlobalsAndUtility.sol | 29 +++++++++++++++++++---------- contracts/StakeableToken.sol | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/contracts/GlobalsAndUtility.sol b/contracts/GlobalsAndUtility.sol index 6b599fe..3f3980b 100644 --- a/contracts/GlobalsAndUtility.sol +++ b/contracts/GlobalsAndUtility.sol @@ -151,7 +151,7 @@ contract GlobalsAndUtility is ERC20 { uint256 _nextStakeSharesTotal; uint48 _latestStakeId; // share price - uint256 _sharesPerHeart; + uint80 _sharesPerHeart; // total "paper" payouts for share price uint256 _pendingPayoutTotal; // 2 @@ -167,8 +167,8 @@ contract GlobalsAndUtility is ERC20 { // 1 uint16 daysStored; uint80 stakedHeartsTotal; - uint80 stakeSharesTotal; - uint80 nextStakeSharesTotal; + uint256 stakeSharesTotal; + uint256 nextStakeSharesTotal; uint48 latestStakeId; uint80 sharesPerHeart; uint80 pendingPayoutTotal; @@ -187,7 +187,7 @@ contract GlobalsAndUtility is ERC20 { /* Period data */ struct DailyDataStore { uint80 dayPayoutTotal; - uint80 dayStakeSharesTotal; + uint256 dayStakeSharesTotal; } mapping(uint256 => DailyDataStore) public dailyData; @@ -271,13 +271,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, @@ -384,8 +386,8 @@ contract GlobalsAndUtility is ERC20 { { globals.daysStored = uint16(g._daysStored); globals.stakedHeartsTotal = uint80(g._stakedHeartsTotal); - globals.stakeSharesTotal = uint80(g._stakeSharesTotal); - globals.nextStakeSharesTotal = uint80(g._nextStakeSharesTotal); + globals.stakeSharesTotal = uint256(g._stakeSharesTotal); + globals.nextStakeSharesTotal = uint256(g._nextStakeSharesTotal); globals.latestStakeId = g._latestStakeId; globals.sharesPerHeart = uint80(g._sharesPerHeart); globals.pendingPayoutTotal = uint80(g._pendingPayoutTotal); @@ -535,7 +537,7 @@ contract GlobalsAndUtility is ERC20 { 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; @@ -573,8 +575,15 @@ contract GlobalsAndUtility is ERC20 { * so our shares per hearts is 1 / price * i.e. sharesTotal/(stakedHearts + payoutTotal) */ - - g._sharesPerHeart = g._stakeSharesTotal / (totalPendingPayout + g._stakedHeartsTotal); + 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; diff --git a/contracts/StakeableToken.sol b/contracts/StakeableToken.sol index 5866ee1..b77c3aa 100644 --- a/contracts/StakeableToken.sol +++ b/contracts/StakeableToken.sol @@ -279,7 +279,7 @@ contract StakeableToken is UTXORedeemableToken { * @param sharesPerHeart number of shares for each heart */ - function calcStakeShares(uint256 newStakedHearts, uint256 sharesPerHeart) + function calcStakeShares(uint256 newStakedHearts, uint80 sharesPerHeart) private pure returns (uint256) From ac66c74e639d2475881b1d04f9f29d7f3bb4b53a Mon Sep 17 00:00:00 2001 From: Kyle Bahr Date: Sat, 13 Apr 2019 03:50:53 -0700 Subject: [PATCH 4/5] minor defect where we double counted shares --- contracts/StakeableToken.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/StakeableToken.sol b/contracts/StakeableToken.sol index afe3541..0c5004d 100644 --- a/contracts/StakeableToken.sol +++ b/contracts/StakeableToken.sol @@ -206,7 +206,7 @@ contract StakeableToken is UTXORedeemableToken { g._nextStakeSharesTotal -= st._stakeShares; stakeReturn = st._stakedHearts; - pendingValueRemoved = stakeReturn; + pendingValueRemoved = 0; } /* remove hearts from share price calculation */ From 90bf1f0a3e5c1efefa51c2957fff2adbc1f6a246 Mon Sep 17 00:00:00 2001 From: Kyle Bahr Date: Sun, 14 Apr 2019 03:51:59 -0700 Subject: [PATCH 5/5] bug fix - was double counting if goodAccounting was run before endStake --- contracts/StakeableToken.sol | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/StakeableToken.sol b/contracts/StakeableToken.sol index 0c5004d..a6030e3 100644 --- a/contracts/StakeableToken.sol +++ b/contracts/StakeableToken.sol @@ -185,11 +185,14 @@ contract StakeableToken is UTXORedeemableToken { 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); @@ -200,17 +203,19 @@ contract StakeableToken is UTXORedeemableToken { } (stakeReturn, payout, penalty, cappedPenalty) = _calcStakeReturn(g, st, servedDays); - pendingValueRemoved = payout; + //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; stakeReturn = st._stakedHearts; - pendingValueRemoved = 0; } /* remove hearts from share price calculation */ - g._stakedHeartsTotal -= st._stakedHearts; + g._stakedHeartsTotal -= stakedHeartsRemoved; /* remove pending value from share price calculation */ g._pendingPayoutTotal -= pendingValueRemoved;