From 5370c8c69b0794960b661b0b8085258d0ce7250c Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Mon, 15 Jul 2024 19:29:13 +0100 Subject: [PATCH] Static pool reward (#2924) * Initial work on new pool reward logic * Fix failing test * Add loan reward to new owner reward calculations * Add remaining rewards * Test multiple liquidity in pools * Pass 0 for DFI. Avoid shadowing. --- src/amount.h | 1 + src/dfi/loan.h | 2 - src/dfi/masternodes.cpp | 34 +- src/dfi/poolpairs.cpp | 260 +++++-- src/dfi/poolpairs.h | 62 ++ .../functional/feature_static_pool_rewards.py | 708 ++++++++++++++++++ test/functional/test_runner.py | 1 + 7 files changed, 1006 insertions(+), 62 deletions(-) create mode 100755 test/functional/feature_static_pool_rewards.py diff --git a/src/amount.h b/src/amount.h index 640050726c3..d33c02ec692 100644 --- a/src/amount.h +++ b/src/amount.h @@ -79,6 +79,7 @@ static constexpr CAmount COIN = 100000000; static constexpr CAmount CENT = 1000000; static constexpr int64_t WEI_IN_GWEI = 1000000000; static constexpr int64_t CAMOUNT_TO_GWEI = 10; +static constexpr CAmount HIGH_PRECISION_SCALER = COIN * COIN; // 1,0000,0000,0000,0000 //Converts the given value to decimal format string with COIN precision. inline std::string GetDecimalString(CAmount nValue) diff --git a/src/dfi/loan.h b/src/dfi/loan.h index 1191068db61..47e60369a32 100644 --- a/src/dfi/loan.h +++ b/src/dfi/loan.h @@ -328,8 +328,6 @@ inline auto InterestAddition = [](const CInterestAmount &a, const CInterestAmoun return interest; }; -static const CAmount HIGH_PRECISION_SCALER = COIN * COIN; // 1,0000,0000,0000,0000 - CAmount TotalInterest(const CInterestRateV3 &rate, const uint32_t height); CInterestAmount TotalInterestCalculation(const CInterestRateV3 &rate, const uint32_t height); CAmount CeilInterest(const base_uint<128> &value, uint32_t height); diff --git a/src/dfi/masternodes.cpp b/src/dfi/masternodes.cpp index 4c3478da987..ef40993b3f0 100644 --- a/src/dfi/masternodes.cpp +++ b/src/dfi/masternodes.cpp @@ -971,17 +971,29 @@ bool CCustomCSView::CalculateOwnerRewards(const CScript &owner, uint32_t targetH 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, onLiquidity, beginHeight, targetHeight, [&](RewardType, CTokenAmount amount, uint32_t height) { - auto res = AddBalance(owner, amount); - if (!res) { - LogPrintf("Pool rewards: can't update balance of %s: %s, height %ld\n", - owner.GetHex(), - res.msg, - targetHeight); - } - }); + const auto beginHeight = std::max(*height, balanceHeight); + auto onReward = [&](RewardType, const CTokenAmount &amount, const uint32_t height) { + if (auto res = AddBalance(owner, amount); !res) { + LogPrintf( + "Pool rewards: can't update balance of %s: %s, height %ld\n", owner.GetHex(), res.msg, height); + } + }; + + if (beginHeight < Params().GetConsensus().DF24Height) { + // Calculate just up to the fork height + const auto targetNewHeight = targetHeight >= Params().GetConsensus().DF24Height + ? Params().GetConsensus().DF24Height - 1 + : targetHeight; + CalculatePoolRewards(poolId, onLiquidity, beginHeight, targetNewHeight, onReward); + } + + if (targetHeight >= Params().GetConsensus().DF24Height) { + // Calculate from the fork height + const auto beginNewHeight = + beginHeight < Params().GetConsensus().DF24Height ? Params().GetConsensus().DF24Height : beginHeight; + CalculateStaticPoolRewards(onLiquidity, onReward, poolId.v, beginNewHeight, targetHeight); + } + return true; }); diff --git a/src/dfi/poolpairs.cpp b/src/dfi/poolpairs.cpp index b4d09a55ad4..6a54140aef2 100644 --- a/src/dfi/poolpairs.cpp +++ b/src/dfi/poolpairs.cpp @@ -187,8 +187,8 @@ std::optional CPoolPairView::GetPoolPair(const DCT_ID &poolId) const return pool; } -std::optional > CPoolPairView::GetPoolPair(const DCT_ID &tokenA, - const DCT_ID &tokenB) const { +std::optional> CPoolPairView::GetPoolPair(const DCT_ID &tokenA, + const DCT_ID &tokenB) const { DCT_ID poolId; ByPairKey key{tokenA, tokenB}; if (ReadBy(key, poolId)) { @@ -241,6 +241,50 @@ auto InitPoolVars(CPoolPairView &view, PoolHeightKey poolKey, uint32_t end) { return std::make_tuple(std::move(value), std::move(it), height); } +static auto GetRewardPerShares(const CPoolPairView &view, const TotalRewardPerShareKey &key) { + return std::make_tuple(view.GetTotalRewardPerShare(key), + view.GetTotalLoanRewardPerShare(key), + view.GetTotalCommissionPerShare(key), + view.GetTotalCustomRewardPerShare(key)); +} + +void CPoolPairView::CalculateStaticPoolRewards(std::function onLiquidity, + std::function onReward, + const uint32_t poolID, + const uint32_t beginHeight, + const uint32_t endHeight) { + if (beginHeight >= endHeight) { + return; + } + + // Get start and end reward per share + TotalRewardPerShareKey key{beginHeight, poolID}; + auto [startCoinbase, startLoan, startCommission, startCustom] = GetRewardPerShares(*this, key); + key.height = endHeight - 1; + auto [endCoinbase, endLoan, endCommission, endCustom] = GetRewardPerShares(*this, key); + + // Get owner's liquidity + const auto liquidity = onLiquidity(); + + auto calcReward = [&](RewardType type, const arith_uint256 &start, const arith_uint256 &end, const uint32_t id) { + if (const auto rewardPerShare = end - start; rewardPerShare > 0) { + // Calculate reward + const auto reward = (liquidity * rewardPerShare / HIGH_PRECISION_SCALER).GetLow64(); + // Pay reward to the owner + onReward(type, {DCT_ID{id}, static_cast(reward)}, endHeight); + } + }; + + calcReward(RewardType::Coinbase, startCoinbase, endCoinbase, 0); + calcReward(RewardType::LoanTokenDEXReward, startLoan, endLoan, 0); + calcReward(RewardType::Commission, startCommission.commissionA, endCommission.commissionA, endCommission.tokenA); + calcReward(RewardType::Commission, startCommission.commissionB, endCommission.commissionB, endCommission.tokenB); + + for (const auto &[id, end] : endCustom) { + calcReward(RewardType::Pool, startCustom[id], end, id); + } +} + void CPoolPairView::CalculatePoolRewards(DCT_ID const &poolId, std::function onLiquidity, uint32_t begin, @@ -317,14 +361,22 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const &poolId, } // commissions if (poolSwapHeight == height && poolSwap.swapEvent) { - CAmount feeA, feeB; + CAmount feeA{}, feeB{}; if (height < newCalcHeight) { uint32_t liqWeight = liquidity * PRECISION / totalLiquidity; - feeA = poolSwap.blockCommissionA * liqWeight / PRECISION; - feeB = poolSwap.blockCommissionB * liqWeight / PRECISION; + if (poolSwap.blockCommissionA) { + feeA = poolSwap.blockCommissionA * liqWeight / PRECISION; + } + if (poolSwap.blockCommissionB) { + feeB = poolSwap.blockCommissionB * liqWeight / PRECISION; + } } else { - feeA = liquidityReward(poolSwap.blockCommissionA, liquidity, totalLiquidity); - feeB = liquidityReward(poolSwap.blockCommissionB, liquidity, totalLiquidity); + if (poolSwap.blockCommissionA) { + feeA = liquidityReward(poolSwap.blockCommissionA, liquidity, totalLiquidity); + } + if (poolSwap.blockCommissionB) { + feeB = liquidityReward(poolSwap.blockCommissionB, liquidity, totalLiquidity); + } } if (feeA) { onReward(RewardType::Commission, {tokenIds->idTokenA, feeA}, height); @@ -335,9 +387,9 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const &poolId, } // custom rewards if (height >= startCustomRewards) { - for (const auto &reward : customRewards.balances) { - if (auto providerReward = liquidityReward(reward.second, liquidity, totalLiquidity)) { - onReward(RewardType::Pool, {reward.first, providerReward}, height); + for (const auto &[id, poolCustomReward] : customRewards.balances) { + if (auto providerReward = liquidityReward(poolCustomReward, liquidity, totalLiquidity)) { + onReward(RewardType::Pool, {id, providerReward}, height); } } } @@ -529,53 +581,54 @@ std::pair CPoolPairView::UpdatePoolRewards( std::function onGetBalance, std::function onTransfer, int nHeight) { - bool newRewardCalc = nHeight >= Params().GetConsensus().DF4BayfrontGardensHeight; - bool newRewardLogic = nHeight >= Params().GetConsensus().DF8EunosHeight; - bool newCustomRewards = nHeight >= Params().GetConsensus().DF5ClarkeQuayHeight; + const bool newRewardCalc = nHeight >= Params().GetConsensus().DF4BayfrontGardensHeight; + const bool newRewardLogic = nHeight >= Params().GetConsensus().DF8EunosHeight; + const bool newCustomRewards = nHeight >= Params().GetConsensus().DF5ClarkeQuayHeight; + const bool newRewardCalculations = nHeight >= Params().GetConsensus().DF24Height; - constexpr const uint32_t PRECISION = 10000; // (== 100%) just searching the way to avoid arith256 inflating - CAmount totalDistributed = 0; - CAmount totalLoanDistributed = 0; + CAmount totalDistributed{}; + CAmount totalLoanDistributed{}; ForEachPoolId([&](DCT_ID const &poolId) { - CAmount distributedFeeA = 0; - CAmount distributedFeeB = 0; - std::optional ownerAddress; + CAmount distributedFeeA{}; + CAmount distributedFeeB{}; + CBalances poolCustomRewards; + CScript ownerAddress; + std::optional pool; PoolHeightKey poolKey = {poolId, uint32_t(nHeight)}; - CBalances rewards; if (newCustomRewards) { - if (auto pool = ReadBy(poolId)) { - rewards = std::move(pool->rewards); - ownerAddress = std::move(pool->ownerAddress); - } - - for (auto it = rewards.balances.begin(), next_it = it; it != rewards.balances.end(); it = next_it) { - ++next_it; + pool = ReadBy(poolId); + assert(pool); + poolCustomRewards = std::move(pool->rewards); + ownerAddress = std::move(pool->ownerAddress); + for (auto it = poolCustomRewards.balances.begin(); it != poolCustomRewards.balances.end();) { // Get token balance - const auto balance = onGetBalance(*ownerAddress, it->first).nValue; + const auto balance = onGetBalance(ownerAddress, it->first).nValue; // Make there's enough to pay reward otherwise remove it if (balance < it->second) { - rewards.balances.erase(it); + it = poolCustomRewards.balances.erase(it); + } else { + ++it; } } - if (rewards != ReadValueAt(this, poolKey)) { - WriteBy(poolKey, rewards); + if (poolCustomRewards != ReadValueAt(this, poolKey)) { + WriteBy(poolKey, poolCustomRewards); } } - auto totalLiquidity = ReadValueAt(this, poolKey); + const auto totalLiquidity = ReadValueAt(this, poolKey); if (!totalLiquidity) { return true; } auto swapValue = ReadBy(poolKey); const auto swapEvent = swapValue && swapValue->swapEvent; - auto poolReward = ReadValueAt(this, poolKey); + const auto poolReward = ReadValueAt(this, poolKey); if (newRewardLogic) { if (swapEvent) { @@ -585,22 +638,73 @@ std::pair CPoolPairView::UpdatePoolRewards( } // Get LP loan rewards - auto poolLoanReward = ReadValueAt(this, poolKey); + const auto poolLoanReward = ReadValueAt(this, poolKey); // increase by pool block reward totalDistributed += poolReward; totalLoanDistributed += poolLoanReward; - for (const auto &reward : rewards.balances) { + for (const auto &[id, poolCustomReward] : poolCustomRewards.balances) { // subtract pool's owner account by custom block reward - onTransfer(*ownerAddress, {}, {reward.first, reward.second}); + onTransfer(ownerAddress, {}, {id, poolCustomReward}); + } + + if (newRewardCalculations) { + auto calculateReward = [&](const CAmount reward) { + return (arith_uint256(reward) * HIGH_PRECISION_SCALER) / arith_uint256(totalLiquidity); + }; + + // Calculate the reward for each LP + const auto sharePerLP = calculateReward(poolReward); + const auto sharePerLoanLP = calculateReward(poolLoanReward); + + // Get total from last block + TotalRewardPerShareKey key{static_cast(nHeight - 1), poolId.v}; + auto [totalCoinbase, totalLoan, totalCommission, totalCustom] = GetRewardPerShares(*this, key); + + // Add the reward to the total + totalCoinbase += sharePerLP; + totalLoan += sharePerLoanLP; + + if (swapEvent) { + // Calculate commission per LP + arith_uint256 commissionA{}, commissionB{}; + if (distributedFeeA) { + commissionA = calculateReward(distributedFeeA); + } + if (distributedFeeB) { + commissionB = calculateReward(distributedFeeB); + } + totalCommission.tokenA = pool->idTokenA.v; + totalCommission.tokenB = pool->idTokenB.v; + totalCommission.commissionA += commissionA; + totalCommission.commissionB += commissionB; + } + + // Calculate custom rewards + for (const auto &[id, poolCustomReward] : poolCustomRewards.balances) { + // Calculate the reward for each custom LP + const auto sharePerCustomLP = + (arith_uint256(poolCustomReward) * HIGH_PRECISION_SCALER) / arith_uint256(totalLiquidity); + // Add the reward to the total + totalCustom[id.v] += sharePerCustomLP; + } + + // Store new total at current height + key.height = nHeight; + SetTotalRewardPerShare(key, totalCoinbase); + SetTotalLoanRewardPerShare(key, totalLoan); + SetTotalCustomRewardPerShare(key, totalCustom); + SetTotalCommissionPerShare(key, totalCommission); } } else { - if (!swapEvent && poolReward == 0 && rewards.balances.empty()) { + if (!swapEvent && poolReward == 0 && poolCustomRewards.balances.empty()) { return true; // no events, skip to the next pool } + constexpr const uint32_t PRECISION = 10000; // (== 100%) just searching the way to avoid arith256 inflating + ForEachPoolShare( [&](DCT_ID const ¤tId, CScript const &provider, uint32_t) { if (currentId != poolId) { @@ -613,21 +717,33 @@ std::pair CPoolPairView::UpdatePoolRewards( // distribute trading fees if (swapEvent) { - CAmount feeA, feeB; + CAmount feeA{}, feeB{}; if (newRewardCalc) { - feeA = liquidityReward(swapValue->blockCommissionA, liquidity, totalLiquidity); - feeB = liquidityReward(swapValue->blockCommissionB, liquidity, totalLiquidity); + if (swapValue->blockCommissionA) { + feeA = liquidityReward(swapValue->blockCommissionA, liquidity, totalLiquidity); + } + if (swapValue->blockCommissionB) { + feeB = liquidityReward(swapValue->blockCommissionB, liquidity, totalLiquidity); + } } else { - feeA = swapValue->blockCommissionA * liqWeight / PRECISION; - feeB = swapValue->blockCommissionB * liqWeight / PRECISION; + if (swapValue->blockCommissionA) { + feeA = swapValue->blockCommissionA * liqWeight / PRECISION; + } + if (swapValue->blockCommissionB) { + feeB = swapValue->blockCommissionB * liqWeight / PRECISION; + } } auto tokenIds = ReadBy(poolId); assert(tokenIds); - if (onTransfer({}, provider, {tokenIds->idTokenA, feeA})) { - distributedFeeA += feeA; + if (feeA) { + if (onTransfer({}, provider, {tokenIds->idTokenA, feeA})) { + distributedFeeA += feeA; + } } - if (onTransfer({}, provider, {tokenIds->idTokenB, feeB})) { - distributedFeeB += feeB; + if (feeB) { + if (onTransfer({}, provider, {tokenIds->idTokenB, feeB})) { + distributedFeeB += feeB; + } } } @@ -644,9 +760,9 @@ std::pair CPoolPairView::UpdatePoolRewards( } } - for (const auto &reward : rewards.balances) { - if (auto providerReward = liquidityReward(reward.second, liquidity, totalLiquidity)) { - onTransfer(*ownerAddress, provider, {reward.first, providerReward}); + for (const auto &[id, poolCustomReward] : poolCustomRewards.balances) { + if (auto providerReward = liquidityReward(poolCustomReward, liquidity, totalLiquidity)) { + onTransfer(ownerAddress, provider, {id, providerReward}); } } @@ -713,6 +829,52 @@ void CPoolPairView::ForEachTokenAverageLiquidity( start); } +bool CPoolPairView::SetTotalRewardPerShare(const TotalRewardPerShareKey &key, const arith_uint256 &totalReward) { + return WriteBy(key, totalReward); +} + +arith_uint256 CPoolPairView::GetTotalRewardPerShare(const TotalRewardPerShareKey &key) const { + if (const auto value = ReadBy(key); value) { + return *value; + } + return {}; +} + +bool CPoolPairView::SetTotalLoanRewardPerShare(const TotalRewardPerShareKey &key, const arith_uint256 &totalReward) { + return WriteBy(key, totalReward); +} + +arith_uint256 CPoolPairView::GetTotalLoanRewardPerShare(const TotalRewardPerShareKey &key) const { + if (const auto value = ReadBy(key); value) { + return *value; + } + return {}; +} + +bool CPoolPairView::SetTotalCustomRewardPerShare(const TotalRewardPerShareKey &key, + const std::map &customRewards) { + return WriteBy(key, customRewards); +} + +std::map CPoolPairView::GetTotalCustomRewardPerShare(const TotalRewardPerShareKey &key) const { + if (const auto value = ReadBy>(key); value) { + return *value; + } + return {}; +} + +bool CPoolPairView::SetTotalCommissionPerShare(const TotalRewardPerShareKey &key, + const TotalCommissionPerShareValue &totalCommission) { + return WriteBy(key, totalCommission); +} + +TotalCommissionPerShareValue CPoolPairView::GetTotalCommissionPerShare(const TotalRewardPerShareKey &key) const { + if (const auto value = ReadBy(key); value) { + return *value; + } + return {}; +} + Res CPoolPairView::DelShare(DCT_ID const &poolId, const CScript &provider) { EraseBy(PoolShareKey{poolId, provider}); return Res::Ok(); diff --git a/src/dfi/poolpairs.h b/src/dfi/poolpairs.h index 6ecf349f45b..3c1ceff188c 100644 --- a/src/dfi/poolpairs.h +++ b/src/dfi/poolpairs.h @@ -225,6 +225,36 @@ struct PoolShareKey { } }; +struct TotalRewardPerShareKey { + uint32_t height; + uint32_t poolID; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream &s, Operation ser_action) { + READWRITE(WrapBigEndian(height)); + READWRITE(WrapBigEndian(poolID)); + } +}; + +struct TotalCommissionPerShareValue { + uint32_t tokenA; + uint32_t tokenB; + arith_uint256 commissionA; + arith_uint256 commissionB; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream &s, Operation ser_action) { + READWRITE(tokenA); + READWRITE(tokenB); + READWRITE(commissionA); + READWRITE(commissionB); + } +}; + struct LoanTokenAverageLiquidityKey { uint32_t sourceID; uint32_t destID; @@ -320,6 +350,12 @@ class CPoolPairView : public virtual CStorageView { uint32_t end, std::function onReward); + void CalculateStaticPoolRewards(std::function onLiquidity, + std::function onReward, + const uint32_t poolID, + const uint32_t beginHeight, + const uint32_t endHeight); + Res SetLoanDailyReward(const uint32_t height, const CAmount reward); Res SetDailyReward(uint32_t height, CAmount reward); Res SetRewardPct(DCT_ID const &poolId, uint32_t height, CAmount rewardPct); @@ -349,6 +385,17 @@ class CPoolPairView : public virtual CStorageView { std::function callback, const LoanTokenAverageLiquidityKey start = LoanTokenAverageLiquidityKey{}); + bool SetTotalRewardPerShare(const TotalRewardPerShareKey &key, const arith_uint256 &totalReward); + arith_uint256 GetTotalRewardPerShare(const TotalRewardPerShareKey &totalReward) const; + bool SetTotalLoanRewardPerShare(const TotalRewardPerShareKey &key, const arith_uint256 &totalReward); + arith_uint256 GetTotalLoanRewardPerShare(const TotalRewardPerShareKey &totalReward) const; + bool SetTotalCustomRewardPerShare(const TotalRewardPerShareKey &key, + const std::map &customRewards); + std::map GetTotalCustomRewardPerShare(const TotalRewardPerShareKey &key) const; + bool SetTotalCommissionPerShare(const TotalRewardPerShareKey &key, + const TotalCommissionPerShareValue &totalCommission); + TotalCommissionPerShareValue GetTotalCommissionPerShare(const TotalRewardPerShareKey &key) const; + // tags struct ByID { static constexpr uint8_t prefix() { return 'i'; } @@ -401,6 +448,21 @@ class CPoolPairView : public virtual CStorageView { struct ByLoanTokenLiquidityAverage { static constexpr uint8_t prefix() { return '+'; } }; + struct ByTotalRewardPerShare { + static constexpr uint8_t prefix() { return '-'; } + }; + + struct ByTotalLoanRewardPerShare { + static constexpr uint8_t prefix() { return '='; } + }; + + struct ByTotalCustomRewardPerShare { + static constexpr uint8_t prefix() { return '_'; } + }; + + struct ByTotalCommissionPerShare { + static constexpr uint8_t prefix() { return '/'; } + }; }; struct CLiquidityMessage { diff --git a/test/functional/feature_static_pool_rewards.py b/test/functional/feature_static_pool_rewards.py new file mode 100755 index 00000000000..c6a64003cb7 --- /dev/null +++ b/test/functional/feature_static_pool_rewards.py @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test static pool rewards""" + +from test_framework.test_framework import DefiTestFramework +from test_framework.util import assert_equal + +from decimal import Decimal +import time + + +class TokenFractionalSplitTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.df24height = 250 + self.extra_args = [ + [ + "-jellyfish_regtest=1", + "-subsidytest=1", + "-txnotokens=0", + "-amkheight=1", + "-bayfrontheight=1", + "-bayfrontgardensheight=1", + "-clarkequayheight=1", + "-eunosheight=1", + "-fortcanningheight=1", + "-fortcanningmuseumheight=1", + "-fortcanninghillheight=1", + "-fortcanningroadheight=1", + "-fortcanningcrunchheight=1", + "-fortcanningspringheight=1", + "-fortcanninggreatworldheight=1", + "-grandcentralheight=1", + "-grandcentralepilogueheight=1", + "-metachainheight=105", + "-df23height=110", + f"-df24height={self.df24height}", + ] + ] + + def run_test(self): + + # Setup test + self.setup_tests() + + # Test new pool reward with single share + self.static_reward_single() + + # Test new pool reward with multiple share + self.static_reward_multiple() + + # Test loan token reward + self.static_loan_single() + + # Test loan token reward + self.static_loan_multiple() + + # Test custom token reward + self.static_custom_single() + + # Test custom token reward + self.static_custom_multiple() + + # Test commission + self.static_commission_single() + + # Test commission + self.static_commission_multiple() + + # Test reward when LP_SPLITS zeroed + self.lp_split_zero_reward() + + # Add liquidity before LP_SPLITS + self.liquidity_before_setting_splits() + + # Add liquidity after LP_SPLITS + self.liquidity_after_setting_splits() + + def setup_tests(self): + + # Set up test tokens + self.setup_test_tokens() + + # Set up pool pair + self.setup_poolpair() + + # Save start block + self.start_block = self.nodes[0].getblockcount() + + # Create variable for pre-fork coinbase reward + self.pre_fork_reward = None + + def setup_poolpair(self): + + # Fund address for pool + self.nodes[0].utxostoaccount({self.owner_address: f"100000@{self.symbolDFI}"}) + self.nodes[0].minttokens([f"100000@{self.idBTC}"]) + self.nodes[0].minttokens([f"100000@{self.idLTC}"]) + self.nodes[0].minttokens([f"100000@{self.idTSLA}"]) + self.nodes[0].minttokens([f"100000@{self.idETH}"]) + self.nodes[0].generate(1) + + # Create pool symbol + btc_pool_symbol = self.symbolBTC + "-" + self.symbolDFI + ltc_pool_symbol = self.symbolLTC + "-" + self.symbolDFI + tsla_pool_symbol = self.symbolTSLA + "-" + self.symbolDFI + + # Create pool pairs + self.nodes[0].createpoolpair( + { + "tokenA": self.symbolBTC, + "tokenB": self.symbolDFI, + "status": True, + "ownerAddress": self.owner_address, + "commission": 0, + "symbol": btc_pool_symbol, + } + ) + + self.nodes[0].createpoolpair( + { + "tokenA": self.symbolLTC, + "tokenB": self.symbolDFI, + "status": True, + "ownerAddress": self.owner_address, + "commission": 0.01, + "symbol": ltc_pool_symbol, + } + ) + + self.nodes[0].createpoolpair( + { + "tokenA": self.symbolTSLA, + "tokenB": self.symbolDFI, + "status": True, + "ownerAddress": self.owner_address, + "commission": 0, + "symbol": self.symbolTSLA + "-" + self.symbolDFI, + } + ) + self.nodes[0].generate(1) + + # Get pool pair IDs + self.btc_pool_id = list(self.nodes[0].getpoolpair(btc_pool_symbol).keys())[0] + self.ltc_pool_id = list(self.nodes[0].getpoolpair(ltc_pool_symbol).keys())[0] + tsla_pool_id = list(self.nodes[0].getpoolpair(tsla_pool_symbol).keys())[0] + + # Set pool pair splits + self.nodes[0].setgov({"LP_SPLITS": {self.btc_pool_id: 1}}) + self.nodes[0].setgov({"LP_LOAN_TOKEN_SPLITS": {tsla_pool_id: 1}}) + self.nodes[0].generate(1) + + def setup_test_tokens(self): + self.nodes[0].generate(110) + + # Symbols + self.symbolBTC = "BTC" + self.symbolLTC = "LTC" + self.symbolDFI = "DFI" + self.symbolTSLA = "TSLA" + self.symbolETH = "ETH" + + # Store addresses + self.owner_address = self.nodes[0].get_genesis_keys().ownerAuthAddress + self.address = self.nodes[0].getnewaddress("", "legacy") + + # Create tokens + self.nodes[0].createtoken( + { + "symbol": self.symbolBTC, + "name": self.symbolBTC, + "isDAT": True, + "collateralAddress": self.owner_address, + } + ) + + self.nodes[0].createtoken( + { + "symbol": self.symbolLTC, + "name": self.symbolLTC, + "isDAT": True, + "collateralAddress": self.owner_address, + } + ) + + self.nodes[0].createtoken( + { + "symbol": self.symbolETH, + "name": self.symbolETH, + "isDAT": True, + "collateralAddress": self.owner_address, + } + ) + self.nodes[0].generate(1) + + # Price feeds + price_feed = [ + {"currency": "USD", "token": self.symbolTSLA}, + {"currency": "USD", "token": self.symbolDFI}, + ] + + # Appoint oracle + oracle_address = self.nodes[0].getnewaddress("", "legacy") + oracle = self.nodes[0].appointoracle(oracle_address, price_feed, 10) + self.nodes[0].generate(1) + + # Set Oracle prices + oracle_prices = [ + {"currency": "USD", "tokenAmount": f"1@{self.symbolTSLA}"}, + {"currency": "USD", "tokenAmount": f"1@{self.symbolDFI}"}, + ] + self.nodes[0].setoracledata(oracle, int(time.time()), oracle_prices) + self.nodes[0].generate(11) + + # Create loan token + self.nodes[0].setloantoken( + { + "symbol": self.symbolTSLA, + "name": self.symbolTSLA, + "fixedIntervalPriceId": f"{self.symbolTSLA}/USD", + "mintable": True, + "interest": 0, + } + ) + self.nodes[0].generate(1) + + # Store token IDs + self.idBTC = list(self.nodes[0].gettoken(self.symbolBTC).keys())[0] + self.idLTC = list(self.nodes[0].gettoken(self.symbolLTC).keys())[0] + self.idTSLA = list(self.nodes[0].gettoken(self.symbolTSLA).keys())[0] + self.idETH = list(self.nodes[0].gettoken(self.symbolETH).keys())[0] + + def fund_pool_multiple(self, token_a, token_b): + + # Fund pool with multiple addresses + liquidity_addresses = [] + for i in range(100): + address = self.nodes[0].getnewaddress() + amount = 10 + (i * 10) + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"{amount}@{token_a}", f"{amount}@{token_b}"]}, + address, + ) + self.nodes[0].generate(1) + liquidity_addresses.append(address) + + return liquidity_addresses + + def test_single_liquidity( + self, token_a, token_b, balance_index=0, custom_token=None + ): + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{token_a}", f"1000@{token_b}"]}, + self.address, + ) + self.nodes[0].generate(1) + + # Add custom reward + if custom_token: + self.nodes[0].updatepoolpair( + {"pool": self.btc_pool_id, "customRewards": [f"1@{custom_token}"]} + ) + self.nodes[0].generate(1) + + # Calculate pre-fork reward + pre_fork_reward = Decimal( + self.nodes[0].getaccount(self.address)[balance_index].split("@")[0] + ) + + # Save first pre-fork reward + if self.pre_fork_reward is None: + self.pre_fork_reward = pre_fork_reward + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Get initial balance + start_balance = Decimal( + self.nodes[0].getaccount(self.address)[balance_index].split("@")[0] + ) + self.nodes[0].generate(1) + + # Get balance after a block + end_balance = Decimal( + self.nodes[0].getaccount(self.address)[balance_index].split("@")[0] + ) + + # Calculate post-fork reward + post_fork_reward = end_balance - start_balance + + # Check rewards are the same + assert_equal(pre_fork_reward, post_fork_reward) + + def test_multiple_liquidity( + self, token_a, token_b, balance_index=0, custom_token=None + ): + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool with multiple addresses + liquidity_addresses = self.fund_pool_multiple(token_a, token_b) + + # Add custom reward + if custom_token: + self.nodes[0].updatepoolpair( + {"pool": self.btc_pool_id, "customRewards": [f"1@{custom_token}"]} + ) + self.nodes[0].generate(1) + + # Get initial balances + intitial_balances = [] + for address in liquidity_addresses: + intitial_balances.append( + Decimal(self.nodes[0].getaccount(address)[balance_index].split("@")[0]) + ) + self.nodes[0].generate(1) + + # Get balances after a block + end_balances = [] + for address in liquidity_addresses: + end_balances.append( + Decimal(self.nodes[0].getaccount(address)[balance_index].split("@")[0]) + ) + + # Calculate pre-fork rewards + pre_fork_rewards = [ + end - start for start, end in zip(intitial_balances, end_balances) + ] + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Get initial balances + intitial_balances = [] + for address in liquidity_addresses: + intitial_balances.append( + Decimal(self.nodes[0].getaccount(address)[balance_index].split("@")[0]) + ) + self.nodes[0].generate(1) + + # Get balances after a block + end_balances = [] + for address in liquidity_addresses: + end_balances.append( + Decimal(self.nodes[0].getaccount(address)[balance_index].split("@")[0]) + ) + + # Calculate post-fork reward + post_fork_rewards = [ + end - start for start, end in zip(intitial_balances, end_balances) + ] + + # Check rewards are the same + assert_equal(pre_fork_rewards, post_fork_rewards) + + def static_reward_single(self): + + # Test single coinbase reward + self.test_single_liquidity(self.symbolBTC, self.symbolDFI) + + def static_reward_multiple(self): + + # Test multiple coinbase reward + self.test_multiple_liquidity(self.symbolTSLA, self.symbolDFI) + + def static_loan_single(self): + + # Test single loan reward + self.test_single_liquidity(self.symbolTSLA, self.symbolDFI) + + def static_loan_multiple(self): + + # Test multiple loan reward + self.test_multiple_liquidity(self.symbolBTC, self.symbolDFI) + + def static_custom_single(self): + + # Test single custom reward + self.test_single_liquidity(self.symbolBTC, self.symbolDFI, 1, self.symbolETH) + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{self.symbolBTC}", f"1000@{self.symbolDFI}"]}, + self.address, + ) + self.nodes[0].generate(1) + + # Add custom reward + self.nodes[0].updatepoolpair( + {"pool": self.btc_pool_id, "customRewards": [f"1@{self.symbolETH}"]} + ) + self.nodes[0].generate(1) + + # Calculate pre-fork reward + pre_fork_reward = Decimal( + self.nodes[0].getaccount(self.address)[1].split("@")[0] + ) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Get initial balance + start_balance = Decimal(self.nodes[0].getaccount(self.address)[1].split("@")[0]) + self.nodes[0].generate(1) + + # Get balance after a block + end_balance = Decimal(self.nodes[0].getaccount(self.address)[1].split("@")[0]) + + # Calculate post-fork reward + post_fork_reward = end_balance - start_balance + + # Check rewards are the same + assert_equal(pre_fork_reward, post_fork_reward) + + def static_custom_multiple(self): + + # Test multiple custom reward + self.test_multiple_liquidity(self.symbolBTC, self.symbolDFI, 1, self.symbolETH) + + def static_commission_single(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{self.symbolLTC}", f"1000@{self.symbolDFI}"]}, + self.address, + ) + self.nodes[0].generate(1) + + # Store rollback block + rollback_block = self.nodes[0].getblockcount() + + # Swap LTC to DFI + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolLTC, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolDFI, + } + ) + self.nodes[0].generate(1) + + # Swap DFI to LTC + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolDFI, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolLTC, + } + ) + self.nodes[0].generate(1) + + # Get commission balances + pre_dfi_balance = Decimal( + self.nodes[0].getaccount(self.address)[0].split("@")[0] + ) + pre_ltc_balance = Decimal( + self.nodes[0].getaccount(self.address)[1].split("@")[0] + ) + + # Rollback swaps + self.rollback_to(rollback_block) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Swap LTC to DFI + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolLTC, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolDFI, + } + ) + self.nodes[0].generate(1) + + # Swap DFI to LTC + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolDFI, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolLTC, + } + ) + self.nodes[0].generate(1) + + # Get commission balances + post_dfi_balance = Decimal( + self.nodes[0].getaccount(self.address)[0].split("@")[0] + ) + post_ltc_balance = Decimal( + self.nodes[0].getaccount(self.address)[1].split("@")[0] + ) + + # Check commission is the same pre and post fork + assert_equal(pre_dfi_balance, post_dfi_balance) + assert_equal(pre_ltc_balance, post_ltc_balance) + + def static_commission_multiple(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool with multiple addresses + liquidity_addresses = self.fund_pool_multiple(self.symbolLTC, self.symbolDFI) + + # Store rollback block + rollback_block = self.nodes[0].getblockcount() + + # Swap LTC to DFI + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolLTC, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolDFI, + } + ) + self.nodes[0].generate(1) + + # Swap DFI to LTC + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolDFI, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolLTC, + } + ) + self.nodes[0].generate(1) + + # Get commission balances + pre_dfi_balances = [] + for address in liquidity_addresses: + pre_dfi_balances.append( + Decimal(self.nodes[0].getaccount(address)[0].split("@")[0]) + ) + pre_ltc_balances = [] + for address in liquidity_addresses: + pre_ltc_balances.append( + Decimal(self.nodes[0].getaccount(address)[1].split("@")[0]) + ) + + # Rollback swaps + self.rollback_to(rollback_block) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Swap LTC to DFI + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolLTC, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolDFI, + } + ) + self.nodes[0].generate(1) + + # Swap DFI to LTC + self.nodes[0].poolswap( + { + "from": self.owner_address, + "tokenFrom": self.symbolDFI, + "amountFrom": 1, + "to": self.owner_address, + "tokenTo": self.symbolLTC, + } + ) + self.nodes[0].generate(1) + + # Get commission balances + post_dfi_balances = [] + for address in liquidity_addresses: + post_dfi_balances.append( + Decimal(self.nodes[0].getaccount(address)[0].split("@")[0]) + ) + post_ltc_balances = [] + for address in liquidity_addresses: + post_ltc_balances.append( + Decimal(self.nodes[0].getaccount(address)[1].split("@")[0]) + ) + + # Check commission is the same pre and post fork + assert_equal(pre_dfi_balances, post_dfi_balances) + assert_equal(pre_ltc_balances, post_ltc_balances) + + def lp_split_zero_reward(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Fund pool + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{self.symbolBTC}", f"1000@{self.symbolDFI}"]}, + self.address, + ) + self.nodes[0].generate(1) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Set pool pair splits to different pool + self.nodes[0].setgov({"LP_SPLITS": {self.ltc_pool_id: 1}}) + self.nodes[0].generate(1) + + # Get initial balance + start_balance = Decimal(self.nodes[0].getaccount(self.address)[0].split("@")[0]) + self.nodes[0].generate(1) + + # Get balance after a block + end_balance = Decimal(self.nodes[0].getaccount(self.address)[0].split("@")[0]) + + # Check balances are the same + assert_equal(start_balance, end_balance) + + def liquidity_before_setting_splits(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Fund pool + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{self.symbolLTC}", f"1000@{self.symbolDFI}"]}, + self.address, + ) + self.nodes[0].generate(1) + + # Set pool pair splits to different pool and calculate owner rewards + self.nodes[0].setgov({"LP_SPLITS": {self.ltc_pool_id: 1}}) + self.nodes[0].generate(1) + + # Get initial balance + start_balance = Decimal(self.nodes[0].getaccount(self.address)[0].split("@")[0]) + self.nodes[0].generate(1) + + # Get balance after a block + end_balance = Decimal(self.nodes[0].getaccount(self.address)[0].split("@")[0]) + + # Calculate new pool reward + new_pool_reward = end_balance - start_balance + + # Add 1 Sat to start balance as new reward is higher precsision over multiple blocks + old_reward = self.pre_fork_reward + Decimal("0.00000001") + + # Check rewards matches + assert_equal(old_reward, new_pool_reward) + + def liquidity_after_setting_splits(self): + + # Rollback block + self.rollback_to(self.start_block) + + # Move to fork height + self.nodes[0].generate(self.df24height - self.nodes[0].getblockcount()) + + # Set pool pair splits to different pool + self.nodes[0].setgov({"LP_SPLITS": {self.ltc_pool_id: 1}}) + self.nodes[0].generate(10) + + # Fund pool and calculate owner rewards + self.nodes[0].addpoolliquidity( + {self.owner_address: [f"1000@{self.symbolLTC}", f"1000@{self.symbolDFI}"]}, + self.address, + ) + self.nodes[0].generate(2) + + # Balance started at zero, new balance is the reward + new_pool_reward = Decimal( + self.nodes[0].getaccount(self.address)[0].split("@")[0] + ) + + # Check rewards matches + assert_equal(self.pre_fork_reward, new_pool_reward) + + +if __name__ == "__main__": + TokenFractionalSplitTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 762d452e770..4d377876cc6 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -298,6 +298,7 @@ "feature_loan_estimateloan.py", "feature_loan_priceupdate.py", "feature_loan_vaultstate.py", + "feature_static_pool_rewards.py", "feature_loan.py", "feature_evm_miner.py", "feature_evm.py",