From 046d0a2a3c59542f7eefd8ff195985215e7447b7 Mon Sep 17 00:00:00 2001 From: Anthony Fieroni Date: Mon, 22 Mar 2021 17:13:18 +0200 Subject: [PATCH] Correct update liquidity on rewards calculation Signed-off-by: Anthony Fieroni --- src/consensus/params.h | 4 + src/masternodes/gv.h | 4 +- src/masternodes/masternodes.cpp | 8 +- src/masternodes/mn_checks.cpp | 34 ++--- src/masternodes/poolpairs.cpp | 253 ++++++++++++++++++++----------- src/masternodes/poolpairs.h | 122 +++------------ src/masternodes/rpc_accounts.cpp | 13 +- src/masternodes/rpc_poolpair.cpp | 12 +- src/test/liquidity_tests.cpp | 58 ++++--- src/validation.cpp | 3 +- 10 files changed, 252 insertions(+), 259 deletions(-) diff --git a/src/consensus/params.h b/src/consensus/params.h index 279a3548fa8..d627e0624b1 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -103,6 +103,10 @@ struct Params { }; PoS pos; + uint32_t blocksPerDay() const { + static const uint32_t blocks = 60 * 60 * 24 / pos.nTargetSpacing; + return blocks; + } /** * Minimum blocks including miner confirmation of the total of 2016 blocks in a retargeting period, * (nTargetTimespan / nTargetSpacing) which is also used for BIP9 deployments. diff --git a/src/masternodes/gv.h b/src/masternodes/gv.h index 3edfcc745c6..aa7c72b14e7 100644 --- a/src/masternodes/gv.h +++ b/src/masternodes/gv.h @@ -25,8 +25,8 @@ class GovVariable virtual Res Import(UniValue const &) = 0; virtual UniValue Export() const = 0; /// @todo it looks like Validate+Apply may be redundant. refactor for one? - virtual Res Validate(CCustomCSView const &mnview) const = 0; - virtual Res Apply(CCustomCSView &mnview, uint32_t height) = 0; + virtual Res Validate(CCustomCSView const &) const = 0; + virtual Res Apply(CCustomCSView &, uint32_t) = 0; virtual void Serialize(CVectorWriter& s) const = 0; virtual void Unserialize(VectorReader& s) = 0; diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index 55aede0cf87..d51286d780c 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -767,10 +767,12 @@ bool CCustomCSView::CalculateOwnerRewards(CScript const & owner, uint32_t target if (!height || *height >= targetHeight) { return true; // no share or target height is before a pool share' one } + auto onLiquidity = [&]() -> CAmount { + return GetBalance(owner, poolId).nValue; + }; auto beginHeight = std::max(*height, balanceHeight); - CalculatePoolRewards(poolId, GetBalance(owner, poolId).nValue, beginHeight, targetHeight, - [&](CScript const & from, uint8_t type, CTokenAmount amount, uint32_t begin, uint32_t end) { - amount.nValue *= (end - begin); + CalculatePoolRewards(poolId, onLiquidity, beginHeight, targetHeight, + [&](CScript const & from, uint8_t type, CTokenAmount amount, uint32_t height) { if (!from.empty()) { auto res = SubBalance(from, amount); if (!res) { diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index b7c2b7a0261..6a21b5812e7 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -658,6 +658,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor auto pairSymbol = obj.pairSymbol; poolPair.creationTx = tx.GetHash(); poolPair.creationHeight = height; + auto& rewards = poolPair.rewards; auto tokenA = mnview.GetToken(poolPair.idTokenA); if (!tokenA) { @@ -691,19 +692,16 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return std::move(tokenId); } - auto res = mnview.SetPoolPair(tokenId, height, poolPair); - if (!res) { - return res; - } - - if (!obj.rewards.balances.empty()) { - auto rewards = obj.rewards; + rewards = obj.rewards; + if (!rewards.balances.empty()) { // Check tokens exist and remove empty reward amounts auto res = eraseEmptyBalances(rewards.balances); - // Will only fail if pool was not actually created in SetPoolPair - return !res ? res : mnview.SetPoolCustomReward(tokenId, height, rewards); + if (!res) { + return res; + } } - return Res::Ok(); + + return mnview.SetPoolPair(tokenId, height, poolPair); } Res operator()(const CUpdatePoolPairMessage& obj) const { @@ -712,24 +710,20 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("tx not from foundation member"); } - auto res = mnview.UpdatePoolPair(obj.poolId, height, obj.status, obj.commission, obj.ownerAddress); - if (!res) { - return res; - } - - if (!obj.rewards.balances.empty()) { + auto rewards = obj.rewards; + if (!rewards.balances.empty()) { // Check for special case to wipe rewards - auto rewards = obj.rewards; if (rewards.balances.size() == 1 && rewards.balances.cbegin()->first == DCT_ID{std::numeric_limits::max()} && rewards.balances.cbegin()->second == std::numeric_limits::max()) { rewards.balances.clear(); } // Check if tokens exist and remove empty reward amounts auto res = eraseEmptyBalances(rewards.balances); - // Will only fail if pool was not actually created in SetPoolPair - return !res ? res : mnview.SetPoolCustomReward(obj.poolId, height, rewards); + if (!res) { + return res; + } } - return Res::Ok(); + return mnview.UpdatePoolPair(obj.poolId, height, obj.status, obj.commission, obj.ownerAddress, obj.rewards); } Res operator()(const CPoolSwapMessage& obj) const { diff --git a/src/masternodes/poolpairs.cpp b/src/masternodes/poolpairs.cpp index 85f77169553..fe651f755a0 100644 --- a/src/masternodes/poolpairs.cpp +++ b/src/masternodes/poolpairs.cpp @@ -10,8 +10,7 @@ const unsigned char CPoolPairView::ByID ::prefix = 'i'; const unsigned char CPoolPairView::ByPair ::prefix = 'j'; const unsigned char CPoolPairView::ByShare ::prefix = 'k'; -const unsigned char CPoolPairView::Reward ::prefix = 'I'; -const unsigned char CPoolPairView::ByDailyReward ::prefix = 'C'; +const unsigned char CPoolPairView::ByDailyReward ::prefix = 'I'; const unsigned char CPoolPairView::ByPoolHeight ::prefix = 'P'; Res CPoolPairView::SetPoolPair(DCT_ID const & poolId, uint32_t height, CPoolPair const & pool) @@ -33,7 +32,7 @@ Res CPoolPairView::SetPoolPair(DCT_ID const & poolId, uint32_t height, CPoolPair WriteBy(ByPairKey{pool.idTokenA, pool.idTokenB}, poolId); WriteBy(ByPairKey{pool.idTokenB, pool.idTokenA}, poolId); if (height < UINT_MAX) { - WriteBy(PoolRewardKey{poolId, height}, pool); + WriteBy(PoolHeightKey{poolId, height}, pool); } return Res::Ok(); } @@ -44,7 +43,7 @@ Res CPoolPairView::SetPoolPair(DCT_ID const & poolId, uint32_t height, CPoolPair && poolPairByTokens->second.idTokenB == pool.idTokenB) { WriteBy(poolId, pool); if (height < UINT_MAX) { - WriteBy(PoolRewardKey{poolId, height}, pool); + WriteBy(PoolHeightKey{poolId, height}, pool); } return Res::Ok(); } @@ -60,7 +59,7 @@ Res CPoolPairView::SetPoolPair(DCT_ID const & poolId, uint32_t height, CPoolPair return Res::Err("Error: Couldn't create/update pool pair."); } -Res CPoolPairView::UpdatePoolPair(DCT_ID const & poolId, uint32_t height, bool status, CAmount const & commission, CScript const & ownerAddress) +Res CPoolPairView::UpdatePoolPair(DCT_ID const & poolId, uint32_t height, bool status, CAmount const & commission, CScript const & ownerAddress, CBalances const & rewards) { auto poolPair = GetPoolPair(poolId); if (!poolPair) { @@ -86,6 +85,11 @@ Res CPoolPairView::UpdatePoolPair(DCT_ID const & poolId, uint32_t height, bool s pool.ownerAddress = ownerAddress; } + if (pool.rewards != rewards) { + usedHeight = height; + pool.rewards = rewards; + } + auto res = SetPoolPair(poolId, usedHeight, pool); if (!res.ok) { return Res::Err("Update poolpair: %s" , res.msg); @@ -110,108 +114,170 @@ boost::optional > CPoolPairView::GetPoolPair(const return {}; } -Res CPoolPairView::SetPoolCustomReward(const DCT_ID &poolId, uint32_t height, const CBalances& rewards) -{ - if (!GetPoolPair(poolId)) { - return Res::Err("Error %s: poolID %s does not exist", __func__, poolId.ToString()); - } +template +inline bool IterReadKey(std::unique_ptr & it, T & dbKey, DCT_ID const & poolId) { + return it->Valid() && CPoolPairView::ReadDbKey(it->Key(), dbKey) && dbKey.second.poolID == poolId; +} - WriteBy(PoolRewardKey{poolId, height}, rewards); - return Res::Ok(); +template +inline uint32_t IterReadKeyHeight(std::unique_ptr & it, T & dbKey, DCT_ID const & poolId) { + return IterReadKey(it, dbKey, poolId) ? dbKey.second.height : UINT_MAX; } -boost::optional CPoolPairView::GetPoolCustomReward(const DCT_ID &poolId) -{ - auto dbKey = MakeDbKey(PoolRewardKey{poolId, UINT_MAX}); - auto it = LowerBound(dbKey); - if (!it->Valid() || !ReadDbKey(it->Key(), dbKey) || dbKey.second.poolID != poolId) { - return {}; - } - return ReadValue(it->Value()); +inline CAmount poolRewardPerBlock(CAmount dailyReward, CAmount rewardPct) { + return dailyReward / Params().GetConsensus().blocksPerDay() * rewardPct / COIN; } -void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, CAmount liquidity, uint32_t begin, uint32_t end, std::function onReward) { - if (begin >= end || liquidity == 0) { +void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, std::function onLiquidity, uint32_t begin, uint32_t end, std::function onReward) { + if (begin >= end) { return; } constexpr const uint32_t PRECISION = 10000; const auto newCalcHeight = uint32_t(Params().GetConsensus().BayfrontGardensHeight); - const auto blocksPerDay = (60 * 60 * 24 / Params().GetConsensus().pos.nTargetSpacing); - const auto beginCustomRewards = std::max(begin, uint32_t(Params().GetConsensus().ClarkeQuayHeight)); - auto dbPct = MakeDbKey(PoolRewardKey{{}, end - 1}); - auto dbReward = MakeDbKey(PoolRewardKey{poolId, end - 1}); - auto dbPool = MakeDbKey(PoolRewardKey{poolId, end - 1}); + /// @Note we store keys in desc oreder so Prev is actually go in forward + auto dbPct = MakeDbKey(PoolHeightKey{{}, begin}); + auto dbPool = MakeDbKey(PoolHeightKey{poolId, begin}); auto itPct = LowerBound(dbPct); auto itPool = LowerBound(dbPool); - auto itReward = LowerBound(dbReward); - while (itPool->Valid() && ReadDbKey(itPool->Key(), dbPool) && dbPool.second.poolID == poolId) { + CAmount dailyReward = 0; + auto dailyBeginHeight = UINT_MAX; + while (!IterReadKey(itPct, dbPct, DCT_ID{}) && dbPct.second.height < end) { + dbPct.second.height++; + itPct->Seek(DbTypeToBytes(dbPct)); + } + if (dbPct.second.height < end) { + dailyReward = ReadValue(itPct->Value()); + dailyBeginHeight = dbPct.second.height; + itPct->Prev(); + } + auto dailyEndHeight = IterReadKeyHeight(itPct, dbPct, DCT_ID{}); + while (IterReadKey(itPool, dbPool, poolId)) { // rewards starting in same block as pool const auto poolHeight = dbPool.second.height; const auto pool = ReadValue(itPool->Value()); - // daily rewards - for (auto endHeight = end; itPct->Valid() && ReadDbKey(itPct->Key(), dbPct); itPct->Next()) { - // we have desc order so we select higher height - auto beginHeight = std::max(begin, std::max(poolHeight, dbPct.second.height)); - auto poolReward = ReadValue(itPct->Value()) / blocksPerDay * pool.rewardPct / COIN; - if (poolReward != 0 && pool.totalLiquidity != 0) { - auto beginCalcHeight = beginHeight; - auto endCalcHeight = std::min(endHeight, newCalcHeight); - if (endCalcHeight > beginHeight) { // old calculation - uint32_t liqWeight = liquidity * PRECISION / pool.totalLiquidity; - auto providerReward = poolReward * liqWeight / PRECISION; - onReward({}, uint8_t(RewardType::Rewards), {DCT_ID{0}, providerReward}, beginHeight, endCalcHeight); - beginCalcHeight = endCalcHeight; - } - if (endHeight > beginCalcHeight) { // new calculation - auto providerReward = static_cast((arith_uint256(poolReward) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); - onReward({}, uint8_t(RewardType::Rewards), {DCT_ID{0}, providerReward}, beginCalcHeight, endHeight); - } + auto poolReward = poolRewardPerBlock(dailyReward, pool.rewardPct); + itPool->Prev(); + auto endHeight = std::min(end, IterReadKeyHeight(itPool, dbPool, poolId)); + for (auto height = begin; height < endHeight; height++) { + if (height == dailyEndHeight) { + dailyReward = ReadValue(itPct->Value()); + poolReward = poolRewardPerBlock(dailyReward, pool.rewardPct); + itPct->Prev(); + dailyEndHeight = IterReadKeyHeight(itPct, dbPct, DCT_ID{}); } - if (beginHeight == begin || beginHeight == poolHeight) { - break; + if (pool.totalLiquidity == 0) { + continue; } - endHeight = beginHeight; - } - // commissions - if (pool.swapEvent && pool.totalLiquidity != 0) { - CAmount feeA, feeB; - if (poolHeight < newCalcHeight) { - uint32_t liqWeight = liquidity * PRECISION / pool.totalLiquidity; - feeA = pool.blockCommissionA * liqWeight / PRECISION; - feeB = pool.blockCommissionB * liqWeight / PRECISION; - } else { - feeA = static_cast((arith_uint256(pool.blockCommissionA) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); - feeB = static_cast((arith_uint256(pool.blockCommissionB) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); + auto liquidity = onLiquidity(); + // daily rewards + if (height >= dailyBeginHeight && poolReward != 0) { + CAmount providerReward; + if (height < newCalcHeight) { // old calculation + uint32_t liqWeight = liquidity * PRECISION / pool.totalLiquidity; + providerReward = poolReward * liqWeight / PRECISION; + } else { // new calculation + providerReward = static_cast((arith_uint256(poolReward) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); + } + onReward({}, uint8_t(RewardType::Rewards), {DCT_ID{0}, providerReward}, height); } - onReward({}, uint8_t(RewardType::Commission), {pool.idTokenA, feeA}, poolHeight, poolHeight + 1); - onReward({}, uint8_t(RewardType::Commission), {pool.idTokenB, feeB}, poolHeight, poolHeight + 1); - } - // custom rewards - if (end > beginCustomRewards) { - for (auto endHeight = end; itReward->Valid() && ReadDbKey(itReward->Key(), dbReward) && dbReward.second.poolID == poolId; itReward->Next()) { - auto beginHeight = std::max(beginCustomRewards, std::max(poolHeight, dbReward.second.height)); - if (endHeight > beginHeight && pool.totalLiquidity != 0) { - for (const auto& reward : ReadValue(itReward->Value()).balances) { - if (auto providerReward = static_cast((arith_uint256(reward.second) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64())) { - onReward(pool.ownerAddress, uint8_t(RewardType::Rewards), {reward.first, providerReward}, beginHeight, endHeight); - } - } + // commissions + if (height == poolHeight && pool.swapEvent) { + CAmount feeA, feeB; + if (height < newCalcHeight) { + uint32_t liqWeight = liquidity * PRECISION / pool.totalLiquidity; + feeA = pool.blockCommissionA * liqWeight / PRECISION; + feeB = pool.blockCommissionB * liqWeight / PRECISION; + } else { + feeA = static_cast((arith_uint256(pool.blockCommissionA) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); + feeB = static_cast((arith_uint256(pool.blockCommissionB) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); } - if (beginHeight == beginCustomRewards || beginHeight == poolHeight) { - break; + onReward({}, uint8_t(RewardType::Commission), {pool.idTokenA, feeA}, height); + onReward({}, uint8_t(RewardType::Commission), {pool.idTokenB, feeB}, height); + } + // custom rewards + for (const auto& reward : pool.rewards.balances) { + if (auto providerReward = static_cast((arith_uint256(reward.second) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64())) { + onReward(pool.ownerAddress, uint8_t(RewardType::Rewards), {reward.first, providerReward}, height); } - endHeight = beginHeight; } } - if (begin >= poolHeight) { + if (endHeight == end) { break; } - itPool->Next(); - end = poolHeight; + begin = endHeight; } } -Res CPoolPair::Swap(CTokenAmount in, PoolPrice const & maxPrice, std::function onTransfer, int height) { +Res CPoolPair::AddLiquidity(CAmount amountA, CAmount amountB, std::function onMint, bool slippageProtection) { + // instead of assertion due to tests + if (amountA <= 0 || amountB <= 0) { + return Res::Err("amounts should be positive"); + } + + CAmount liquidity{0}; + if (totalLiquidity == 0) { + liquidity = (CAmount) (arith_uint256(amountA) * arith_uint256(amountB)).sqrt().GetLow64(); // sure this is below std::numeric_limits::max() due to sqrt natue + if (liquidity <= MINIMUM_LIQUIDITY) { // ensure that it'll be non-zero + return Res::Err("liquidity too low"); + } + liquidity -= MINIMUM_LIQUIDITY; + // MINIMUM_LIQUIDITY is a hack for non-zero division + totalLiquidity = MINIMUM_LIQUIDITY; + } else { + CAmount liqA = (arith_uint256(amountA) * arith_uint256(totalLiquidity) / reserveA).GetLow64(); + CAmount liqB = (arith_uint256(amountB) * arith_uint256(totalLiquidity) / reserveB).GetLow64(); + liquidity = std::min(liqA, liqB); + + if (liquidity == 0) { + return Res::Err("amounts too low, zero liquidity"); + } + + if(slippageProtection) { + if ((std::max(liqA, liqB) - liquidity) * 100 / liquidity >= 3) { + return Res::Err("Exceeds max ratio slippage protection of 3%%"); + } + } + } + + // increasing totalLiquidity + auto resTotal = SafeAdd(totalLiquidity, liquidity); + if (!resTotal.ok) { + return Res::Err("can't add %d to totalLiquidity: %s", liquidity, resTotal.msg); + } + totalLiquidity = *resTotal.val; + + // increasing reserves + auto resA = SafeAdd(reserveA, amountA); + auto resB = SafeAdd(reserveB, amountB); + if (resA.ok && resB.ok) { + reserveA = *resA.val; + reserveB = *resB.val; + } else { + return Res::Err("overflow when adding to reserves"); + } + + return onMint(liquidity); +} + +Res CPoolPair::RemoveLiquidity(CAmount liqAmount, std::function onReclaim) { + // instead of assertion due to tests + // IRL it can't be more than "total-1000", and was checked indirectly by balances before. but for tests and incapsulation: + if (liqAmount <= 0 || liqAmount >= totalLiquidity) { + return Res::Err("incorrect liquidity"); + } + + CAmount resAmountA, resAmountB; + resAmountA = (arith_uint256(liqAmount) * arith_uint256(reserveA) / totalLiquidity).GetLow64(); + resAmountB = (arith_uint256(liqAmount) * arith_uint256(reserveB) / totalLiquidity).GetLow64(); + + reserveA -= resAmountA; // safe due to previous math + reserveB -= resAmountB; + totalLiquidity -= liqAmount; + + return onReclaim(resAmountA, resAmountB); +} + +Res CPoolPair::Swap(CTokenAmount in, PoolPrice const & maxPrice, std::function onTransfer, int height) { if (in.nTokenId != idTokenA && in.nTokenId != idTokenB) return Res::Err("Error, input token ID (" + in.nTokenId.ToString() + ") doesn't match pool tokens (" + idTokenA.ToString() + "," + idTokenB.ToString() + ")"); @@ -292,13 +358,30 @@ CAmount CPoolPair::slopeSwap(CAmount unswapped, CAmount &poolFrom, CAmount &pool return swapped.GetLow64(); } -void CPoolPairView::ForEachPoolPair(std::function)> callback, DCT_ID const & start) -{ +Res CPoolPairView::SetShare(DCT_ID const & poolId, CScript const & provider, uint32_t height) { + WriteBy(PoolShareKey{poolId, provider}, height); + return Res::Ok(); +} + +Res CPoolPairView::DelShare(DCT_ID const & poolId, CScript const & provider) { + EraseBy(PoolShareKey{poolId, provider}); + return Res::Ok(); +} + +boost::optional CPoolPairView::GetShare(DCT_ID const & poolId, CScript const & provider) { + return ReadBy(PoolShareKey{poolId, provider}); +} + +Res CPoolPairView::SetDailyReward(uint32_t height, CAmount reward) { + WriteBy(PoolHeightKey{{}, height}, reward); + return Res::Ok(); +} + +void CPoolPairView::ForEachPoolPair(std::function)> callback, DCT_ID const & start) { ForEach(callback, start); } -void CPoolPairView::ForEachPoolShare(std::function callback, const PoolShareKey &startKey) -{ +void CPoolPairView::ForEachPoolShare(std::function callback, const PoolShareKey &startKey) { ForEach([&callback] (PoolShareKey const & poolShareKey, uint32_t height) { return callback(poolShareKey.poolID, poolShareKey.owner, height); }, startKey); diff --git a/src/masternodes/poolpairs.h b/src/masternodes/poolpairs.h index c98750a7bdb..27e369a6e25 100644 --- a/src/masternodes/poolpairs.h +++ b/src/masternodes/poolpairs.h @@ -88,97 +88,23 @@ class CPoolPair : public CPoolPairMessage static const CAmount MINIMUM_LIQUIDITY = 1000; static const CAmount SLOPE_SWAP_RATE = 1000; static const uint32_t PRECISION = (uint32_t) COIN; // or just PRECISION_BITS for "<<" and ">>" - CPoolPair(CPoolPairMessage const & msg = {}) - : CPoolPairMessage(msg) - , reserveA(0) - , reserveB(0) - , totalLiquidity(0) - , blockCommissionA(0) - , blockCommissionB(0) - , rewardPct(0) - , swapEvent(false) - , creationTx() - , creationHeight(-1) - {} + CPoolPair(CPoolPairMessage const & msg = {}) : CPoolPairMessage(msg) {} virtual ~CPoolPair() = default; - CAmount reserveA, reserveB, totalLiquidity; - CAmount blockCommissionA, blockCommissionB; + CBalances rewards; + CAmount reserveA = 0, reserveB = 0, totalLiquidity = 0; + CAmount blockCommissionA = 0, blockCommissionB = 0; - CAmount rewardPct; // pool yield farming reward %% + CAmount rewardPct = 0; // pool yield farming reward %% bool swapEvent = false; uint256 creationTx; - uint32_t creationHeight; + uint32_t creationHeight = -1; // 'amountA' && 'amountB' should be normalized (correspond) to actual 'tokenA' and 'tokenB' ids in the pair!! // otherwise, 'AddLiquidity' should be () external to 'CPairPool' (i.e. CPoolPairView::AddLiquidity(TAmount a,b etc) with internal lookup of pool by TAmount a,b) - Res AddLiquidity(CAmount amountA, CAmount amountB, std::function onMint, bool slippageProtection = false) { - // instead of assertion due to tests - if (amountA <= 0 || amountB <= 0) { - return Res::Err("amounts should be positive"); - } - - CAmount liquidity{0}; - if (totalLiquidity == 0) { - liquidity = (CAmount) (arith_uint256(amountA) * arith_uint256(amountB)).sqrt().GetLow64(); // sure this is below std::numeric_limits::max() due to sqrt natue - if (liquidity <= MINIMUM_LIQUIDITY) // ensure that it'll be non-zero - return Res::Err("liquidity too low"); - liquidity -= MINIMUM_LIQUIDITY; - // MINIMUM_LIQUIDITY is a hack for non-zero division - totalLiquidity = MINIMUM_LIQUIDITY; - } else { - CAmount liqA = (arith_uint256(amountA) * arith_uint256(totalLiquidity) / reserveA).GetLow64(); - CAmount liqB = (arith_uint256(amountB) * arith_uint256(totalLiquidity) / reserveB).GetLow64(); - liquidity = std::min(liqA, liqB); - - if (liquidity == 0) - return Res::Err("amounts too low, zero liquidity"); - - if(slippageProtection) { - if ((std::max(liqA, liqB) - liquidity) * 100 / liquidity >= 3) { - return Res::Err("Exceeds max ratio slippage protection of 3%%"); - } - } - } - - // increasing totalLiquidity - auto resTotal = SafeAdd(totalLiquidity, liquidity); - if (!resTotal.ok) { - return Res::Err("can't add %d to totalLiquidity: %s", liquidity, resTotal.msg); - } - totalLiquidity = *resTotal.val; - - // increasing reserves - auto resA = SafeAdd(reserveA, amountA); - auto resB = SafeAdd(reserveB, amountB); - if (resA.ok && resB.ok) { - reserveA = *resA.val; - reserveB = *resB.val; - } else { - return Res::Err("overflow when adding to reserves"); - } - - return onMint(liquidity); - } - - Res RemoveLiquidity(CAmount const & liqAmount, std::function onReclaim) { - // instead of assertion due to tests - // IRL it can't be more than "total-1000", and was checked indirectly by balances before. but for tests and incapsulation: - if (liqAmount <= 0 || liqAmount >= totalLiquidity) { - return Res::Err("incorrect liquidity"); - } - - CAmount resAmountA, resAmountB; - resAmountA = (arith_uint256(liqAmount) * arith_uint256(reserveA) / totalLiquidity).GetLow64(); - resAmountB = (arith_uint256(liqAmount) * arith_uint256(reserveB) / totalLiquidity).GetLow64(); - - reserveA -= resAmountA; // safe due to previous math - reserveB -= resAmountB; - totalLiquidity -= liqAmount; - - return onReclaim(resAmountA, resAmountB); - } + Res AddLiquidity(CAmount amountA, CAmount amountB, std::function onMint, bool slippageProtection = false); + Res RemoveLiquidity(CAmount liqAmount, std::function onReclaim); Res Swap(CTokenAmount in, PoolPrice const & maxPrice, std::function onTransfer, int height = INT_MAX); @@ -213,6 +139,7 @@ class CPoolPair : public CPoolPairMessage READWRITE(swapEvent); READWRITE(creationTx); READWRITE(creationHeight); + READWRITE(rewards); if (ser_action.ForRead()) ioProofer(); } @@ -226,12 +153,12 @@ struct PoolShareKey { template inline void SerializationOp(Stream& s, Operation ser_action) { - READWRITE(poolID); + READWRITE(WrapBigEndian(poolID.v)); READWRITE(owner); } }; -struct PoolRewardKey { +struct PoolHeightKey { DCT_ID poolID; uint32_t height; @@ -270,10 +197,7 @@ class CPoolPairView : public virtual CStorageView { public: Res SetPoolPair(const DCT_ID &poolId, uint32_t height, CPoolPair const & pool); - Res UpdatePoolPair(DCT_ID const & poolId, uint32_t height, bool status, CAmount const & commission, CScript const & ownerAddress); - - Res SetPoolCustomReward(const DCT_ID &poolId, uint32_t height, const CBalances &rewards); - boost::optional GetPoolCustomReward(const DCT_ID &poolId); + Res UpdatePoolPair(DCT_ID const & poolId, uint32_t height, bool status, CAmount const & commission, CScript const & ownerAddress, CBalances const & rewards); boost::optional GetPoolPair(const DCT_ID &poolId) const; boost::optional > GetPoolPair(DCT_ID const & tokenA, DCT_ID const & tokenB) const; @@ -281,26 +205,14 @@ class CPoolPairView : public virtual CStorageView void ForEachPoolPair(std::function)> callback, DCT_ID const & start = DCT_ID{0}); void ForEachPoolShare(std::function callback, PoolShareKey const &startKey = {}); - Res SetShare(DCT_ID const & poolId, CScript const & provider, uint32_t height) { - WriteBy(PoolShareKey{poolId, provider}, height); - return Res::Ok(); - } - - Res DelShare(DCT_ID const & poolId, CScript const & provider) { - EraseBy(PoolShareKey{poolId, provider}); - return Res::Ok(); - } + Res SetShare(DCT_ID const & poolId, CScript const & provider, uint32_t height); + Res DelShare(DCT_ID const & poolId, CScript const & provider); - boost::optional GetShare(DCT_ID const & poolId, CScript const & provider) { - return ReadBy(PoolShareKey{poolId, provider}); - } + boost::optional GetShare(DCT_ID const & poolId, CScript const & provider); - void CalculatePoolRewards(DCT_ID const & poolId, CAmount liquidity, uint32_t begin, uint32_t end, std::function onReward); + void CalculatePoolRewards(DCT_ID const & poolId, std::function onLiquidity, uint32_t begin, uint32_t end, std::function onReward); - Res SetDailyReward(uint32_t height, CAmount reward) { - WriteBy(PoolRewardKey{{}, height}, reward); - return Res::Ok(); - } + Res SetDailyReward(uint32_t height, CAmount reward); void UpdatePoolCommissions(std::function onGetBalance, int nHeight = 0) { diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index 95a20c2a7ef..ba19b0e18f2 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -89,17 +89,20 @@ UniValue outputEntryToJSON(COutputEntry const & entry, CBlockIndex const * index } static void onPoolRewards(CCustomCSView & view, CScript const & owner, uint32_t begin, uint32_t end, std::function onReward) { + CCustomCSView mnview(view); view.ForEachPoolPair([&] (DCT_ID const & poolId, CLazySerialize) { auto height = view.GetShare(poolId, owner); if (!height || *height >= end) { return true; // no share or target height is before a pool share' one } + auto onLiquidity = [&]() -> CAmount { + return mnview.GetBalance(owner, poolId).nValue; + }; auto beginHeight = std::max(*height, begin); - view.CalculatePoolRewards(poolId, view.GetBalance(owner, poolId).nValue, beginHeight, end, - [&](CScript const &, uint8_t type, CTokenAmount amount, uint32_t begin, uint32_t end) { - for (auto height = end - 1; height >= begin; height--) { - onReward(height, poolId, type, amount); - } + view.CalculatePoolRewards(poolId, onLiquidity, beginHeight, end, + [&](CScript const &, uint8_t type, CTokenAmount amount, uint32_t height) { + onReward(height, poolId, type, amount); + mnview.AddBalance(owner, amount); // update owner liquidity } ); return true; diff --git a/src/masternodes/rpc_poolpair.cpp b/src/masternodes/rpc_poolpair.cpp index a658dddebe8..217de586644 100644 --- a/src/masternodes/rpc_poolpair.cpp +++ b/src/masternodes/rpc_poolpair.cpp @@ -34,9 +34,9 @@ UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token poolObj.pushKV("rewardPct", ValueFromAmount(pool.rewardPct)); - auto rewards = pcustomcsview->GetPoolCustomReward(id); - if (rewards && !rewards->balances.empty()) { - for (auto it = rewards->balances.cbegin(), next_it = it; it != rewards->balances.cend(); it = next_it) { + auto rewards = pool.rewards; + if (!rewards.balances.empty()) { + for (auto it = rewards.balances.cbegin(), next_it = it; it != rewards.balances.cend(); it = next_it) { ++next_it; // Get token balance @@ -44,14 +44,14 @@ UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token // Make there's enough to pay reward otherwise remove it if (balance < it->second) { - rewards->balances.erase(it); + rewards.balances.erase(it); } } - if (!rewards->balances.empty()) { + if (!rewards.balances.empty()) { UniValue rewardArr(UniValue::VARR); - for (const auto& reward : rewards->balances) { + for (const auto& reward : rewards.balances) { rewardArr.push_back(CTokenAmount{reward.first, reward.second}.ToString()); } diff --git a/src/test/liquidity_tests.cpp b/src/test/liquidity_tests.cpp index b30a646eb61..d49c1299748 100644 --- a/src/test/liquidity_tests.cpp +++ b/src/test/liquidity_tests.cpp @@ -388,7 +388,6 @@ BOOST_AUTO_TEST_CASE(owner_rewards) pool.swapEvent = true; pool.ownerAddress = shareAddress[0]; mnview.SetPoolPair(idPool, 1, pool); - mnview.SetPoolCustomReward(idPool, 1, {TAmounts{{idPool, COIN}}}); return true; }); @@ -409,25 +408,24 @@ BOOST_AUTO_TEST_CASE(owner_rewards) }; mnview.ForEachPoolPair([&] (DCT_ID const & idPool, CPoolPair pool) { - auto liquidity = mnview.GetBalance(shareAddress[0], idPool).nValue; - mnview.CalculatePoolRewards(idPool, liquidity, 1, 10, - [&](CScript const &, uint8_t type, CTokenAmount amount, uint32_t begin, uint32_t end) { + auto onLiquidity = [&]() -> CAmount { + return mnview.GetBalance(shareAddress[0], idPool).nValue; + }; + mnview.CalculatePoolRewards(idPool, onLiquidity, 1, 10, + [&](CScript const &, uint8_t type, CTokenAmount amount, uint32_t height) { switch(type) { case int(RewardType::Rewards): - BOOST_CHECK_EQUAL(begin, 3); - BOOST_CHECK_EQUAL(end, 10); - BOOST_CHECK_EQUAL(amount.nValue, oldRewardCalculation(liquidity, pool)); + BOOST_CHECK_EQUAL(amount.nValue, oldRewardCalculation(onLiquidity(), pool)); break; case int(RewardType::Commission): - BOOST_CHECK_EQUAL(begin, 1); - BOOST_CHECK_EQUAL(end, 2); if (amount.nTokenId == pool.idTokenA) { - BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(liquidity, pool).first); + BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(onLiquidity(), pool).first); } else { - BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(liquidity, pool).second); + BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(onLiquidity(), pool).second); } break; } + mnview.AddBalance(shareAddress[0], amount); } ); return true; @@ -435,8 +433,6 @@ BOOST_AUTO_TEST_CASE(owner_rewards) // new calculation const_cast(Params().GetConsensus().BayfrontGardensHeight) = 6; - // custom rewards - const_cast(Params().GetConsensus().ClarkeQuayHeight) = 7; auto newRewardCalculation = [](CAmount liquidity, const CPoolPair& pool) -> CAmount { return COIN / 2880 * pool.rewardPct / COIN * liquidity / pool.totalLiquidity; @@ -451,43 +447,41 @@ BOOST_AUTO_TEST_CASE(owner_rewards) mnview.ForEachPoolPair([&] (DCT_ID const & idPool, CPoolPair pool) { pool.swapEvent = true; pool.ownerAddress = shareAddress[1]; + pool.rewards = CBalances{TAmounts{{idPool, COIN}}}; mnview.SetPoolPair(idPool, 8, pool); return false; }); mnview.ForEachPoolPair([&] (DCT_ID const & idPool, CPoolPair pool) { - auto liquidity = mnview.GetBalance(shareAddress[0], idPool).nValue; - mnview.CalculatePoolRewards(idPool, liquidity, 1, 10, - [&](CScript const & from, uint8_t type, CTokenAmount amount, uint32_t begin, uint32_t end) { - if (begin >= Params().GetConsensus().BayfrontGardensHeight) { + auto onLiquidity = [&] () -> CAmount { + return mnview.GetBalance(shareAddress[1], idPool).nValue; + }; + mnview.CalculatePoolRewards(idPool, onLiquidity, 1, 10, + [&](CScript const & from, uint8_t type, CTokenAmount amount, uint32_t height) { + if (height >= Params().GetConsensus().BayfrontGardensHeight) { if (!from.empty()) { - if (begin >= 8) { - BOOST_CHECK(from == shareAddress[1]); - } else { - BOOST_CHECK(from == shareAddress[0]); - } - auto rewards = mnview.GetPoolCustomReward(idPool); - BOOST_REQUIRE(rewards); - for (const auto& reward : rewards->balances) { - auto providerReward = static_cast((arith_uint256(reward.second) * arith_uint256(liquidity) / arith_uint256(pool.totalLiquidity)).GetLow64()); + BOOST_CHECK(from == shareAddress[1]); + for (const auto& reward : pool.rewards.balances) { + auto providerReward = static_cast((arith_uint256(reward.second) * arith_uint256(onLiquidity()) / arith_uint256(pool.totalLiquidity)).GetLow64()); BOOST_CHECK_EQUAL(amount.nValue, providerReward); } } else if (type == int(RewardType::Rewards)) { - BOOST_CHECK_EQUAL(amount.nValue, newRewardCalculation(liquidity, pool)); + BOOST_CHECK_EQUAL(amount.nValue, newRewardCalculation(onLiquidity(), pool)); } else if (amount.nTokenId == pool.idTokenA) { - BOOST_CHECK_EQUAL(amount.nValue, newCommissionCalculation(liquidity, pool).first); + BOOST_CHECK_EQUAL(amount.nValue, newCommissionCalculation(onLiquidity(), pool).first); } else { - BOOST_CHECK_EQUAL(amount.nValue, newCommissionCalculation(liquidity, pool).second); + BOOST_CHECK_EQUAL(amount.nValue, newCommissionCalculation(onLiquidity(), pool).second); } } else { if (type == int(RewardType::Rewards)) { - BOOST_CHECK_EQUAL(amount.nValue, oldRewardCalculation(liquidity, pool)); + BOOST_CHECK_EQUAL(amount.nValue, oldRewardCalculation(onLiquidity(), pool)); } else if (amount.nTokenId == pool.idTokenA) { - BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(liquidity, pool).first); + BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(onLiquidity(), pool).first); } else { - BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(liquidity, pool).second); + BOOST_CHECK_EQUAL(amount.nValue, oldCommissionCalculation(onLiquidity(), pool).second); } } + mnview.AddBalance(shareAddress[1], amount); } ); return false; diff --git a/src/validation.cpp b/src/validation.cpp index 97fb5cae407..b5b76a1992a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2359,7 +2359,8 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl CCustomCSView cache(mnview); // hardfork commissions update - cache.UpdatePoolCommissions([&cache](CScript const & owner, DCT_ID tokenID) { + cache.UpdatePoolCommissions([&](CScript const & owner, DCT_ID tokenID) { + cache.CalculateOwnerRewards(owner, pindex->nHeight); return cache.GetBalance(owner, tokenID); }, pindex->nHeight); // Remove `Finalized` and/or `LPS` flags _possibly_set_ by bytecoded (cheated) txs before bayfront fork