From c86289c10948d3ac88ae7ff335c41b3bb5418eda Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Fri, 8 Jan 2021 18:08:44 +0000 Subject: [PATCH 1/4] Add custom rewards to pool pairs --- src/chainparams.cpp | 15 ++ src/consensus/params.h | 2 + src/init.cpp | 1 + src/masternodes/mn_checks.cpp | 79 +++++++ src/masternodes/mn_rpc.cpp | 59 ++++- src/masternodes/poolpairs.cpp | 31 +++ src/masternodes/poolpairs.h | 51 +++- src/rpc/blockchain.cpp | 1 + src/test/liquidity_tests.cpp | 2 +- src/validation.cpp | 10 +- test/functional/feature_custom_poolreward.py | 236 +++++++++++++++++++ test/functional/rpc_blockchain.py | 1 + test/functional/test_runner.py | 1 + 13 files changed, 474 insertions(+), 15 deletions(-) create mode 100644 test/functional/feature_custom_poolreward.py diff --git a/src/chainparams.cpp b/src/chainparams.cpp index ed2a4c1571a..c6f5c26294e 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -118,6 +118,7 @@ class CMainParams : public CChainParams { consensus.BayfrontHeight = 405000; consensus.BayfrontMarinaHeight = 465150; consensus.BayfrontGardensHeight = 488300; + consensus.ClarkeQuayHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -279,6 +280,7 @@ class CTestNetParams : public CChainParams { consensus.BayfrontHeight = 3000; consensus.BayfrontMarinaHeight = 90470; consensus.BayfrontGardensHeight = 101342; + consensus.ClarkeQuayHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -417,6 +419,7 @@ class CDevNetParams : public CChainParams { consensus.BayfrontHeight = 250; consensus.BayfrontMarinaHeight = 0; consensus.BayfrontGardensHeight = 0; + consensus.ClarkeQuayHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 5 * 60; // 5 min == 10 blocks @@ -550,6 +553,7 @@ class CRegTestParams : public CChainParams { consensus.BayfrontHeight = 10000000; consensus.BayfrontMarinaHeight = 10000000; consensus.BayfrontGardensHeight = 10000000; + consensus.ClarkeQuayHeight = 10000000; consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -726,6 +730,17 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) consensus.BayfrontGardensHeight = static_cast(height); } + if (gArgs.IsArgSet("-clarkequayheight")) { + int64_t height = gArgs.GetArg("-clarkequayheight", consensus.ClarkeQuayHeight); + if (height < -1 || height >= std::numeric_limits::max()) { + throw std::runtime_error(strprintf("Activation height %ld for ClarkeQuay is out of valid range. Use -1 to disable clarkequay features.", height)); + } else if (height == -1) { + LogPrintf("CQ disabled for testing\n"); + height = std::numeric_limits::max(); + } + consensus.ClarkeQuayHeight = static_cast(height); + } + if (!args.IsArgSet("-vbparams")) return; for (const std::string& strDeployment : args.GetArgs("-vbparams")) { diff --git a/src/consensus/params.h b/src/consensus/params.h index 1155371cc2f..bbdee88da9e 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -76,6 +76,8 @@ struct Params { int BayfrontHeight; int BayfrontMarinaHeight; int BayfrontGardensHeight; + /** Third major fork. */ + int ClarkeQuayHeight; /** Foundation share after AMK, normalized to COIN = 100% */ CAmount foundationShareDFIP1; diff --git a/src/init.cpp b/src/init.cpp index 9d10e8c0f32..62f89cc0a61 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -468,6 +468,7 @@ void SetupServerArgs() gArgs.AddArg("-amkheight", "AMK fork activation height (regtest only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-bayfrontheight", "Bayfront fork activation height (regtest only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-bayfrontgardensheight", "Bayfront Gardens fork activation height (regtest only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-clarkequayheight", "ClarkeQuay fork activation height (regtest only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); #ifdef USE_UPNP #if USE_UPNP gArgs.AddArg("-upnp", "Use UPnP to map the listening port (default: 1 when listening and no -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 8197da9f0d6..9bf10eb1906 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1018,9 +1018,16 @@ Res ApplyCreatePoolPairTx(CCustomCSView &mnview, const CCoinsViewCache &coins, c CPoolPairMessage poolPairMsg; std::string pairSymbol; + CBalances rewards; CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); ss >> poolPairMsg; ss >> pairSymbol; + + // Read custom pool rewards + if (static_cast(height) >= consensusParams.ClarkeQuayHeight && !ss.empty()) { + ss >> rewards; + } + if (!ss.empty()) { return Res::Err("%s: deserialization failed: excess %d bytes", base, ss.size()); } @@ -1079,6 +1086,20 @@ Res ApplyCreatePoolPairTx(CCustomCSView &mnview, const CCoinsViewCache &coins, c rpcInfo->pushKV("tradeable", token.IsTradeable()); rpcInfo->pushKV("finalized", token.IsFinalized()); + if (!rewards.balances.empty()) { + UniValue rewardArr(UniValue::VARR); + + for (const auto& reward : rewards.balances) { + if (reward.second > 0) { + rewardArr.push_back(CTokenAmount{reward.first, reward.second}.ToString()); + } + } + + if (!rewardArr.empty()) { + rpcInfo->pushKV("customRewards", rewardArr); + } + } + return Res::Ok(base); } @@ -1098,6 +1119,24 @@ Res ApplyCreatePoolPairTx(CCustomCSView &mnview, const CCoinsViewCache &coins, c return Res::Err("%s %s: %s", base, pairSymbol, resPP.msg); } + if (!rewards.balances.empty()) { + // Remove empty reward amounts + for (auto it = rewards.balances.cbegin(), next_it = it; it != rewards.balances.cend(); it = next_it) { + ++next_it; + + if (it->second == 0) { + rewards.balances.erase(it); + } + } + + auto resCR = mnview.SetPoolCustomReward(pairToken->first, rewards); + + // Will only fail if pool was not actually created in SetPoolPair + if (!resCR.ok) { + return Res::Err("%s %s: %s", base, pairSymbol, resCR.msg); + } + } + return Res::Ok(base); } @@ -1113,11 +1152,18 @@ Res ApplyUpdatePoolPairTx(CCustomCSView & mnview, CCoinsViewCache const & coins, bool status; CAmount commission; CScript ownerAddress; + CBalances rewards; CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); ss >> poolId; ss >> status; ss >> commission; ss >> ownerAddress; + + // Read custom pool rewards + if (static_cast(height) >= consensusParams.ClarkeQuayHeight && !ss.empty()) { + ss >> rewards; + } + if (!ss.empty()) { return Res::Err("Pool Update: deserialization failed: excess %d bytes", ss.size()); } @@ -1141,6 +1187,39 @@ Res ApplyUpdatePoolPairTx(CCustomCSView & mnview, CCoinsViewCache const & coins, rpcInfo->pushKV("commission", ValueFromAmount(commission)); rpcInfo->pushKV("status", status); rpcInfo->pushKV("ownerAddress", ownerAddress.GetHex()); + + // Add rewards here before processing them below to avoid adding current rewards + if (!rewards.balances.empty()) { + UniValue rewardArr(UniValue::VARR); + + for (const auto& reward : rewards.balances) { + if (reward.second > 0) { + rewardArr.push_back(CTokenAmount{reward.first, reward.second}.ToString()); + } + } + + if (!rewardArr.empty()) { + rpcInfo->pushKV("customRewards", rewardArr); + } + } + } + + if (!rewards.balances.empty()) { + // Remove empty reward amounts + for (auto it = rewards.balances.cbegin(), next_it = it; it != rewards.balances.cend(); it = next_it) { + ++next_it; + + if (it->second == 0) { + rewards.balances.erase(it); + } + } + + auto resCR = mnview.SetPoolCustomReward(poolId, rewards); + + // Will only fail if pool was not actually created in SetPoolPair + if (!resCR.ok) { + return Res::Err("%s %s: %s", base, poolId.ToString(), resCR.msg); + } } return Res::Ok(base); diff --git a/src/masternodes/mn_rpc.cpp b/src/masternodes/mn_rpc.cpp index e6d1b0121b8..f7d65055889 100644 --- a/src/masternodes/mn_rpc.cpp +++ b/src/masternodes/mn_rpc.cpp @@ -1815,6 +1815,31 @@ UniValue poolToJSON(DCT_ID const& id, CPoolPair const& pool, CToken const& token poolObj.pushKV("rewardPct", ValueFromAmount(pool.rewardPct)); + auto rewards = pcustomcsview->GetPoolCustomReward(id); + if (rewards && !rewards->balances.empty()) { + for (auto it = rewards->balances.cbegin(), next_it = it; it != rewards->balances.cend(); it = next_it) { + ++next_it; + + // Get token balance + const auto balance = pcustomcsview->GetBalance(pool.ownerAddress, it->first).nValue; + + // Make there's enough to pay reward otherwise remove it + if (balance < it->second) { + rewards->balances.erase(it); + } + } + + if (!rewards->balances.empty()) { + UniValue rewardArr(UniValue::VARR); + + for (const auto& reward : rewards->balances) { + rewardArr.push_back(CTokenAmount{reward.first, reward.second}.ToString()); + } + + poolObj.pushKV("customRewards", rewardArr); + } + } + poolObj.pushKV("creationTx", pool.creationTx.GetHex()); poolObj.pushKV("creationHeight", (uint64_t) pool.creationHeight); } @@ -2500,6 +2525,8 @@ UniValue createpoolpair(const JSONRPCRequest& request) { "Pool Status: True is Active, False is Restricted"}, {"ownerAddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Address of the pool owner."}, + {"customReward", RPCArg::Type::STR, RPCArg::Optional::OMITTED, + "Token reward to be paid on each block, multiple can be specified."}, {"pairSymbol", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Pair symbol (unique), no longer than " + std::to_string(CToken::MAX_TOKEN_SYMBOL_LENGTH)}, @@ -2524,14 +2551,16 @@ UniValue createpoolpair(const JSONRPCRequest& request) { "\"tokenB\":\"MyToken2\"," "\"commission\":\"0.001\"," "\"status\":\"True\"," - "\"ownerAddress\":\"Address\"" + "\"ownerAddress\":\"Address\"," + "\"customReward\":\"[\\\"1@tokena\\\",\\\"10@tokenb\\\"]\"" "}' '[{\"txid\":\"id\",\"vout\":0}]'") + HelpExampleRpc("createpoolpair", "'{\"tokenA\":\"MyToken1\"," "\"tokenB\":\"MyToken2\"," "\"commission\":\"0.001\"," "\"status\":\"True\"," - "\"ownerAddress\":\"Address\"" - "}' '[{\"txid\":\"id\",\"vout\":0}]'") + "\"ownerAddress\":\"Address\"," + "\"customReward\":\"[\\\"1@tokena\\\",\\\"10@tokenb\\\"]\"" + "}' '[{\"txid\":\"id\",\"vout\":0}]'") }, }.Check(request); @@ -2546,6 +2575,7 @@ UniValue createpoolpair(const JSONRPCRequest& request) { std::string tokenA, tokenB, pairSymbol; CAmount commission = 0; // !!! CScript ownerAddress; + CBalances rewards; bool status = true; // default Active UniValue metadataObj = request.params[0].get_obj(); if (!metadataObj["tokenA"].isNull()) { @@ -2566,6 +2596,9 @@ UniValue createpoolpair(const JSONRPCRequest& request) { if (!metadataObj["pairSymbol"].isNull()) { pairSymbol = metadataObj["pairSymbol"].getValStr(); } + if (!metadataObj["customReward"].isNull()) { + rewards = DecodeAmounts(pwallet->chain(), metadataObj["customReward"], ""); + } int targetHeight; DCT_ID idtokenA, idtokenB; @@ -2594,6 +2627,10 @@ UniValue createpoolpair(const JSONRPCRequest& request) { metadata << static_cast(CustomTxType::CreatePoolPair) << poolPairMsg << pairSymbol; + if (targetHeight >= Params().GetConsensus().ClarkeQuayHeight) { + metadata << rewards; + } + CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); @@ -2648,7 +2685,7 @@ UniValue updatepoolpair(const JSONRPCRequest& request) { {"status", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Pool Status new property (bool)"}, {"commission", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Pool commission, up to 10^-8"}, {"ownerAddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Address of the pool owner."}, - + {"customReward", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Token reward to be paid on each block, multiple can be specified."}, }, }, {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", @@ -2668,10 +2705,12 @@ UniValue updatepoolpair(const JSONRPCRequest& request) { }, RPCExamples{ HelpExampleCli("updatepoolpair", "'{\"pool\":\"POOL\",\"status\":true," - "\"commission\":0.01,\"ownerAddress\":\"Address\"}' " + "\"commission\":0.01,\"ownerAddress\":\"Address\"," + "\"customReward\":\"[\\\"1@tokena\\\",\\\"10@tokenb\\\"]\"}' " "'[{\"txid\":\"id\",\"vout\":0}]'") + HelpExampleRpc("updatepoolpair", "'{\"pool\":\"POOL\",\"status\":true," - "\"commission\":0.01,\"ownerAddress\":\"Address\"}' " + "\"commission\":0.01,\"ownerAddress\":\"Address\"," + "\"customReward\":\"[\\\"1@tokena\\\",\\\"10@tokenb\\\"]\"}' " "'[{\"txid\":\"id\",\"vout\":0}]'") }, }.Check(request); @@ -2688,6 +2727,7 @@ UniValue updatepoolpair(const JSONRPCRequest& request) { bool status = true; CAmount commission = -1; CScript ownerAddress; + CBalances rewards; UniValue const & metaObj = request.params[0].get_obj(); UniValue const & txInputs = request.params[1]; @@ -2718,6 +2758,9 @@ UniValue updatepoolpair(const JSONRPCRequest& request) { if (!metaObj["ownerAddress"].isNull()) { ownerAddress = DecodeScript(metaObj["ownerAddress"].getValStr()); } + if (!metaObj["customReward"].isNull()) { + rewards = DecodeAmounts(pwallet->chain(), metaObj["customReward"], ""); + } const auto txVersion = GetTransactionVersion(targetHeight); CMutableTransaction rawTx(txVersion); @@ -2730,6 +2773,10 @@ UniValue updatepoolpair(const JSONRPCRequest& request) { metadata << static_cast(CustomTxType::UpdatePoolPair) << poolId << status << commission << ownerAddress; + if (targetHeight >= Params().GetConsensus().ClarkeQuayHeight) { + metadata << rewards; + } + CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); diff --git a/src/masternodes/poolpairs.cpp b/src/masternodes/poolpairs.cpp index 2f20a46d0cd..e3c39d841d2 100644 --- a/src/masternodes/poolpairs.cpp +++ b/src/masternodes/poolpairs.cpp @@ -10,6 +10,7 @@ const unsigned char CPoolPairView::ByID ::prefix = 'i'; const unsigned char CPoolPairView::ByPair ::prefix = 'j'; const unsigned char CPoolPairView::ByShare ::prefix = 'k'; +const unsigned char CPoolPairView::Reward ::prefix = 'I'; Res CPoolPairView::SetPoolPair(DCT_ID const & poolId, CPoolPair const & pool) { @@ -98,6 +99,36 @@ boost::optional > CPoolPairView::GetPoolPair(const return {}; } +Res CPoolPairView::SetPoolCustomReward(const DCT_ID &poolId, CBalances& rewards) +{ + DCT_ID poolID = poolId; + if (!GetPoolPair(poolID)) + { + return Res::Err("Error %s: poolID %s does not exist", __func__, poolID.ToString()); + } + + // Get existing rewards, can be called from update pool pair. + auto currentRewards = ReadBy(WrapVarInt(poolID.v)); + if (currentRewards) { + for (const auto& item : currentRewards->balances) { + + // If current token is not included in update, add it to rewards so it remains unchanged + if (rewards.balances.find(item.first) == rewards.balances.end()) { + rewards.Add(CTokenAmount{item.first, item.second}); + } + } + } + + WriteBy(WrapVarInt(poolID.v), rewards); + return Res::Ok(); +} + +boost::optional CPoolPairView::GetPoolCustomReward(const DCT_ID &poolId) +{ + DCT_ID poolID = poolId; + return ReadBy(WrapVarInt(poolID.v)); +} + Res CPoolPair::Swap(CTokenAmount in, PoolPrice const & maxPrice, std::function onTransfer, bool postBayfrontGardens) { if (in.nTokenId != idTokenA && in.nTokenId != idTokenB) { throw std::runtime_error("Error, input token ID (" + in.nTokenId.ToString() + ") doesn't match pool tokens (" + idTokenA.ToString() + "," + idTokenB.ToString() + ")"); diff --git a/src/masternodes/poolpairs.h b/src/masternodes/poolpairs.h index 3a94902a017..f7bb8035744 100644 --- a/src/masternodes/poolpairs.h +++ b/src/masternodes/poolpairs.h @@ -9,6 +9,7 @@ #include #include +#include #include #include