From b45613cc48c959dea2df72956b518e72c21a772b Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Wed, 22 Jun 2022 20:57:43 +0100 Subject: [PATCH] Fix commission for first deposit after fork (#1348) * Commission missing if added before first pool swap * Fix commission missing before swap --- src/masternodes/poolpairs.cpp | 82 +++--- test/functional/feature_commission_fix.py | 304 ++++++++++++++++++++++ test/functional/test_runner.py | 1 + 3 files changed, 355 insertions(+), 32 deletions(-) create mode 100755 test/functional/feature_commission_fix.py diff --git a/src/masternodes/poolpairs.cpp b/src/masternodes/poolpairs.cpp index 6aea7738816..fb79d74e565 100644 --- a/src/masternodes/poolpairs.cpp +++ b/src/masternodes/poolpairs.cpp @@ -2,12 +2,15 @@ // Distributed under the MIT software license, see the accompanying // file LICENSE or http://www.opensource.org/licenses/mit-license.php. -#include #include + +#include #include #include #include +#include + struct PoolSwapValue { bool swapEvent; CAmount blockCommissionA; @@ -201,28 +204,50 @@ inline CAmount liquidityReward(CAmount reward, CAmount liquidity, CAmount totalL return static_cast((arith_uint256(reward) * arith_uint256(liquidity) / arith_uint256(totalLiquidity)).GetLow64()); } +template +bool MatchPoolId(TIterator & it, DCT_ID poolId) { + return it.Valid() && it.Key().poolID == poolId; +} + template void ReadValueMoveToNext(TIterator & it, DCT_ID poolId, ValueType & value, uint32_t & height) { - if (it.Valid() && it.Key().poolID == poolId) { + if (MatchPoolId(it, poolId)) { value = it.Value(); /// @Note we store keys in desc order so Prev is actually go in forward it.Prev(); - if (it.Valid() && it.Key().poolID == poolId) { - height = it.Key().height; - } else { - height = UINT_MAX; - } + height = MatchPoolId(it, poolId) ? it.Key().height : UINT_MAX; } else { - value = {}; height = UINT_MAX; } } +template +auto InitPoolVars(CPoolPairView & view, PoolHeightKey poolKey, uint32_t end) { + + auto poolId = poolKey.poolID; + auto it = view.LowerBound(poolKey); + + auto height = poolKey.height; + static const uint32_t startHeight = Params().GetConsensus().GreatWorldHeight; + poolKey.height = std::max(height, startHeight); + + while (!MatchPoolId(it, poolId) && poolKey.height < end) { + height = poolKey.height; + it.Seek(poolKey); + poolKey.height++; + } + + Value value = MatchPoolId(it, poolId) ? it.Value() : Value{}; + + return std::make_tuple(std::move(value), std::move(it), height); +} + 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); @@ -231,28 +256,19 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, std::function(poolKey); - auto itPoolLoanReward = LowerBound(poolKey); + auto [poolReward, itPoolReward, startPoolReward] = InitPoolVars(*this, poolKey, end); + auto nextPoolReward = startPoolReward; - CAmount totalLiquidity = 0; - auto nextTotalLiquidity = begin; - auto itTotalLiquidity = LowerBound(poolKey); + auto [poolLoanReward, itPoolLoanReward, startPoolLoanReward] = InitPoolVars(*this, poolKey, end); + auto nextPoolLoanReward = startPoolLoanReward; - CBalances customRewards; - auto nextCustomRewards = begin; - auto itCustomRewards = LowerBound(poolKey); + auto [totalLiquidity, itTotalLiquidity, nextTotalLiquidity] = InitPoolVars(*this, poolKey, end); - PoolSwapValue poolSwap{}; - auto nextPoolSwap = UINT_MAX; - auto poolSwapHeight = UINT_MAX; - auto itPoolSwap = LowerBound(poolKey); - if (itPoolSwap.Valid() && itPoolSwap.Key().poolID == poolId) { - nextPoolSwap = itPoolSwap.Key().height; - } + auto [customRewards, itCustomRewards, startCustomRewards] = InitPoolVars(*this, poolKey, end); + auto nextCustomRewards = startCustomRewards; + + auto [poolSwap, itPoolSwap, poolSwapHeight] = InitPoolVars(*this, poolKey, end); + auto nextPoolSwap = poolSwapHeight; for (auto height = begin; height < end;) { // find suitable pool liquidity @@ -277,7 +293,7 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, std::function= startPoolReward && poolReward != 0) { CAmount providerReward = 0; if (height < newCalcHeight) { // old calculation uint32_t liqWeight = liquidity * PRECISION / totalLiquidity; @@ -287,7 +303,7 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, std::function= startPoolLoanReward && poolLoanReward != 0) { CAmount providerReward = liquidityReward(poolLoanReward, liquidity, totalLiquidity); onReward(RewardType::LoanTokenDEXReward, {DCT_ID{0}, providerReward}, height); } @@ -310,9 +326,11 @@ void CPoolPairView::CalculatePoolRewards(DCT_ID const & poolId, std::function= startCustomRewards) { + for (const auto& reward : customRewards.balances) { + if (auto providerReward = liquidityReward(reward.second, liquidity, totalLiquidity)) { + onReward(RewardType::Pool, {reward.first, providerReward}, height); + } } } ++height; diff --git a/test/functional/feature_commission_fix.py b/test/functional/feature_commission_fix.py new file mode 100755 index 00000000000..5108133aab3 --- /dev/null +++ b/test/functional/feature_commission_fix.py @@ -0,0 +1,304 @@ +#!/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 commission fix""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal +from decimal import Decimal +import time + +class CommissionFixTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.great_world = 200 + self.extra_args = [ + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-fortcanningheight=1', '-fortcanningmuseumheight=1', '-fortcanninghillheight=1', '-fortcanningroadheight=1', '-fortcanningcrunchheight=1', f'-greatworldheight={self.great_world}', '-subsidytest=1']] + + def run_test(self): + # Set up test tokens + self.setup_test_tokens() + + # Set up pool + self.setup_test_pool() + + # Test pool commission + self.pool_commission() + + # Set up pool after fork + self.setup_test_pool_fork() + + # Test pool commission after fork + self.pool_commission_fork() + + def setup_test_tokens(self): + # Generate chain + self.nodes[0].generate(101) + + # Symbols + self.symbolDUSD = 'DUSD' + self.symbolGOOGL = 'GOOGL' + self.symbolTSLA = 'TSLA' + self.symbolGD = 'GOOGL-DUSD' + self.symbolTD = 'TSLA-DUSD' + + # Store address + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + + # Price feeds + price_feed = [ + {"currency": "USD", "token": self.symbolDUSD}, + {"currency": "USD", "token": self.symbolTSLA}, + ] + + # Appoint oracle + oracle_address = self.nodes[0].getnewaddress("", "legacy") + self.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.symbolDUSD}"}, + {"currency": "USD", "tokenAmount": f"1@{self.symbolTSLA}"}, + ] + self.nodes[0].setoracledata(self.oracle, int(time.time()), oracle_prices) + self.nodes[0].generate(10) + + # Create tokens + self.nodes[0].createtoken({ + 'symbol': self.symbolGOOGL, + 'name': self.symbolGOOGL, + "isDAT": True, + "collateralAddress": self.address + }) + self.nodes[0].generate(1) + + self.nodes[0].createtoken({ + 'symbol': self.symbolDUSD, + 'name': self.symbolDUSD, + "isDAT": True, + "collateralAddress": self.address + }) + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': self.symbolTSLA, + 'name': self.symbolTSLA, + 'fixedIntervalPriceId': f"{self.symbolTSLA}/USD", + "isDAT": True, + 'interest': 0 + }) + self.nodes[0].generate(1) + + # Store token IDs + self.idDUSD = list(self.nodes[0].gettoken(self.symbolDUSD).keys())[0] + self.idGOOGL = list(self.nodes[0].gettoken(self.symbolGOOGL).keys())[0] + self.idTSLA = list(self.nodes[0].gettoken(self.symbolTSLA).keys())[0] + + # Mint some loan tokens + self.nodes[0].minttokens([ + f'1000000@{self.symbolDUSD}', + f'1000000@{self.symbolGOOGL}', + f'1000000@{self.symbolTSLA}', + ]) + self.nodes[0].generate(1) + + def setup_test_pool(self): + + # Create pool pair + self.nodes[0].createpoolpair({ + "tokenA": self.symbolGOOGL, + "tokenB": self.symbolDUSD, + "commission": 0.01, + "status": True, + "ownerAddress": self.address, + "symbol": self.symbolGD + }) + self.nodes[0].generate(1) + + # Store pool ID + self.idGD = list(self.nodes[0].gettoken(self.symbolGD).keys())[0] + + def setup_test_pool_fork(self): + + # Create pool pair + self.nodes[0].createpoolpair({ + "tokenA": self.symbolTSLA, + "tokenB": self.symbolDUSD, + "commission": 0.01, + "status": True, + "ownerAddress": self.address, + "symbol": self.symbolTD + }) + self.nodes[0].generate(1) + + # Store pool ID + self.idTD = list(self.nodes[0].gettoken(self.symbolTD).keys())[0] + + def pool_commission(self): + + # Set up commission address + commission_address = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].sendtoaddress(commission_address, 1) + self.nodes[0].accounttoaccount(self.address, { + commission_address: [f'1000@{self.symbolGOOGL}', f'1000@{self.symbolDUSD}'] + }) + self.nodes[0].generate(1) + + # Save block for revert + revert_block = self.nodes[0].getblockcount() + 1 + + # Add pool liquidity + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolGOOGL}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Add liquidity twice for valid GetShare, possible bug? + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolGOOGL}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Execute pool swap + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": self.symbolGOOGL, + "amountFrom": 1, + "to": self.address, + "tokenTo": self.symbolDUSD + }) + self.nodes[0].generate(2) + + # Commission missing + for result in self.nodes[0].listaccounthistory(commission_address): + assert(result['type'] != 'Commission') + + # Show Commission with low depth + result = self.nodes[0].listaccounthistory(commission_address, {'depth': 1}) + assert_equal(result[0]['type'], 'Commission') + + # Test accounttoaccount temp fix + self.nodes[0].accounttoaccount(commission_address, { + commission_address: self.nodes[0].getaccount(commission_address) + }) + self.nodes[0].generate(1) + + # Execute pool swap + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": self.symbolGOOGL, + "amountFrom": 1, + "to": self.address, + "tokenTo": self.symbolDUSD + }) + self.nodes[0].generate(2) + + # Show Commission + result = self.nodes[0].listaccounthistory(commission_address) + assert_equal(result[0]['type'], 'Commission') + + # Revert to before add liqudity + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(revert_block)) + self.nodes[0].clearmempool() + + # Check pool empty + result = self.nodes[0].getpoolpair(self.symbolGD) + assert_equal(result[f'{self.idGD}']['reserveA'], Decimal('0')) + + # Move to fork + self.nodes[0].generate(self.great_world - self.nodes[0].getblockcount()) + + # Add pool liquidity + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolGOOGL}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Add liquidity twice for valid GetShare, possible bug? + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolGOOGL}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Execute pool swap + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": self.symbolGOOGL, + "amountFrom": 1, + "to": self.address, + "tokenTo": self.symbolDUSD + }) + self.nodes[0].generate(2) + + # Show Commission + result = self.nodes[0].listaccounthistory(commission_address) + assert_equal(result[0]['type'], 'Commission') + + def pool_commission_fork(self): + + # Set up commission address + commission_address = self.nodes[0].getnewaddress("", "legacy") + self.nodes[0].sendtoaddress(commission_address, 1) + self.nodes[0].accounttoaccount(self.address, { + commission_address: [f'1000@{self.symbolTSLA}', f'1000@{self.symbolDUSD}'] + }) + self.nodes[0].generate(1) + + # Add pool liquidity + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolTSLA}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Add liquidity twice for valid GetShare, possible bug? + self.nodes[0].addpoolliquidity({ + commission_address: [f'100@{self.symbolTSLA}', f'100@{self.symbolDUSD}'] + }, commission_address) + self.nodes[0].generate(1) + + # Execute pool swap + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": self.symbolTSLA, + "amountFrom": 1, + "to": self.address, + "tokenTo": self.symbolDUSD + }) + self.nodes[0].generate(2) + + # Show Commission + result = self.nodes[0].listaccounthistory(commission_address) + assert_equal(result[0]['type'], 'Commission') + + # Token split + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/oracles/splits/{str(self.nodes[0].getblockcount() + 2)}':f'{self.idTSLA}/2'}}) + self.nodes[0].generate(2) + + # Swap old for new values + self.idTSLA = list(self.nodes[0].gettoken(self.symbolTSLA).keys())[0] + + # Unlock token + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/locks/token/{self.idTSLA}':'false'}}) + self.nodes[0].generate(1) + + # Execute pool swap + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": self.symbolTSLA, + "amountFrom": 1, + "to": self.address, + "tokenTo": self.symbolDUSD + }) + self.nodes[0].generate(2) + + # Show Commission + result = self.nodes[0].listaccounthistory(commission_address) + assert_equal(result[0]['type'], 'Commission') + +if __name__ == '__main__': + CommissionFixTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a1332097ceb..b3a77d8ecd5 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -182,6 +182,7 @@ 'feature_poolswap_mechanism.py', 'feature_poolswap_mainnet.py', 'feature_prevent_bad_tx_propagation.py', + 'feature_commission_fix.py', 'feature_masternode_operator.py', 'feature_mine_cached.py', 'feature_mempool_dakota.py',