Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Asymmetric pool swap fee
Browse files Browse the repository at this point in the history
Bushstar committed Jun 21, 2022

Verified

This commit was signed with the committer’s verified signature.
Bushstar Peter John Bushnell
1 parent 2dda0f7 commit b30a7e7
Showing 11 changed files with 480 additions and 44 deletions.
63 changes: 45 additions & 18 deletions src/masternodes/govvariables/attributes.cpp
Original file line number Diff line number Diff line change
@@ -132,8 +132,10 @@ const std::map<uint8_t, std::map<std::string, uint8_t>>& 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<uint8_t, std::map<uint8_t, std::string>>& 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<CAttributeValue> VerifyCurrencyPair(const std::string& str) {
return {CTokenCurrencyPair{token, currency}, Res::Ok()};
}

static std::set<std::string> dirSet{"in", "out", "both"};

static ResVal<CAttributeValue> 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::map<uint8_t, std::map<uint8_t,
{
AttributeTypes::Poolpairs, {
{PoolKeys::TokenAFeePCT, VerifyPct},
{PoolKeys::TokenAFeeDir, VerifyFeeDirection},
{PoolKeys::TokenBFeePCT, VerifyPct},
{PoolKeys::TokenBFeeDir, VerifyFeeDirection},
}
},
{
@@ -712,6 +728,8 @@ UniValue ATTRIBUTES::ExportFiltered(GovVarsFilter filter, const std::string &pre
ret.pushKV(key, KeyBuilder(ascendantPair->first, ascendantPair->second));
} else if (const auto currencyPair = std::get_if<CTokenCurrencyPair>(&attribute.second)) {
ret.pushKV(key, currencyPair->first + '/' + currencyPair->second);
} else if (const auto str = std::get_if<std::string>(&attribute.second)) {
ret.pushKV(key, *str);
}
} catch (const std::out_of_range&) {
// Should not get here, that's mean maps are mismatched
@@ -839,16 +857,22 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const
break;

case AttributeTypes::Poolpairs:
if (!std::get_if<CAmount>(&attribute.second)) {
return Res::Err("Unsupported value");
}
switch (attrV0->key) {
case PoolKeys::TokenAFeePCT:
case PoolKeys::TokenBFeePCT:
if (!view.GetPoolPair({attrV0->typeId})) {
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<CAmount>(&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<CAmount>(&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
4 changes: 3 additions & 1 deletion src/masternodes/govvariables/attributes.h
Original file line number Diff line number Diff line change
@@ -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<uint32_t, int32_t>;
using DescendantValue = std::pair<uint32_t, int32_t>;
using AscendantValue = std::pair<uint32_t, std::string>;
using CAttributeType = std::variant<CDataStructureV0, CDataStructureV1>;
using CAttributeValue = std::variant<bool, CAmount, CBalances, CTokenPayback, CTokenCurrencyPair, OracleSplits, DescendantValue, AscendantValue>;
using CAttributeValue = std::variant<bool, CAmount, CBalances, CTokenPayback, CTokenCurrencyPair, OracleSplits, DescendantValue, AscendantValue, std::string>;

enum GovVarsFilter {
All,
13 changes: 11 additions & 2 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
@@ -4298,15 +4298,24 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView& view, std::vector<DCT_ID> 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;
}
4 changes: 2 additions & 2 deletions src/masternodes/poolpairs.cpp
Original file line number Diff line number Diff line change
@@ -387,7 +387,7 @@ Res CPoolPair::RemoveLiquidity(CAmount liqAmount, std::function<Res(CAmount, CAm
return onReclaim(resAmountA, resAmountB);
}

Res CPoolPair::Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, std::function<Res (const CTokenAmount &, const CTokenAmount &)> onTransfer, int height) {
Res CPoolPair::Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, const std::pair<std::string, std::string>& asymmetricFee, std::function<Res (const CTokenAmount &, const CTokenAmount &)> 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%%");
}
18 changes: 17 additions & 1 deletion src/masternodes/poolpairs.h
Original file line number Diff line number Diff line change
@@ -127,7 +127,7 @@ class CPoolPair : public CPoolPairMessage
Res AddLiquidity(CAmount amountA, CAmount amountB, std::function<Res(CAmount)> onMint, bool slippageProtection = false);
Res RemoveLiquidity(CAmount liqAmount, std::function<Res(CAmount, CAmount)> onReclaim);

Res Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, std::function<Res(CTokenAmount const &, CTokenAmount const &)> onTransfer, int height = INT_MAX);
Res Swap(CTokenAmount in, CAmount dexfeeInPct, PoolPrice const & maxPrice, const std::pair<std::string, std::string>& asymmetricFee, std::function<Res(CTokenAmount const &, CTokenAmount const &)> 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<std::string, std::string>& 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<std::string, std::string>& 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
45 changes: 37 additions & 8 deletions src/masternodes/rpc_poolpair.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <masternodes/mn_rpc.h>

UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token, bool verbose) {
#include <masternodes/govvariables/attributes.h>

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;
15 changes: 8 additions & 7 deletions src/test/liquidity_tests.cpp
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

#include <boost/test/unit_test.hpp>

std::pair<std::string, std::string> 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<CAmount>::max(), 0}, FAIL_onSwap);
res = pool.Swap(CTokenAmount{pool.idTokenA, 1}, 0, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, FAIL_onSwap);
res = pool.Swap(CTokenAmount{pool.idTokenB, 2}, 0, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{
res = pool.Swap(CTokenAmount{pool.idTokenA, 1000000}, dexfeeInPct, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{
res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{
res = pool.Swap(CTokenAmount{pool.idTokenA, 2*COIN}, dexfeeInPct, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{
res = pool.Swap(CTokenAmount{pool.idTokenA, COIN}, 0, PoolPrice{std::numeric_limits<CAmount>::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<CAmount>::max(), 0}, [&] (CTokenAmount const &df, CTokenAmount const &ta) -> Res{
res = pool.Swap(CTokenAmount{pool.idTokenA, COIN/1000}, 0, PoolPrice{std::numeric_limits<CAmount>::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();
299 changes: 299 additions & 0 deletions test/functional/feature_asymmetric_fee.py
Original file line number Diff line number Diff line change
@@ -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()
50 changes: 48 additions & 2 deletions test/functional/feature_setgov.py
Original file line number Diff line number Diff line change
@@ -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 ()
12 changes: 9 additions & 3 deletions test/functional/feature_token_split.py
Original file line number Diff line number Diff line change
@@ -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'))

1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
@@ -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',

0 comments on commit b30a7e7

Please sign in to comment.