diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index a6f0ed291ff..b7cde40a1a2 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -132,8 +132,10 @@ const std::map>& ATTRIBUTES::allowedKeys }, { AttributeTypes::Poolpairs, { - {"token_a_fee_pct", PoolKeys::TokenAFeePCT}, - {"token_b_fee_pct", PoolKeys::TokenBFeePCT}, + {"token_a_fee_pct", PoolKeys::TokenAFeePCT}, + {"token_a_fee_direction",PoolKeys::TokenAFeeDir}, + {"token_b_fee_pct", PoolKeys::TokenBFeePCT}, + {"token_b_fee_direction",PoolKeys::TokenBFeeDir}, } }, { @@ -173,7 +175,9 @@ const std::map>& ATTRIBUTES::displayKeys { AttributeTypes::Poolpairs, { {PoolKeys::TokenAFeePCT, "token_a_fee_pct"}, + {PoolKeys::TokenAFeeDir, "token_a_fee_direction"}, {PoolKeys::TokenBFeePCT, "token_b_fee_pct"}, + {PoolKeys::TokenBFeeDir, "token_b_fee_direction"}, } }, { @@ -282,6 +286,16 @@ static ResVal VerifyCurrencyPair(const std::string& str) { return {CTokenCurrencyPair{token, currency}, Res::Ok()}; } +static std::set dirSet{"in", "out", "both"}; + +static ResVal VerifyFeeDirection(const std::string& str) { + auto lowerStr = ToLower(str); + if (!dirSet.count(lowerStr)) { + return Res::Err("Fee direction value must be both, in or out"); + } + return {lowerStr, Res::Ok()}; +} + static bool VerifyToken(const CCustomCSView& view, const uint32_t id) { return view.GetToken(DCT_ID{id}).has_value(); } @@ -316,7 +330,9 @@ const std::mapfirst, ascendantPair->second)); } else if (const auto currencyPair = std::get_if(&attribute.second)) { ret.pushKV(key, currencyPair->first + '/' + currencyPair->second); + } else if (const auto str = std::get_if(&attribute.second)) { + ret.pushKV(key, *str); } } catch (const std::out_of_range&) { // Should not get here, that's mean maps are mismatched @@ -839,9 +857,6 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const break; case AttributeTypes::Poolpairs: - if (!std::get_if(&attribute.second)) { - return Res::Err("Unsupported value"); - } switch (attrV0->key) { case PoolKeys::TokenAFeePCT: case PoolKeys::TokenBFeePCT: @@ -849,6 +864,15 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const return Res::Err("No such pool (%d)", attrV0->typeId); } break; + case PoolKeys::TokenAFeeDir: + case PoolKeys::TokenBFeeDir: + if (view.GetLastHeight() < Params().GetConsensus().FortCanningGardensHeight) { + return Res::Err("Cannot be set before FortCanningGardensHeight"); + } + if (!view.GetPoolPair({attrV0->typeId})) { + return Res::Err("No such pool (%d)", attrV0->typeId); + } + break; default: return Res::Err("Unsupported key"); } @@ -896,20 +920,23 @@ Res ATTRIBUTES::Apply(CCustomCSView & mnview, const uint32_t height) continue; } if (attrV0->type == AttributeTypes::Poolpairs) { - auto poolId = DCT_ID{attrV0->typeId}; - auto pool = mnview.GetPoolPair(poolId); - if (!pool) { - return Res::Err("No such pool (%d)", poolId.v); - } - auto tokenId = attrV0->key == PoolKeys::TokenAFeePCT ? - pool->idTokenA : pool->idTokenB; + if (attrV0->key == PoolKeys::TokenAFeePCT || + attrV0->key == PoolKeys::TokenBFeePCT) { + auto poolId = DCT_ID{attrV0->typeId}; + auto pool = mnview.GetPoolPair(poolId); + if (!pool) { + return Res::Err("No such pool (%d)", poolId.v); + } + auto tokenId = attrV0->key == PoolKeys::TokenAFeePCT ? + pool->idTokenA : pool->idTokenB; - const auto valuePct = std::get_if(&attribute.second); - if (!valuePct) { - return Res::Err("Unexpected type"); - } - if (auto res = mnview.SetDexFeePct(poolId, tokenId, *valuePct); !res) { - return res; + const auto valuePct = std::get_if(&attribute.second); + if (!valuePct) { + return Res::Err("Unexpected type"); + } + if (auto res = mnview.SetDexFeePct(poolId, tokenId, *valuePct); !res) { + return res; + } } } else if (attrV0->type == AttributeTypes::Token) { if (attrV0->key == TokenKeys::DexInFeePct diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index c8be629b4ab..644169a6383 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -71,6 +71,8 @@ enum TokenKeys : uint8_t { enum PoolKeys : uint8_t { TokenAFeePCT = 'a', TokenBFeePCT = 'b', + TokenAFeeDir = 'c', + TokenBFeeDir = 'd', }; struct CDataStructureV0 { @@ -133,7 +135,7 @@ using OracleSplits = std::map; using DescendantValue = std::pair; using AscendantValue = std::pair; using CAttributeType = std::variant; -using CAttributeValue = std::variant; +using CAttributeValue = std::variant; enum GovVarsFilter { All, diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 8891a427f61..4a034e61796 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -4298,15 +4298,24 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector poolIDs, boo auto dexfeeInPct = view.GetDexFeeInPct(currentID, swapAmount.nTokenId); + const auto attributes = view.GetAttributes(); + assert(attributes); + + CDataStructureV0 dirAKey{AttributeTypes::Poolpairs, currentID.v, PoolKeys::TokenAFeeDir}; + CDataStructureV0 dirBKey{AttributeTypes::Poolpairs, currentID.v, PoolKeys::TokenBFeeDir}; + const auto dirA = attributes->GetValue(dirAKey, std::string{"both"}); + const auto dirB = attributes->GetValue(dirBKey, std::string{"both"}); + const auto asymmetricFee = std::make_pair(dirA, dirB); + // Perform swap - poolResult = pool->Swap(swapAmount, dexfeeInPct, poolPrice, [&] (const CTokenAmount& dexfeeInAmount, const CTokenAmount& tokenAmount) { + poolResult = pool->Swap(swapAmount, dexfeeInPct, poolPrice, asymmetricFee, [&] (const CTokenAmount& dexfeeInAmount, const CTokenAmount& tokenAmount) { // Save swap amount for next loop swapAmountResult = tokenAmount; CTokenAmount dexfeeOutAmount{tokenAmount.nTokenId, 0}; auto dexfeeOutPct = view.GetDexFeeOutPct(currentID, tokenAmount.nTokenId); - if (dexfeeOutPct > 0) { + if (dexfeeOutPct > 0 && poolOutFee(swapAmount.nTokenId == pool->idTokenA, asymmetricFee)) { dexfeeOutAmount.nValue = MultiplyAmounts(tokenAmount.nValue, dexfeeOutPct); swapAmountResult.nValue -= dexfeeOutAmount.nValue; } diff --git a/src/masternodes/poolpairs.cpp b/src/masternodes/poolpairs.cpp index 1620a6d15de..d1591c7da75 100644 --- a/src/masternodes/poolpairs.cpp +++ b/src/masternodes/poolpairs.cpp @@ -387,7 +387,7 @@ Res CPoolPair::RemoveLiquidity(CAmount liqAmount, std::function onTransfer, int height) { +Res CPoolPair::Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, const std::pair& asymmetricFee, 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() + ")"); @@ -422,7 +422,7 @@ Res CPoolPair::Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxP } CTokenAmount dexfeeInAmount{in.nTokenId, 0}; - if (dexfeeInPct > 0) { + if (dexfeeInPct > 0 && poolInFee(forward, asymmetricFee)) { if (dexfeeInPct > COIN) { return Res::Err("Dex fee input percentage over 100%%"); } diff --git a/src/masternodes/poolpairs.h b/src/masternodes/poolpairs.h index f413e5ef133..b698ac5cce0 100644 --- a/src/masternodes/poolpairs.h +++ b/src/masternodes/poolpairs.h @@ -127,7 +127,7 @@ class CPoolPair : public CPoolPairMessage Res AddLiquidity(CAmount amountA, CAmount amountB, std::function onMint, bool slippageProtection = false); Res RemoveLiquidity(CAmount liqAmount, std::function onReclaim); - Res Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, std::function onTransfer, int height = INT_MAX); + Res Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, const std::pair& asymmetricFee, std::function onTransfer, int height = INT_MAX); private: CAmount slopeSwap(CAmount unswapped, CAmount & poolFrom, CAmount & poolTo, int height); @@ -282,4 +282,20 @@ struct CRemoveLiquidityMessage { } }; +inline bool poolInFee(const bool forward, const std::pair& asymmetricFee) { + const auto& [dirA, dirB] = asymmetricFee; + if ((forward && (dirA == "both" || dirA == "in")) || (!forward && (dirB == "both" || dirB == "in"))) { + return true; + } + return false; +} + +inline bool poolOutFee(const bool forward, const std::pair& asymmetricFee) { + const auto& [dirA, dirB] = asymmetricFee; + if ((forward && (dirB == "both" || dirB == "out")) || (!forward && (dirA == "both" || dirA == "out"))) { + return true; + } + return false; +} + #endif // DEFI_MASTERNODES_POOLPAIRS_H diff --git a/src/masternodes/rpc_poolpair.cpp b/src/masternodes/rpc_poolpair.cpp index b43ade3da5a..55e3eecd2c1 100644 --- a/src/masternodes/rpc_poolpair.cpp +++ b/src/masternodes/rpc_poolpair.cpp @@ -1,6 +1,8 @@ #include -UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token, bool verbose) { +#include + +UniValue poolToJSON(const CCustomCSView view, DCT_ID const& id, CPoolPair const& pool, CToken const& token, bool verbose) { UniValue poolObj(UniValue::VOBJ); poolObj.pushKV("symbol", token.symbol); poolObj.pushKV("name", token.name); @@ -9,20 +11,38 @@ UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token poolObj.pushKV("idTokenB", pool.idTokenB.ToString()); if (verbose) { + const auto attributes = view.GetAttributes(); + assert(attributes); + + CDataStructureV0 dirAKey{AttributeTypes::Poolpairs, id.v, PoolKeys::TokenAFeeDir}; + CDataStructureV0 dirBKey{AttributeTypes::Poolpairs, id.v, PoolKeys::TokenBFeeDir}; + const auto dirA = attributes->GetValue(dirAKey, std::string{"both"}); + const auto dirB = attributes->GetValue(dirBKey, std::string{"both"}); + if (const auto dexFee = pcustomcsview->GetDexFeeInPct(id, pool.idTokenA)) { poolObj.pushKV("dexFeePctTokenA", ValueFromAmount(dexFee)); - poolObj.pushKV("dexFeeInPctTokenA", ValueFromAmount(dexFee)); + if (dirA == "in" || dirA == "both") { + poolObj.pushKV("dexFeeInPctTokenA", ValueFromAmount(dexFee)); + } } if (const auto dexFee = pcustomcsview->GetDexFeeOutPct(id, pool.idTokenB)) { poolObj.pushKV("dexFeePctTokenB", ValueFromAmount(dexFee)); - poolObj.pushKV("dexFeeOutPctTokenB", ValueFromAmount(dexFee)); + + if (dirB == "out" || dirB == "both") { + poolObj.pushKV("dexFeeOutPctTokenB", ValueFromAmount(dexFee)); + } } if (const auto dexFee = pcustomcsview->GetDexFeeInPct(id, pool.idTokenB)) { - poolObj.pushKV("dexFeeInPctTokenB", ValueFromAmount(dexFee)); + if (dirB == "in" || dirB == "both") { + poolObj.pushKV("dexFeeInPctTokenB", ValueFromAmount(dexFee)); + } } if (const auto dexFee = pcustomcsview->GetDexFeeOutPct(id, pool.idTokenA)) { - poolObj.pushKV("dexFeeOutPctTokenA", ValueFromAmount(dexFee)); + if (dirA == "out" || dirA == "both") { + poolObj.pushKV("dexFeeOutPctTokenA", ValueFromAmount(dexFee)); + } } + poolObj.pushKV("reserveA", ValueFromAmount(pool.reserveA)); poolObj.pushKV("reserveB", ValueFromAmount(pool.reserveB)); poolObj.pushKV("commission", ValueFromAmount(pool.commission)); @@ -218,7 +238,7 @@ UniValue listpoolpairs(const JSONRPCRequest& request) { pcustomcsview->ForEachPoolPair([&](DCT_ID const & id, CPoolPair pool) { const auto token = pcustomcsview->GetToken(id); if (token) { - ret.pushKVs(poolToJSON(id, pool, *token, verbose)); + ret.pushKVs(poolToJSON(*pcustomcsview, id, pool, *token, verbose)); limit--; } @@ -260,7 +280,7 @@ UniValue getpoolpair(const JSONRPCRequest& request) { if (token) { auto pool = pcustomcsview->GetPoolPair(id); if (pool) { - auto res = poolToJSON(id, *pool, *token, verbose); + auto res = poolToJSON(*pcustomcsview, id, *pool, *token, verbose); return GetRPCResultCache().Set(request, res); } throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Pool not found"); @@ -1083,7 +1103,16 @@ UniValue testpoolswap(const JSONRPCRequest& request) { auto dexfeeInPct = mnview_dummy.GetDexFeeInPct(poolPair->first, poolSwapMsg.idTokenFrom); - res = pp.Swap({poolSwapMsg.idTokenFrom, poolSwapMsg.amountFrom}, dexfeeInPct, poolSwapMsg.maxPrice, [&] (const CTokenAmount &, const CTokenAmount &tokenAmount) { + const auto attributes = mnview_dummy.GetAttributes(); + assert(attributes); + + CDataStructureV0 dirAKey{AttributeTypes::Poolpairs, poolPair->first.v, PoolKeys::TokenAFeeDir}; + CDataStructureV0 dirBKey{AttributeTypes::Poolpairs, poolPair->first.v, PoolKeys::TokenBFeeDir}; + const auto dirA = attributes->GetValue(dirAKey, std::string{"both"}); + const auto dirB = attributes->GetValue(dirBKey, std::string{"both"}); + const auto asymmetricFee = std::make_pair(dirA, dirB); + + res = pp.Swap({poolSwapMsg.idTokenFrom, poolSwapMsg.amountFrom}, dexfeeInPct, poolSwapMsg.maxPrice, asymmetricFee, [&] (const CTokenAmount &, const CTokenAmount &tokenAmount) { auto resPP = mnview_dummy.SetPoolPair(poolPair->first, targetHeight, pp); if (!resPP) { return resPP; diff --git a/src/test/liquidity_tests.cpp b/src/test/liquidity_tests.cpp index 483510ba156..df539f79ceb 100644 --- a/src/test/liquidity_tests.cpp +++ b/src/test/liquidity_tests.cpp @@ -7,6 +7,7 @@ #include +std::pair asymmetricFee{"both", "both"}; inline uint256 NextTx() { @@ -135,11 +136,11 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) BOOST_CHECK(!res.ok && res.msg == "amounts too low, zero liquidity"); // we can't swap forward even 1 satoshi - res = pool.Swap(CTokenAmount{pool.idTokenA, 1}, 0, PoolPrice{std::numeric_limits::max(), 0}, FAIL_onSwap); + res = pool.Swap(CTokenAmount{pool.idTokenA, 1}, 0, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, FAIL_onSwap); BOOST_CHECK(!res.ok && res.msg == "Lack of liquidity."); // and backward too - res = pool.Swap(CTokenAmount{pool.idTokenB, 2}, 0, PoolPrice{std::numeric_limits::max(), 0}, FAIL_onSwap); + res = pool.Swap(CTokenAmount{pool.idTokenB, 2}, 0, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, FAIL_onSwap); BOOST_CHECK(!res.ok && res.msg == "Lack of liquidity."); // thats all, we can't place anything here until removing. trading disabled due to reserveB < SLOPE_SWAP_RATE @@ -171,7 +172,7 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) return Res::Ok(); }); auto dexfeeInPct = 5 * COIN / 100; - res = pool.Swap(CTokenAmount{pool.idTokenA, 1000000}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ + res = pool.Swap(CTokenAmount{pool.idTokenA, 1000000}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ auto trade = MultiplyAmounts(1000000, pool.commission); auto amount = 1000000 - trade; BOOST_CHECK_EQUAL(df.nValue, MultiplyAmounts(amount, dexfeeInPct)); @@ -191,7 +192,7 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) return Res::Ok(); }); auto dexfeeInPct = 1 * COIN / 100; - res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ + res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ auto trade = MultiplyAmounts(2*COIN, pool.commission); auto amount = 2*COIN - trade; BOOST_CHECK_EQUAL(df.nValue, MultiplyAmounts(amount, dexfeeInPct)); @@ -211,7 +212,7 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) return Res::Ok(); }); auto dexfeeInPct = 12 * COIN / 100; - res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ + res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ auto trade = MultiplyAmounts(2*COIN, pool.commission); auto amount = 2*COIN - trade; BOOST_CHECK_EQUAL(df.nValue, MultiplyAmounts(amount, dexfeeInPct)); @@ -229,7 +230,7 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) res = pool.AddLiquidity(COIN, 1000*COIN, [](CAmount liq)-> Res { return Res::Ok(); }); - res = pool.Swap(CTokenAmount{pool.idTokenA, COIN}, 0, PoolPrice{std::numeric_limits::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ + res = pool.Swap(CTokenAmount{pool.idTokenA, COIN}, 0, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ BOOST_CHECK_EQUAL(df.nValue, 0); BOOST_CHECK_EQUAL(ta.nValue, 49748743718); // pre-optimization: 49773755285 return Res::Ok(); @@ -245,7 +246,7 @@ BOOST_AUTO_TEST_CASE(math_liquidity_and_trade) res = pool.AddLiquidity(COIN, 1000*COIN, [](CAmount liq)-> Res { return Res::Ok(); }); - res = pool.Swap(CTokenAmount{pool.idTokenA, COIN/1000}, 0, PoolPrice{std::numeric_limits::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ + res = pool.Swap(CTokenAmount{pool.idTokenA, COIN/1000}, 0, PoolPrice{std::numeric_limits::max(), 0}, asymmetricFee, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{ BOOST_CHECK_EQUAL(df.nValue, 0); BOOST_CHECK_EQUAL(ta.nValue, 98902086); // pre-optimization: 99000000 return Res::Ok(); diff --git a/test/functional/feature_asymmetric_fee.py b/test/functional/feature_asymmetric_fee.py new file mode 100755 index 00000000000..e16c9fa7453 --- /dev/null +++ b/test/functional/feature_asymmetric_fee.py @@ -0,0 +1,299 @@ +#!/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 asymmetric pool fees""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal + +from decimal import Decimal +from math import trunc + +class PoolPairAsymmetricTest (DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-bayfrontgardensheight=1', '-dakotaheight=1', '-fortcanningheight=1', '-fortcanninghillheight=1', '-fortcanningroadheight=1', '-fortcanninggardensheight=150', '-jellyfish_regtest=1']] + + def run_test(self): + + # Set up test tokens + self.setup_test_tokens() + + # Set up test pools + self.setup_test_pools() + + # Test pool swaps + self.pool_swaps() + + def pool_swaps(self): + + # Move to fork + self.nodes[0].generate(150 - self.nodes[0].getblockcount()) + + # Set DUSD fee on in only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_pct': '0.05', + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'in' + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert_equal(result['dexFeePctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenA'], Decimal('0.05000000')) + assert('dexFeeOutPctTokenA' not in result) + assert('dexFeePctTokenB' not in result) + assert('dexFeeInPctTokenB' not in result) + assert('dexFeeOutPctTokenB' not in result) + + # Test DFI to DUSD, no fees incurred + self.test_swap(self.symbolDFI, self.symbolDUSD, 0, 0) + + # Test DUSD to DFI, 5% fee on DUSD + self.test_swap(self.symbolDUSD, self.symbolDFI, Decimal('0.05'), 0) + + # Set DUSD fee on out only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'out' + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert_equal(result['dexFeePctTokenA'], Decimal('0.05000000')) + assert('dexFeeInPctTokenA' not in result) + assert_equal(result['dexFeeOutPctTokenA'], Decimal('0.05000000')) + assert('dexFeePctTokenB' not in result) + assert('dexFeeInPctTokenB' not in result) + assert('dexFeeOutPctTokenB' not in result) + + # Test DFI to DUSD, 5% fee on DUSD + self.test_swap(self.symbolDFI, self.symbolDUSD, 0, Decimal('0.05')) + + # Test DUSD toDFI, no fees incurred + self.test_swap(self.symbolDUSD, self.symbolDFI, 0, 0) + + # Set DFI fee on in only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_pct': '0', + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'both', + f'v0/poolpairs/{self.idDD}/token_b_fee_pct': '0.05', + f'v0/poolpairs/{self.idDD}/token_b_fee_direction': 'in', + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert('dexFeePctTokenA' not in result) + assert('dexFeeInPctTokenA' not in result) + assert('dexFeeOutPctTokenA' not in result) + assert_equal(result['dexFeePctTokenB'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenB'], Decimal('0.05000000')) + assert('dexFeeOutPctTokenB' not in result) + + # Test DFI to DUSD, 5% fee on DFI + self.test_swap(self.symbolDFI, self.symbolDUSD, Decimal('0.05'), 0) + + # Test DUSD to DFI, no fees incurred + self.test_swap(self.symbolDUSD, self.symbolDFI, 0, 0) + + # Set DFI fee on out only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_b_fee_direction': 'out', + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert('dexFeePctTokenA' not in result) + assert('dexFeeInPctTokenA' not in result) + assert('dexFeeOutPctTokenA' not in result) + assert_equal(result['dexFeePctTokenB'], Decimal('0.05000000')) + assert('dexFeeInPctTokenB' not in result) + assert_equal(result['dexFeeOutPctTokenB'], Decimal('0.05000000')) + + # Test DFI to DUSD, no fees incurred + self.test_swap(self.symbolDFI, self.symbolDUSD, 0, 0) + + # Test DUSD to DFI, 5% fee on DFI + self.test_swap(self.symbolDUSD, self.symbolDFI, 0, Decimal('0.05')) + + # Set DFI and DUSD fee on in only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_pct': '0.05', + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'in', + f'v0/poolpairs/{self.idDD}/token_b_fee_direction': 'in', + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert_equal(result['dexFeePctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenA'], Decimal('0.05000000')) + assert('dexFeeOutPctTokenA' not in result) + assert_equal(result['dexFeePctTokenB'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenB'], Decimal('0.05000000')) + assert('dexFeeOutPctTokenB' not in result) + + # Test DFI to DUSD, 5% fee on DFI + self.test_swap(self.symbolDFI, self.symbolDUSD, Decimal('0.05'), 0) + + # Test DUSD to DFI, 5% fee on DUSD + self.test_swap(self.symbolDUSD, self.symbolDFI, Decimal('0.05'), 0) + + # Set DFI and DUSD fee on out only + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'out', + f'v0/poolpairs/{self.idDD}/token_b_fee_direction': 'out', + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert_equal(result['dexFeePctTokenA'], Decimal('0.05000000')) + assert('dexFeeInPctTokenA' not in result) + assert_equal(result['dexFeeOutPctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeePctTokenB'], Decimal('0.05000000')) + assert('dexFeeInPctTokenB' not in result) + assert_equal(result['dexFeeOutPctTokenB'], Decimal('0.05000000')) + + # Test DFI to DUSD, 5% fee on DUSD + self.test_swap(self.symbolDFI, self.symbolDUSD, 0, Decimal('0.05')) + + # Test DUSD to DFI, 5% fee on DFI + self.test_swap(self.symbolDUSD, self.symbolDFI, 0, Decimal('0.05')) + + # Set DFI and DUSD fee on both, normal behaviour. + self.nodes[0].setgov({"ATTRIBUTES":{ + f'v0/poolpairs/{self.idDD}/token_a_fee_direction': 'both', + f'v0/poolpairs/{self.idDD}/token_b_fee_direction': 'both', + }}) + self.nodes[0].generate(1) + + # Check poolpair + result = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + assert_equal(result['dexFeePctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeeOutPctTokenA'], Decimal('0.05000000')) + assert_equal(result['dexFeePctTokenB'], Decimal('0.05000000')) + assert_equal(result['dexFeeInPctTokenB'], Decimal('0.05000000')) + assert_equal(result['dexFeeOutPctTokenB'], Decimal('0.05000000')) + + # Test DFI to DUSD, 5% fee on both + self.test_swap(self.symbolDFI, self.symbolDUSD, Decimal('0.05'), Decimal('0.05')) + + # Test DUSD to DFI, 5% fee on both + self.test_swap(self.symbolDUSD, self.symbolDFI, Decimal('0.05'), Decimal('0.05')) + + def setup_test_tokens(self): + + self.nodes[0].generate(101) + + # Symbols + self.symbolDFI = 'DFI' + self.symbolDUSD = 'DUSD' + self.symbolDD = 'DUSD-DFI' + + # Store address + self.address = self.nodes[0].get_genesis_keys().ownerAuthAddress + + # Create token + self.nodes[0].createtoken({ + "symbol": self.symbolDUSD, + "name": self.symbolDUSD, + "isDAT": True, + "collateralAddress": self.address + }) + self.nodes[0].generate(1) + + # Store token IDs + self.idDFI = list(self.nodes[0].gettoken(self.symbolDFI).keys())[0] + self.idDUSD = list(self.nodes[0].gettoken(self.symbolDUSD).keys())[0] + + # Mint some loan tokens + self.nodes[0].minttokens([ + f'1000000@{self.symbolDUSD}', + ]) + self.nodes[0].generate(1) + + # Fund address with account DFI + self.nodes[0].utxostoaccount({self.address: f'100000@{self.idDFI}'}) + self.nodes[0].generate(1) + + def setup_test_pools(self): + + self.nodes[0].createpoolpair({ + "tokenA": self.symbolDUSD, + "tokenB": self.symbolDFI, + "commission": Decimal('0'), + "status": True, + "ownerAddress": self.address, + "symbol" : self.symbolDD + }) + self.nodes[0].generate(1) + + # Fund pools + self.nodes[0].addpoolliquidity({ + self.address: [f'1000@{self.symbolDUSD}', f'100@{self.symbolDFI}'] + }, self.address) + + # Store pool ID + self.idDD = list(self.nodes[0].gettoken(self.symbolDD).keys())[0] + + def test_swap(self, token_from, token_to, fee_in, fee_out): + + # Create address + swap_address = self.nodes[0].getnewaddress("", "legacy") + + # Set amount to swap + swap_amount = Decimal('1') + + # Define coin + coin = 100000000 + + # Pre-swap values + pool = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + reserve_a = pool['reserveA'] + reserve_b = pool['reserveB'] + + # Swap DFI to DUSD, no fees. + self.nodes[0].poolswap({ + "from": self.address, + "tokenFrom": token_from, + "amountFrom": swap_amount, + "to": swap_address, + "tokenTo": token_to, + }) + + # Calculate dex in fee + dex_in_fee = swap_amount * fee_in + amount_in = swap_amount - dex_in_fee + + # Mint swap TX + self.nodes[0].generate(1) + + # Check results + pool = self.nodes[0].getpoolpair(self.idDD)[self.idDD] + if token_from == self.symbolDFI: + assert_equal(pool['reserveB'] - reserve_b, amount_in) + swapped = self.nodes[0].getaccount(swap_address, {}, True)[self.idDUSD] + reserve_diff = reserve_a - pool['reserveA'] + else: + assert_equal(pool['reserveA'] - reserve_a, amount_in) + swapped = self.nodes[0].getaccount(swap_address, {}, True)[self.idDFI] + reserve_diff = reserve_b - pool['reserveB'] + + # Check swap amount + dex_out_fee = round(trunc(reserve_diff * fee_out * coin) / coin, 8) + assert_equal(reserve_diff - Decimal(str(dex_out_fee)), swapped) + + # Rewind + self.nodes[0].invalidateblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount())) + self.nodes[0].clearmempool() + +if __name__ == '__main__': + PoolPairAsymmetricTest().main() diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index 743b5a86974..78484724884 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -21,8 +21,8 @@ def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-subsidytest=1'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-subsidytest=1']] + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-fortcanninggardensheight=1250', '-subsidytest=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-fortcanninggardensheight=1250', '-subsidytest=1']] def run_test(self): @@ -761,5 +761,51 @@ def run_test(self): attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] assert_equal(attributes['v0/locks/token/5'], 'true') + # Try and set Gov vars before fork + assert_raises_rpc_error(-32600, "Cannot be set before FortCanningGardensHeight", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/poolpairs/3/token_a_fee_direction': 'both'}}) + assert_raises_rpc_error(-32600, "Cannot be set before FortCanningGardensHeight", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/poolpairs/3/token_b_fee_direction': 'both'}}) + + # Move to fork + self.nodes[0].generate(1250 - self.nodes[0].getblockcount()) + + # Test invalid calls + assert_raises_rpc_error(-5, "Fee direction value must be both, in or out", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/poolpairs/3/token_a_fee_direction': 'invalid'}}) + + # Set fee direction Gov vars + self.nodes[0].setgov({"ATTRIBUTES":{ + 'v0/poolpairs/3/token_a_fee_direction': 'both', + 'v0/poolpairs/3/token_b_fee_direction': 'both', + }}) + self.nodes[0].generate(1) + + # Check attributes + result = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(result['v0/poolpairs/3/token_a_fee_direction'], 'both') + assert_equal(result['v0/poolpairs/3/token_b_fee_direction'], "both") + + # Set fee direction Gov vars + self.nodes[0].setgov({"ATTRIBUTES":{ + 'v0/poolpairs/3/token_a_fee_direction': 'in', + 'v0/poolpairs/3/token_b_fee_direction': 'in', + }}) + self.nodes[0].generate(1) + + # Check attributes + result = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(result['v0/poolpairs/3/token_a_fee_direction'], 'in') + assert_equal(result['v0/poolpairs/3/token_b_fee_direction'], "in") + + # Set fee direction Gov vars + self.nodes[0].setgov({"ATTRIBUTES":{ + 'v0/poolpairs/3/token_a_fee_direction': 'out', + 'v0/poolpairs/3/token_b_fee_direction': 'out', + }}) + self.nodes[0].generate(1) + + # Check attributes + result = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(result['v0/poolpairs/3/token_a_fee_direction'], 'out') + assert_equal(result['v0/poolpairs/3/token_b_fee_direction'], "out") + if __name__ == '__main__': GovsetTest ().main () diff --git a/test/functional/feature_token_split.py b/test/functional/feature_token_split.py index 4267552ed4e..36dc52167e7 100755 --- a/test/functional/feature_token_split.py +++ b/test/functional/feature_token_split.py @@ -386,9 +386,11 @@ def check_pool_split(self, pool_id, pool_symbol, token_id, token_symbol, token_s assert_equal(result['status'], False) assert_equal(result['tradeEnabled'], False) assert('dexFeePctTokenA' not in result) - assert('dexFeePctTokenB' not in result) assert('dexFeeInPctTokenA' not in result) assert('dexFeeOutPctTokenA' not in result) + assert('dexFeePctTokenB' not in result) + assert('dexFeeInPctTokenB' not in result) + assert('dexFeeOutPctTokenB' not in result) assert_equal(result['rewardPct'], Decimal('0.00000000')) assert_equal(result['rewardLoanPct'], Decimal('0.00000000')) @@ -420,9 +422,11 @@ def check_pool_split(self, pool_id, pool_symbol, token_id, token_symbol, token_s assert_equal(result['status'], True) assert_equal(result['tradeEnabled'], True) assert_equal(result['dexFeePctTokenA'], Decimal('0.01000000')) - assert_equal(result['dexFeePctTokenB'], Decimal('0.03000000')) assert_equal(result['dexFeeInPctTokenA'], Decimal('0.01000000')) assert_equal(result['dexFeeOutPctTokenA'], Decimal('0.01000000')) + assert_equal(result['dexFeePctTokenB'], Decimal('0.03000000')) + assert_equal(result['dexFeeInPctTokenB'], Decimal('0.03000000')) + assert_equal(result['dexFeeOutPctTokenB'], Decimal('0.03000000')) assert_equal(result['rewardPct'], Decimal('1.00000000')) assert_equal(result['rewardLoanPct'], Decimal('1.00000000')) assert_equal(result['creationTx'], self.nodes[0].getblock(self.nodes[0].getbestblockhash())['tx'][2]) @@ -574,9 +578,11 @@ def pool_split(self): assert_equal(result['status'], True) assert_equal(result['tradeEnabled'], True) assert_equal(result['dexFeePctTokenA'], Decimal('0.01000000')) - assert_equal(result['dexFeePctTokenB'], Decimal('0.03000000')) assert_equal(result['dexFeeInPctTokenA'], Decimal('0.01000000')) assert_equal(result['dexFeeOutPctTokenA'], Decimal('0.01000000')) + assert_equal(result['dexFeePctTokenB'], Decimal('0.03000000')) + assert_equal(result['dexFeeInPctTokenB'], Decimal('0.03000000')) + assert_equal(result['dexFeeOutPctTokenB'], Decimal('0.03000000')) assert_equal(result['rewardPct'], Decimal('1.00000000')) assert_equal(result['rewardLoanPct'], Decimal('1.00000000')) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 419d01bd7d2..a1332097ceb 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -122,6 +122,7 @@ 'feature_lock_unspends.py', 'feature_bitcoin_wallet.py', 'feature_bitcoin_htlc.py', + 'feature_asymmetric_fee.py', 'feature_token_split.py', 'feature_token_split_mechanism.py', 'feature_token_split_usd_value.py',