Skip to content

Commit

Permalink
Asymmetric pool swap
Browse files Browse the repository at this point in the history
  • Loading branch information
Bushstar committed Jun 21, 2022
1 parent 2dda0f7 commit fa5eb2f
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 45 deletions.
63 changes: 45 additions & 18 deletions src/masternodes/govvariables/attributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
},
{
Expand Down Expand Up @@ -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"},
}
},
{
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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},
}
},
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/masternodes/govvariables/attributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ enum TokenKeys : uint8_t {
enum PoolKeys : uint8_t {
TokenAFeePCT = 'a',
TokenBFeePCT = 'b',
TokenAFeeDir = 'c',
TokenBFeeDir = 'd',
};

struct CDataStructureV0 {
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions src/masternodes/mn_checks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/masternodes/poolpairs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() + ")");

Expand Down Expand Up @@ -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%%");
}
Expand Down
18 changes: 17 additions & 1 deletion src/masternodes/poolpairs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
47 changes: 35 additions & 12 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);
Expand All @@ -9,20 +11,32 @@ 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") {
poolObj.pushKV("dexFeeInPctTokenA", ValueFromAmount(dexFee));
} else if (dirA == "out") {
poolObj.pushKV("dexFeeOutPctTokenA", ValueFromAmount(dexFee));
}
}

if (const auto dexFee = pcustomcsview->GetDexFeeOutPct(id, pool.idTokenB)) {
poolObj.pushKV("dexFeePctTokenB", ValueFromAmount(dexFee));
poolObj.pushKV("dexFeeOutPctTokenB", ValueFromAmount(dexFee));
}
if (const auto dexFee = pcustomcsview->GetDexFeeInPct(id, pool.idTokenB)) {
poolObj.pushKV("dexFeeInPctTokenB", ValueFromAmount(dexFee));
}
if (const auto dexFee = pcustomcsview->GetDexFeeOutPct(id, pool.idTokenA)) {
poolObj.pushKV("dexFeeOutPctTokenA", ValueFromAmount(dexFee));
if (dirB == "out") {
poolObj.pushKV("dexFeeOutPctTokenB", ValueFromAmount(dexFee));
} else if (dirB == "in") {
poolObj.pushKV("dexFeeInPctTokenB", ValueFromAmount(dexFee));
}
}

poolObj.pushKV("reserveA", ValueFromAmount(pool.reserveA));
poolObj.pushKV("reserveB", ValueFromAmount(pool.reserveB));
poolObj.pushKV("commission", ValueFromAmount(pool.commission));
Expand Down Expand Up @@ -218,7 +232,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--;
}

Expand Down Expand Up @@ -260,7 +274,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");
Expand Down Expand Up @@ -1083,7 +1097,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;
Expand Down
15 changes: 8 additions & 7 deletions src/test/liquidity_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <boost/test/unit_test.hpp>

std::pair<std::string, std::string> asymmetricFee{"both", "both"};

inline uint256 NextTx()
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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();
Expand All @@ -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();
Expand Down
Loading

0 comments on commit fa5eb2f

Please sign in to comment.