From 2bf708d6ead51b4dc45975234538c27e6e6a4702 Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Wed, 19 Jan 2022 05:48:38 +0000 Subject: [PATCH 1/4] Smart contract exchange of BTC for DFI --- src/chainparams.cpp | 12 + src/consensus/params.h | 1 + src/masternodes/balances.h | 13 + src/masternodes/govvariables/attributes.cpp | 68 ++++- src/masternodes/govvariables/attributes.h | 35 +++ src/masternodes/masternodes.h | 3 +- src/masternodes/mn_checks.cpp | 141 +++++++++++ src/masternodes/mn_checks.h | 3 + src/masternodes/mn_rpc.cpp | 51 ++++ src/masternodes/rpc_accounts.cpp | 129 ++++++++++ src/masternodes/rpc_customtx.cpp | 5 + src/masternodes/rpc_oracles.cpp | 7 +- src/rpc/util.cpp | 11 + src/rpc/util.h | 1 + test/functional/feature_setgov.py | 16 +- test/functional/feature_smart_contracts.py | 259 ++++++++++++++++++++ test/functional/test_runner.py | 1 + 17 files changed, 737 insertions(+), 19 deletions(-) create mode 100644 test/functional/feature_smart_contracts.py diff --git a/src/chainparams.cpp b/src/chainparams.cpp index dae3fc6495..90851cf67a 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -243,6 +243,9 @@ class CMainParams : public CChainParams { consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("dJEbxbfufyPF14SC93yxiquECEfq4YSd9L", *this))); consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("8UAhRuUFCyFUHEPD7qvtj8Zy2HxF5HH5nb", *this))); + consensus.smartContracts.clear(); + consensus.smartContracts["DFIP2201"] = GetScriptForDestination(CTxDestination(WitnessV0KeyHash(std::vector{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}))); + // owner base58, operator base58 vMasternodes.push_back({"8PuErAcazqccCVzRcc8vJ3wFaZGm4vFbLe", "8J846CKFF83Jcj5m4EReJmxiaJ6Jy1Y6Ea"}); vMasternodes.push_back({"8RPZm7SVUNhGN1RgGY3R92rvRkZBwETrCX", "8bzHwhaF2MaVs4owRvpWtZQVug3mKuJji2"}); @@ -456,6 +459,9 @@ class CTestNetParams : public CChainParams { consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("trnZD2qPU1c3WryBi8sWX16mEaq9WkGHeg", *this))); // cVUZfDj1B1o7eVhxuZr8FQLh626KceiGQhZ8G6YCUdeW3CAV49ti consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("75jrurn8tkDLhZ3YPyzhk6D9kc1a4hBrmM", *this))); // cSmsVpoR6dSW5hPNKeGwC561gXHXcksdQb2yAFQdjbSp5MUyzZqr + consensus.smartContracts.clear(); + consensus.smartContracts["DFIP2201"] = GetScriptForDestination(CTxDestination(WitnessV0KeyHash(std::vector{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}))); + // owner base58, operator base58 vMasternodes.push_back({"7LMorkhKTDjbES6DfRxX2RiNMbeemUkxmp", "7KEu9JMKCx6aJ9wyg138W3p42rjg19DR5D"}); vMasternodes.push_back({"7E8Cjn9cqEwnrc3E4zN6c5xKxDSGAyiVUM", "78MWNEcAAJxihddCw1UnZD8T7fMWmUuBro"}); @@ -634,6 +640,9 @@ class CDevNetParams : public CChainParams { consensus.foundationMembers.emplace(GetScriptForDestination(DecodeDestination("7M3g9CSERjLdXisE5pv2qryDbURUj9Vpi1", *this))); consensus.foundationMembers.emplace(GetScriptForDestination(DecodeDestination("7L29itepC13pgho1X2y7mcuf4WjkBi7x2w", *this))); + consensus.smartContracts.clear(); + consensus.smartContracts["DFIP2201"] = GetScriptForDestination(CTxDestination(WitnessV0KeyHash(std::vector{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}))); + // owner base58, operator base58 vMasternodes.push_back({"7M3g9CSERjLdXisE5pv2qryDbURUj9Vpi1", "7Grgx69MZJ4wDKRx1bBxLqTnU9T3quKW7n"}); vMasternodes.push_back({"7L29itepC13pgho1X2y7mcuf4WjkBi7x2w", "773MiaEtQK2HAwWj55gyuRiU8tSwowRTTW"}); @@ -818,6 +827,9 @@ class CRegTestParams : public CChainParams { consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("2MxJf6Ak8MGrLoGdekrU6AusW29szZUFphH", *this))); consensus.accountDestruction.insert(GetScriptForDestination(DecodeDestination("mxiaFfAnCoXEUy4RW8NgsQM7yU5YRCiFSh", *this))); + consensus.smartContracts.clear(); + consensus.smartContracts["DFIP2201"] = GetScriptForDestination(CTxDestination(WitnessV0KeyHash(std::vector{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}))); + // owner base58, operator base58 vMasternodes.push_back({"mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU", "mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy"}); vMasternodes.push_back({"msER9bmJjyEemRpQoS8YYVL21VyZZrSgQ7", "mps7BdmwEF2vQ9DREDyNPibqsuSRZ8LuwQ"}); diff --git a/src/consensus/params.h b/src/consensus/params.h index 5dfd6cffa6..be296123fc 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -59,6 +59,7 @@ struct Params { uint32_t foundationShare; std::set foundationMembers; std::set accountDestruction; + std::map smartContracts; /* Block hash that is excepted from BIP16 enforcement */ uint256 BIP16Exception; /** Block height and hash at which BIP34 becomes active */ diff --git a/src/masternodes/balances.h b/src/masternodes/balances.h index f819b6deeb..64b3539d45 100644 --- a/src/masternodes/balances.h +++ b/src/masternodes/balances.h @@ -211,6 +211,19 @@ struct CUtxosToAccountMessage { } }; +struct CSmartContractMessage { + std::string name; + CAccounts accounts; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(name); + READWRITE(accounts); + } +}; + inline CBalances SumAllTransfers(CAccounts const & to) { CBalances sum; for (const auto& kv : to) { diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index f2539b96b8..0877ce7f5d 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -39,15 +39,23 @@ static ResVal VerifyInt32(const std::string& str) { return {int32, Res::Ok()}; } -static ResVal VerifyPct(const std::string& str) { +static ResVal VerifyFloat(const std::string& str) { CAmount amount = 0; if (!ParseFixedPoint(str, 8, &amount) || amount < 0) { - return Res::Err("Percentage must be a positive integer or float"); + return Res::Err("Amount must be a float"); + } + return {amount, Res::Ok()}; +} + +static ResVal VerifyPct(const std::string& str) { + auto resVal = VerifyFloat(str); + if (!resVal) { + return resVal; } - if (amount > COIN) { + if (*resVal.val > COIN) { return Res::Err("Percentage exceeds 100%%"); } - return ResVal(amount, Res::Ok()); + return resVal; } Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value, @@ -91,9 +99,21 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value auto type = itype->second; - auto typeId = VerifyInt32(keys[2]); - if (!typeId) { - return std::move(typeId); + uint32_t typeId{0}; + if (type != AttributeTypes::Param) { + auto id = VerifyInt32(keys[2]); + if (!id) { + return std::move(id); + } + + typeId = *id.val; + } else { + auto id = allowedParamIDs.find(keys[2]); + if (id == allowedParamIDs.end()) { + return Res::Err("Unsupported ID"); + } + + typeId = id->second; } auto ikey = allowedKeys.find(type); @@ -141,12 +161,35 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value } else { return Res::Err("Unrecognised key"); } + } else if (type == AttributeTypes::Param) { + if (typeId == ParamIDs::DFIP2201) { + if (typeKey == DFIP2201Keys::Active) { + if (value != "true" && value != "false") { + return Res::Err("DFIP2201 actve value must be either \"true\" or \"false\""); + } + valueV0 = value == "true"; + } else if (typeKey == DFIP2201Keys::Premium) { + auto res = VerifyPct(value); + if (!res) { + return std::move(res); + } + valueV0 = *res.val; + } else if (typeKey == DFIP2201Keys::MinSwap) { + auto res = VerifyFloat(value); + if (!res) { + return std::move(res); + } + valueV0 = *res.val; + } else { + return Res::Err("Unrecognised key"); + } + } } else { return Res::Err("Unrecognised type"); } if (applyVariable) { - return applyVariable(CDataStructureV0{type, uint32_t(*typeId.val), typeKey}, valueV0); + return applyVariable(CDataStructureV0{type, typeId, typeKey}, valueV0); } return Res::Ok(); } @@ -185,9 +228,10 @@ UniValue ATTRIBUTES::Export() const { continue; } try { + const std::string id = attrV0->type == AttributeTypes::Param ? displayParamsIDs.at(attrV0->typeId) : KeyBuilder(attrV0->typeId); auto key = KeyBuilder(displayVersions.at(VersionTypes::v0), displayTypes.at(attrV0->type), - attrV0->typeId, + id, displayKeys.at(attrV0->type).at(attrV0->key)); if (auto bool_val = boost::get(valV0)) { @@ -248,6 +292,12 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const continue; } } + if (attrV0->type == AttributeTypes::Param) { + if (attrV0->typeId == ParamIDs::DFIP2201) { + return Res::Ok(); + } + return Res::Err("Unrecognised param id"); + } return Res::Err("Unrecognised type"); } diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index c7d37a9e1e..fc746e8ee6 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -13,10 +13,21 @@ enum VersionTypes : uint8_t { }; enum AttributeTypes : uint8_t { + Param = 'a', Token = 't', Poolpairs = 'p', }; +enum ParamIDs : uint8_t { + DFIP2201 = 'a', +}; + +enum DFIP2201Keys : uint8_t { + Active = 'a', + Premium = 'b', + MinSwap = 'c', +}; + enum TokenKeys : uint8_t { PaybackDFI = 'a', PaybackDFIFeePCT = 'b', @@ -106,6 +117,11 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator allowedTypes{ {"token", AttributeTypes::Token}, {"poolpairs", AttributeTypes::Poolpairs}, + {"params", AttributeTypes::Param}, + }; + + const std::map allowedParamIDs{ + {"dfip2201", ParamIDs::DFIP2201} }; const std::map> allowedKeys{ @@ -121,6 +137,13 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator displayTypes{ {AttributeTypes::Token, "token"}, {AttributeTypes::Poolpairs, "poolpairs"}, + {AttributeTypes::Param, "params"}, + }; + + const std::map displayParamsIDs{ + {ParamIDs::DFIP2201, "dfip2201"} }; const std::map> displayKeys{ @@ -146,6 +174,13 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator GetValidatedIntervalPrice(CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice); public: // Increase version when underlaying tables are changed @@ -431,6 +430,8 @@ class CCustomCSView ResVal GetLoanCollaterals(CVaultId const & vaultId, CBalances const & collaterals, uint32_t height, int64_t blockTime, bool useNextPrice = false, bool requireLivePrice = true); + ResVal GetValidatedIntervalPrice(CTokenCurrencyPair priceFeedId, bool useNextPrice, bool requireLivePrice); + void SetDbVersion(int version); int GetDbVersion() const; diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index c01cb65bc9..70adcc4dbe 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include /// AmountFromValue #include #include #include @@ -47,6 +49,7 @@ std::string ToString(CustomTxType type) { case CustomTxType::AccountToUtxos: return "AccountToUtxos"; case CustomTxType::AccountToAccount: return "AccountToAccount"; case CustomTxType::AnyAccountsToAccounts: return "AnyAccountsToAccounts"; + case CustomTxType::SmartContract: return "SmartContract"; case CustomTxType::SetGovVariable: return "SetGovVariable"; case CustomTxType::SetGovVariableHeight:return "SetGovVariableHeight"; case CustomTxType::AppointOracle: return "AppointOracle"; @@ -127,6 +130,7 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { case CustomTxType::AccountToUtxos: return CAccountToUtxosMessage{}; case CustomTxType::AccountToAccount: return CAccountToAccountMessage{}; case CustomTxType::AnyAccountsToAccounts: return CAnyAccountsToAccountsMessage{}; + case CustomTxType::SmartContract: return CSmartContractMessage{}; case CustomTxType::SetGovVariable: return CGovernanceMessage{}; case CustomTxType::SetGovVariableHeight: return CGovernanceHeightMessage{}; case CustomTxType::AppointOracle: return CAppointOracleMessage{}; @@ -211,6 +215,13 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return Res::Ok(); } + Res isPostFortCanningHillFork() const { + if(static_cast(height) < consensus.FortCanningHillHeight) { + return Res::Err("called before FortCanningHill height"); + } + return Res::Ok(); + } + template Res serialize(T& obj) const { CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); @@ -323,6 +334,11 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return !res ? res : serialize(obj); } + Res operator()(CSmartContractMessage& obj) const { + auto res = isPostFortCanningHillFork(); + return !res ? res : serialize(obj); + } + Res operator()(CCreatePoolPairMessage& obj) const { auto res = isPostBayfrontFork(); if (!res) { @@ -1327,6 +1343,131 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return !res ? res : addBalancesSetShares(obj.to); } + Res operator()(const CSmartContractMessage& obj) const { + const auto pAttributes = mnview.GetAttributes(); + if (!pAttributes) { + return Res::Err("DFIP2201 smart contract is not enabled"); + } + + const auto& attrs = pAttributes->attributes; + CDataStructureV0 activeKey{AttributeTypes::Param, ParamIDs::DFIP2201, DFIP2201Keys::Active}; + try { + const auto& value = attrs.at(activeKey); + auto valueV0 = boost::get(&value); + + if (!valueV0) { + throw std::out_of_range(""); + } + + const auto active = boost::get(valueV0); + if (!active || !*active) { + throw std::out_of_range(""); + } + } catch (const std::out_of_range&) { + return Res::Err("DFIP2201 smart contract is not enabled"); + } + + if (obj.accounts.empty()) { + return Res::Err("No address and amount entries found"); + } + + if (obj.name == Params().GetConsensus().smartContracts.begin()->first) { + if (obj.accounts.size() != 1) { + return Res::Err("Only one address entry expected for " + obj.name); + } + + if (obj.accounts.begin()->second.balances.size() != 1) { + return Res::Err("Only one amount entry expected for " + obj.name); + } + + const auto& script = obj.accounts.begin()->first; + if (!HasAuth(script)) { + return Res::Err("Must have at least one input from supplied address"); + } + + const auto& id = obj.accounts.begin()->second.balances.begin()->first; + const auto& amount = obj.accounts.begin()->second.balances.begin()->second; + + if (amount <= 0) { + return Res::Err("Amount out of range"); + } + + CDataStructureV0 minSwapKey{AttributeTypes::Param, ParamIDs::DFIP2201, DFIP2201Keys::MinSwap}; + CAmount minSwap{0}; + try { + const auto& value = attrs.at(minSwapKey); + auto valueV0 = boost::get(&value); + if (valueV0) { + if (auto storedMinSwap = boost::get(valueV0)) { + minSwap = *storedMinSwap; + } + } + } catch (const std::out_of_range&) {} + + if (minSwap && amount < minSwap) { + return Res::Err("Below minimum swapable amount, must be at least " + ValueFromAmount(minSwap).getValStr() + " BTC"); + } + + const auto token = mnview.GetToken(id); + if (!token) { + return Res::Err("Specified token not found"); + } + + if (token->symbol != "BTC" || token->name != "Bitcoin" || !token->IsDAT()) { + return Res::Err("Only Bitcoin can be swapped in " + obj.name); + } + + auto res = mnview.SubBalance(script, {id, amount}); + if (!res) { + return res; + } + + const std::pair btcUsd{"BTC","USD"}; + const std::pair dfiUsd{"DFI","USD"}; + + bool useNextPrice{false}, requireLivePrice{true}; + auto resVal = mnview.GetValidatedIntervalPrice(btcUsd, useNextPrice, requireLivePrice); + if (!resVal) { + return std::move(resVal); + } + + CDataStructureV0 premiumKey{AttributeTypes::Param, ParamIDs::DFIP2201, DFIP2201Keys::Premium}; + CAmount premium{2500000}; + try { + const auto& value = attrs.at(premiumKey); + auto valueV0 = boost::get(&value); + if (valueV0) { + if (auto storedPremium = boost::get(valueV0)) { + premium = *storedPremium; + } + } + } catch (const std::out_of_range&) {} + + const auto& btcPrice = DivideAmounts(MultiplyAmounts(resVal.val.get(), (premium + COIN)), COIN); + + resVal = mnview.GetValidatedIntervalPrice(dfiUsd, useNextPrice, requireLivePrice); + if (!resVal) { + return std::move(resVal); + } + + const auto totalDFI = DivideAmounts(MultiplyAmounts(DivideAmounts(btcPrice, resVal.val.get()), amount), COIN); + + res = mnview.SubBalance(Params().GetConsensus().smartContracts.begin()->second, {{0}, totalDFI}); + if (!res) { + return res; + } + + res = mnview.AddBalance(script, {{0}, totalDFI}); + if (!res) { + return res; + } + + return Res::Ok(); + } + + return Res::Err("Specified smart contract not found"); + } + Res operator()(const CAnyAccountsToAccountsMessage& obj) const { // check auth for (const auto& kv : obj.from) { diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index a69cfcc470..b0af9d31e2 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -66,6 +66,7 @@ enum class CustomTxType : uint8_t AccountToUtxos = 'b', AccountToAccount = 'B', AnyAccountsToAccounts = 'a', + SmartContract = 'K', //set governance variable SetGovVariable = 'G', SetGovVariableHeight = 'j', @@ -123,6 +124,7 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::AccountToUtxos: case CustomTxType::AccountToAccount: case CustomTxType::AnyAccountsToAccounts: + case CustomTxType::SmartContract: case CustomTxType::SetGovVariable: case CustomTxType::SetGovVariableHeight: case CustomTxType::AutoAuthPrep: @@ -339,6 +341,7 @@ typedef boost::variant< CAccountToUtxosMessage, CAccountToAccountMessage, CAnyAccountsToAccountsMessage, + CSmartContractMessage, CGovernanceMessage, CGovernanceHeightMessage, CAppointOracleMessage, diff --git a/src/masternodes/mn_rpc.cpp b/src/masternodes/mn_rpc.cpp index 8ae57e98d8..1e2643c185 100644 --- a/src/masternodes/mn_rpc.cpp +++ b/src/masternodes/mn_rpc.cpp @@ -705,6 +705,7 @@ UniValue listgovs(const JSONRPCRequest& request) { return result; } + UniValue isappliedcustomtx(const JSONRPCRequest& request) { RPCHelpMan{"isappliedcustomtx", "\nChecks that custom transaction was affected on chain\n", @@ -763,6 +764,55 @@ UniValue isappliedcustomtx(const JSONRPCRequest& request) { return result; } + +static std::string GetContractCall(const std::string& str) { + if (str == "DFIP2201") { + return "dbtcdfiswap"; + } + + return str; +} + +UniValue listsmartcontracts(const JSONRPCRequest& request) { + RPCHelpMan{"listsmartcontracts", + "\nReturns information on smart contracts\n", + { + }, + RPCResult{ + "(array) JSON array with smart contract information\n" + "\"name\":\"name\" smart contract name\n" + "\"address\":\"address\" smart contract address\n" + "\"token id\":x.xxxxxxxx smart contract balance per token\n" + }, + RPCExamples{ + HelpExampleCli("listsmartcontracts", "") + + HelpExampleRpc("listsmartcontracts", "") + }, + }.Check(request); + + UniValue arr(UniValue::VARR); + for (const auto& item : Params().GetConsensus().smartContracts) { + UniValue obj(UniValue::VOBJ); + CTxDestination dest; + ExtractDestination(item.second, dest); + obj.pushKV("name", item.first); + obj.pushKV("call", GetContractCall(item.first)); + obj.pushKV("address", EncodeDestination(dest)); + + pcustomcsview->ForEachBalance([&](CScript const & owner, CTokenAmount balance) { + if (owner != item.second) { + return false; + } + obj.pushKV(balance.nTokenId.ToString(), ValueFromAmount(balance.nValue)); + return true; + }, BalanceKey{item.second, {0}}); + + arr.push_back(obj); + } + return arr; +} + + static UniValue clearmempool(const JSONRPCRequest& request) { auto pwallet = GetWallet(request); @@ -807,6 +857,7 @@ static const CRPCCommand commands[] = {"blockchain", "getgov", &getgov, {"name"}}, {"blockchain", "listgovs", &listgovs, {""}}, {"blockchain", "isappliedcustomtx", &isappliedcustomtx, {"txid", "blockHeight"}}, + {"blockchain", "listsmartcontracts", &listsmartcontracts, {}}, {"blockchain", "clearmempool", &clearmempool, {} }, }; diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index 7ef8b16d2d..428d2503b5 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -1823,6 +1823,134 @@ UniValue getburninfo(const JSONRPCRequest& request) { return result; } + +UniValue executesmartcontract(const JSONRPCRequest& request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{"executesmartcontract", + "\nCreates and sends a transaction to either fund or execute a smart contract. Available contracts: dbtcdfiswap" + + HelpRequiringPassphrase(pwallet) + "\n", + { + {"name", RPCArg::Type::STR, RPCArg::Optional::NO, "Name of the smart contract to send funds to"}, + {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount to send in amount@token format"}, + {"address", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Address to be used in contract execution if required"}, + {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, + "A json array of json objects", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + }, + }, + }, + }, + }, + RPCResult{ + "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" + }, + RPCExamples{ + HelpExampleCli("executesmartcontract", "dbtcdfiswap 1000@DFI") + + HelpExampleRpc("executesmartcontract", "dbtcdfiswap, 1000@DFI") + }, + }.Check(request); + + if (pwallet->chain().isInitialBlockDownload()) { + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot create transactions while still in Initial Block Download"); + } + pwallet->BlockUntilSyncedToCurrentChain(); + + const auto& contractName = request.params[0].get_str(); + CTokenAmount amount = DecodeAmount(pwallet->chain(),request.params[1].get_str(), "amount"); + + if (contractName == "dbtcdfiswap") { + const auto& contractPair= Params().GetConsensus().smartContracts.begin(); + if (amount.nTokenId.v == 0) { + CUtxosToAccountMessage msg{}; + msg.to = {{contractPair->second, {{{{0}, amount.nValue}}}}}; + + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::UtxosToAccount) + << msg; + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + + rawTx.vout.push_back(CTxOut(amount.nValue, scriptMeta)); + + // change + CCoinControl coinControl; + CTxDestination dest; + ExtractDestination(Params().GetConsensus().foundationShareScript, dest); + coinControl.destChange = dest; + + // Only use inputs from dest + coinControl.matchDestination = dest; + + // fund + fund(rawTx, pwallet, {}, &coinControl); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight); + + return signsend(rawTx, pwallet, {})->GetHash().GetHex(); + } else { + if (request.params[2].isNull()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "BTC source address must be provided for " + contractPair->first); + } + + CTxDestination dest = DecodeDestination(request.params[2].get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid address"); + } + const auto script = GetScriptForDestination(dest); + + CSmartContractMessage msg{}; + msg.name = contractPair->first; + msg.accounts = {{script, {{{amount.nTokenId, amount.nValue}}}}}; + + // encode + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::SmartContract) + << msg; + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + + rawTx.vout.emplace_back(0, scriptMeta); + + CTransactionRef optAuthTx; + std::set auth{script}; + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auth, false, optAuthTx, request.params[3]); + + // Set change address + CCoinControl coinControl; + coinControl.destChange = dest; + + // fund + fund(rawTx, pwallet, optAuthTx, &coinControl); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); + } + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Specified smart contract not found"); + } + + return NullUniValue; +} + + static const CRPCCommand commands[] = { // category name actor (function) params @@ -1840,6 +1968,7 @@ static const CRPCCommand commands[] = {"accounts", "listcommunitybalances", &listcommunitybalances, {}}, {"accounts", "sendtokenstoaddress", &sendtokenstoaddress, {"from", "to", "selectionMode"}}, {"accounts", "getburninfo", &getburninfo, {}}, + {"accounts", "executesmartcontract", &executesmartcontract, {"name", "amount", "inputs"}}, }; void RegisterAccountsRPCCommands(CRPCTable& tableRPC) { diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index e7596f46ac..9ff0e4ca90 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -161,6 +161,11 @@ class CCustomTxRpcVisitor : public boost::static_visitor rpcInfo.pushKV("to", accountsInfo(obj.to)); } + void operator()(const CSmartContractMessage& obj) const { + rpcInfo.pushKV("name", obj.name); + rpcInfo.pushKV("accounts", accountsInfo(obj.accounts)); + } + void operator()(const CCreatePoolPairMessage& obj) const { rpcInfo.pushKV("creationTx", tx.GetHash().GetHex()); if (auto tokenPair = mnview.GetTokenByCreationTx(tx.GetHash())) diff --git a/src/masternodes/rpc_oracles.cpp b/src/masternodes/rpc_oracles.cpp index 341f45c8eb..101c61781a 100644 --- a/src/masternodes/rpc_oracles.cpp +++ b/src/masternodes/rpc_oracles.cpp @@ -1033,15 +1033,10 @@ UniValue getfixedintervalprice(const JSONRPCRequest& request) { " 2. Deviation is over the limit to be considered stable.\n" }, RPCExamples{ - HelpExampleCli("getfixedintervalprice", R"('{"fixedIntervalPriceId":"TSLA/USD"}')") + HelpExampleCli("getfixedintervalprice", "TSLA/USD") }, }.Check(request); - RPCTypeCheck(request.params, {UniValue::VSTR}, false); - if (request.params[0].isNull()) - throw JSONRPCError(RPC_INVALID_PARAMETER, - "Invalid parameter, argument fixedIntervalPriceId must be non-null"); - auto fixedIntervalStr = request.params[0].getValStr(); UniValue objPrice{UniValue::VOBJ}; objPrice.pushKV("fixedIntervalPriceId", fixedIntervalStr); diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 4933335e18..3531525ed3 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -81,6 +81,17 @@ CAmount AmountFromValue(const UniValue& value) return amount; } +bool AmountFromValue(const UniValue& value, CAmount& amount) +{ + if (!value.isNum() && !value.isStr()) + return false; + if (!ParseFixedPoint(value.getValStr(), 8, &amount)) + return false; + if (!MoneyRange(amount)) + return false; + return true; +} + uint256 ParseHashV(const UniValue& v, std::string strName) { std::string strHex(v.get_str()); diff --git a/src/rpc/util.h b/src/rpc/util.h index 4ecfc5e93e..59dd776032 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -69,6 +69,7 @@ extern std::vector ParseHexV(const UniValue& v, std::string strNa extern std::vector ParseHexO(const UniValue& o, std::string strKey); extern CAmount AmountFromValue(const UniValue& value); +extern bool AmountFromValue(const UniValue& value, CAmount& amount); extern std::string HelpExampleCli(const std::string& methodname, const std::string& args); extern std::string HelpExampleRpc(const std::string& methodname, const std::string& args); diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index 53ff172b10..27bc4a1fea 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -444,14 +444,18 @@ def run_test(self): assert_raises_rpc_error(-5, "Unsupported version", self.nodes[0].setgov, {"ATTRIBUTES":{'1/token/15/payback_dfi':'true'}}) assert_raises_rpc_error(-5, "Empty value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/15/payback_dfi':''}}) assert_raises_rpc_error(-5, "Incorrect key for . Object of ['//ID/','value'] expected", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/payback_dfi':'true'}}) - assert_raises_rpc_error(-5, "Unrecognised type argument provided, valid types are: poolpairs, token,", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/unrecognised/5/payback_dfi':'true'}}) + assert_raises_rpc_error(-5, "Unrecognised type argument provided, valid types are: params, poolpairs, token,", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/unrecognised/5/payback_dfi':'true'}}) assert_raises_rpc_error(-5, "Unrecognised key argument provided, valid keys are: payback_dfi, payback_dfi_fee_pct,", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/unrecognised':'true'}}) assert_raises_rpc_error(-5, "Identifier must be a positive integer", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/not_a_number/payback_dfi':'true'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'not_a_number'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'unrecognised'}}) - assert_raises_rpc_error(-5, "Percentage must be a positive integer or float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) - assert_raises_rpc_error(-5, "Percentage must be a positive integer or float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) + assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) assert_raises_rpc_error(-32600, "ATTRIBUTES: No such loan token (5)", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'true'}}) + assert_raises_rpc_error(-5, "DFIP2201 actve value must be either \"true\" or \"false\"", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/active':'not_a_bool'}}) + assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': 'not_a_number'}}) + assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '10'}}) # Setup for loan related tests oracle_address = self.nodes[0].getnewaddress("", "legacy") @@ -511,5 +515,11 @@ def run_test(self): assert_equal(self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'], {'v0/token/5/payback_dfi': 'true', 'v0/token/5/payback_dfi_fee_pct': '0.01'}) assert_equal(self.nodes[0].listgovs()[8][0]['ATTRIBUTES'], {'v0/token/5/payback_dfi': 'true', 'v0/token/5/payback_dfi_fee_pct': '0.01'}) + # Test params + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/active':'true','v0/params/dfip2201/minswap':'0.001','v0/params/dfip2201/premium':'0.025'}}) + self.nodes[0].generate(1) + assert_equal(self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'], {'v0/params/dfip2201/active': 'true', 'v0/params/dfip2201/premium': '0.025', 'v0/params/dfip2201/minswap': '0.001', 'v0/token/5/payback_dfi': 'true', 'v0/token/5/payback_dfi_fee_pct': '0.01'}) + assert_equal(self.nodes[0].listgovs()[8][0]['ATTRIBUTES'], {'v0/params/dfip2201/active': 'true', 'v0/params/dfip2201/premium': '0.025', 'v0/params/dfip2201/minswap': '0.001', 'v0/token/5/payback_dfi': 'true', 'v0/token/5/payback_dfi_fee_pct': '0.01'}) + if __name__ == '__main__': GovsetTest ().main () diff --git a/test/functional/feature_smart_contracts.py b/test/functional/feature_smart_contracts.py new file mode 100644 index 0000000000..ed05397be3 --- /dev/null +++ b/test/functional/feature_smart_contracts.py @@ -0,0 +1,259 @@ +#!/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 smart contracts""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal, assert_raises_rpc_error +from decimal import Decimal +import time + +class SmartContractTest(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.setup_clean_chain = True + self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-fortcanningheight=1', '-fortcanninghillheight=1010', '-subsidytest=1', '-txindex=1', '-jellyfish_regtest=1'], + ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-fortcanningheight=1', '-fortcanninghillheight=1010', '-subsidytest=1', '-txindex=1', '-jellyfish_regtest=1']] + + def rollback(self, count): + block = self.nodes[0].getblockhash(count) + self.nodes[0].invalidateblock(block) + self.nodes[1].invalidateblock(block) + self.nodes[0].clearmempool() + self.nodes[1].clearmempool() + self.sync_blocks() + + def run_test(self): + self.nodes[0].generate(1000) + self.sync_blocks() + + address = self.nodes[1].getnewaddress("", "legacy") + invalid_address = self.nodes[0].getnewaddress("", "legacy") + dfi_amount = 1000 + btc_amount = 1 + dfip = 'dbtcdfiswap' + + # Check invalid calls + assert_raises_rpc_error(-5, 'Incorrect authorization for {}'.format(invalid_address), self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2', invalid_address) + assert_raises_rpc_error(-4, 'Insufficient funds', self.nodes[0].executesmartcontract, dfip, str(dfi_amount) + '@0') + assert_raises_rpc_error(-8, 'Specified smart contract not found', self.nodes[1].executesmartcontract, 'DFIP9999', str(dfi_amount) + '@0') + assert_raises_rpc_error(-8, 'BTC source address must be provided for DFIP2201', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2') + + # Create tokens + self.nodes[0].createtoken({ + "symbol": "ETH", + "name": "Ether", + "isDAT": True, + "collateralAddress": self.nodes[0].get_genesis_keys().ownerAuthAddress + }) + self.nodes[0].generate(1) + self.nodes[0].createtoken({ + "symbol": "BTC", + "name": "Bitcoin", + "isDAT": True, + "collateralAddress": self.nodes[0].get_genesis_keys().ownerAuthAddress + }) + self.nodes[0].generate(1) + + # Create and fund address with BTC + address = self.nodes[1].getnewaddress("", "legacy") + self.nodes[0].minttokens("100000@BTC") + self.nodes[0].generate(1) + self.nodes[0].accounttoaccount(self.nodes[0].get_genesis_keys().ownerAuthAddress, {address: "20000@BTC"}) + self.nodes[0].generate(1) + for _ in range(20): + self.nodes[0].sendtoaddress(address, 0.1) + self.nodes[0].generate(1) + self.sync_blocks() + + # Check invalid calls + assert_raises_rpc_error(-32600, 'called before FortCanningHill height', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2', address) + + # Move to FortCanningHill + self.nodes[0].generate(1010 - self.nodes[0].getblockcount()) + self.sync_blocks() + + # Check invalid call + assert_raises_rpc_error(-32600, 'DFIP2201 smart contract is not enabled', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2', address) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/active':'false'}}) + self.nodes[0].generate(1) + + # Check invalid call + assert_raises_rpc_error(-32600, 'DFIP2201 smart contract is not enabled', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2', address) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/active':'true'}}) + self.nodes[0].generate(1) + + # Check invalid calls + assert_raises_rpc_error(-32600, 'is less than', self.nodes[1].executesmartcontract, dfip, '20000.00000001@2', address) + assert_raises_rpc_error(-3, 'Amount out of range', self.nodes[1].executesmartcontract, dfip, '0@2', address) + assert_raises_rpc_error(-32600, 'Specified token not found', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@9999', address) + assert_raises_rpc_error(-32600, 'Only Bitcoin can be swapped in DFIP2201', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@1', address) + assert_raises_rpc_error(-32600, 'fixedIntervalPrice with id not found', self.nodes[1].executesmartcontract, dfip, str(btc_amount) + '@2', address) + + # Test min swap + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/minswap':'0.00001'}}) + self.nodes[0].generate(1) + + # Check invalid calls + assert_raises_rpc_error(-32600, 'Below minimum swapable amount, must be at least 0.00001000 BTC', self.nodes[1].executesmartcontract, dfip, '0.00000999@2', address) + + # Set up oracles + oracle_address = self.nodes[0].getnewaddress("", "legacy") + feeds = [{"currency": "USD", "token": "DFI"}, + {"currency": "USD", "token": "BTC"}] + oracle = self.nodes[0].appointoracle(oracle_address, feeds, 10) + self.nodes[0].generate(1) + + prices = [{'currency': 'USD', 'tokenAmount': '1@DFI'}, + {'currency': 'USD', 'tokenAmount': '1@BTC'}] + self.nodes[0].setoracledata(oracle, int(time.time()), prices) + self.nodes[0].generate(1) + + self.nodes[0].setcollateraltoken({ + 'token': 'DFI', + 'factor': 1, + 'fixedIntervalPriceId': "DFI/USD"}) + + self.nodes[0].setcollateraltoken({ + 'token': 'BTC', + 'factor': 1, + 'fixedIntervalPriceId': "BTC/USD"}) + + self.nodes[0].generate(7) + self.sync_blocks() + + # Import community balance + self.nodes[1].importprivkey('cMv1JaaZ9Mbb3M3oNmcFvko8p7EcHJ8XD7RCQjzNaMs7BWRVZTyR') + balance = self.nodes[1].getbalance() + + # Try and fund more than is in community balance + assert_raises_rpc_error(-4, 'Insufficient funds', self.nodes[1].executesmartcontract, dfip, '18336.22505381@0', address) + + # Check smart contract details and balance + result = self.nodes[0].listsmartcontracts() + assert_equal(result[0]['name'], 'DFIP2201') + assert_equal(result[0]['call'], 'dbtcdfiswap') + assert_equal(result[0]['address'], 'bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202') + assert('0' not in result[0]) + + # Fund smart contract + tx = self.nodes[1].executesmartcontract(dfip, '18336.225@0') + self.nodes[1].generate(1) + self.sync_blocks() + + # Check smart contract details and balance + result = self.nodes[0].listsmartcontracts() + assert_equal(result[0]['0'], Decimal('18336.225')) + + # Check balance has changed as expected + block = self.nodes[0].getblock(self.nodes[0].getblockhash(self.nodes[0].getblockcount() - 100)) + community_reward = self.nodes[0].getrawtransaction(block['tx'][0], 1)['vout'][1]['value'] + fee = self.nodes[1].gettransaction(tx)['fee'] + assert_equal(balance + community_reward - Decimal('18336.225') + fee, self.nodes[1].getbalance()) + + # Test swap for more than in community fund by 1 Sat + block = self.nodes[0].getblockcount() + 1 + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/premium':'0.00000000'}}) + self.nodes[0].generate(1) + self.sync_blocks() + assert_raises_rpc_error(-32600, 'amount 18336.22500000 is less than 18336.22500001', self.nodes[1].executesmartcontract, dfip, '18336.22500001@2', address) + + # Test again for full amount in community balance + self.nodes[1].executesmartcontract(dfip, '18336.22500000@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(18336.22500000, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + assert('0' not in self.nodes[0].listsmartcontracts()) + + # Set "real world" prices + self.rollback(block) + prices = [{'currency': 'USD', 'tokenAmount': '2@DFI'}, + {'currency': 'USD', 'tokenAmount': '40000@BTC'}] + self.nodes[0].setoracledata(oracle, int(time.time()), prices) + self.nodes[0].generate(10) + + # Test default 2.5% premium + block = self.nodes[0].getblockcount() + 1 + self.nodes[1].executesmartcontract(dfip, '0.09999999@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(2049.999795, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test 5% premium + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/premium':'0.05'}}) + self.nodes[0].generate(1) + self.sync_blocks() + self.nodes[1].executesmartcontract(dfip, '0.09999999@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(2099.99979, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test 0.1% premium + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/premium':'0.001'}}) + self.nodes[0].generate(1) + self.sync_blocks() + self.nodes[1].executesmartcontract(dfip, '0.09999999@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(2001.9997998, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test 0.000001% premium + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/premium':'0.00000001'}}) + self.nodes[0].generate(1) + self.sync_blocks() + self.nodes[1].executesmartcontract(dfip, '0.1@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(2000.00002, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test 0% premium + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/premium':'0.00000000'}}) + self.nodes[0].generate(1) + self.sync_blocks() + self.nodes[1].executesmartcontract(dfip, '0.1@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(2000, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Swap min amount + self.rollback(block) + self.nodes[1].executesmartcontract(dfip, '0.00001@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(0.205, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test smallest min amount + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/minswap':'0.00000001'}}) + self.nodes[0].generate(1) + self.nodes[1].executesmartcontract(dfip, '0.00000001@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(0.000205, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test no smallest min amount + self.rollback(block) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/minswap':'0.00000001'}}) + self.nodes[0].generate(1) + self.nodes[1].executesmartcontract(dfip, '0.00000001@2', address) + self.nodes[1].generate(1) + self.sync_blocks() + assert_equal(0.000205, float(self.nodes[0].getaccount(address)[0].split('@')[0])) + + # Test disabling DFIP201 + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2201/active':'false'}}) + self.nodes[0].generate(1) + assert_raises_rpc_error(-32600, 'DFIP2201 smart contract is not enabled', self.nodes[1].executesmartcontract, dfip, '1@2', address) + +if __name__ == '__main__': + SmartContractTest().main() + diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 8098e5226e..95605272b1 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -136,6 +136,7 @@ 'rpc_getchaintips.py', 'rpc_misc.py', 'rpc_mn_basic.py', + 'feature_smart_contracts.py', 'feature_reject_customtxs.py', 'feature_initdist.py', 'feature_tokens_basic.py', From 47570f70c024b39e00bd95e1c5eedbdfb24b5cda Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Wed, 19 Jan 2022 06:00:49 +0000 Subject: [PATCH 2/4] lint: add new "circular deps" --- test/lint/lint-circular-dependencies.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/lint/lint-circular-dependencies.sh b/test/lint/lint-circular-dependencies.sh index a90b797ff3..5d00434790 100755 --- a/test/lint/lint-circular-dependencies.sh +++ b/test/lint/lint-circular-dependencies.sh @@ -33,6 +33,11 @@ EXPECTED_CIRCULAR_DEPENDENCIES=( "chainparams -> masternodes/mn_checks -> chainparams" "chainparams -> masternodes/mn_checks -> masternodes/anchors -> chainparams" "chainparams -> masternodes/mn_checks -> masternodes/vaulthistory -> masternodes/vault -> chainparams" + "chainparams -> masternodes/mn_checks -> rpc/util -> node/transaction -> net -> chainparams" + "chainparams -> masternodes/mn_checks -> rpc/util -> node/transaction -> net_processing -> chainparams" + "addrdb -> chainparams -> masternodes/mn_checks -> rpc/util -> node/transaction -> net -> addrdb" + "blockencodings -> chainparams -> masternodes/mn_checks -> rpc/util -> node/transaction -> net_processing -> blockencodings" + "addrdb -> chainparams -> masternodes/mn_checks -> rpc/util -> node/transaction -> net -> banman -> addrdb" "consensus/tx_verify -> masternodes/masternodes -> validation -> consensus/tx_verify" "consensus/tx_verify -> masternodes/mn_checks -> txmempool -> consensus/tx_verify" "masternodes/govvariables/attributes -> masternodes/gv -> masternodes/govvariables/attributes" @@ -44,6 +49,7 @@ EXPECTED_CIRCULAR_DEPENDENCIES=( "masternodes/govvariables/lp_daily_dfi_reward -> masternodes/masternodes -> validation -> masternodes/govvariables/lp_daily_dfi_reward" "masternodes/govvariables/oracle_block_interval -> masternodes/gv -> masternodes/govvariables/oracle_block_interval" "masternodes/govvariables/oracle_block_interval -> masternodes/masternodes -> masternodes/mn_checks -> masternodes/govvariables/oracle_block_interval" + "masternodes/govvariables/attributes -> masternodes/masternodes -> masternodes/mn_checks -> masternodes/govvariables/attributes" "masternodes/govvariables/oracle_deviation -> masternodes/gv -> masternodes/govvariables/oracle_deviation" "masternodes/govvariables/lp_splits -> masternodes/gv -> masternodes/govvariables/lp_splits" "masternodes/masternodes -> masternodes/oracles -> masternodes/masternodes" From cdaf1557dddc152f386e149733e7b64ad758133d Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Wed, 19 Jan 2022 07:34:55 +0000 Subject: [PATCH 3/4] Remove redundant COIN from calculations --- src/masternodes/govvariables/attributes.cpp | 2 +- src/masternodes/mn_checks.cpp | 4 ++-- test/functional/feature_setgov.py | 12 +++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 0877ce7f5d..6846bb94f5 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -42,7 +42,7 @@ static ResVal VerifyInt32(const std::string& str) { static ResVal VerifyFloat(const std::string& str) { CAmount amount = 0; if (!ParseFixedPoint(str, 8, &amount) || amount < 0) { - return Res::Err("Amount must be a float"); + return Res::Err("Amount must be a positive float"); } return {amount, Res::Ok()}; } diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 70adcc4dbe..8badbe9043 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1443,14 +1443,14 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor } } catch (const std::out_of_range&) {} - const auto& btcPrice = DivideAmounts(MultiplyAmounts(resVal.val.get(), (premium + COIN)), COIN); + const auto& btcPrice = MultiplyAmounts(resVal.val.get(), premium + COIN); resVal = mnview.GetValidatedIntervalPrice(dfiUsd, useNextPrice, requireLivePrice); if (!resVal) { return std::move(resVal); } - const auto totalDFI = DivideAmounts(MultiplyAmounts(DivideAmounts(btcPrice, resVal.val.get()), amount), COIN); + const auto totalDFI = MultiplyAmounts(DivideAmounts(btcPrice, resVal.val.get()), amount); res = mnview.SubBalance(Params().GetConsensus().smartContracts.begin()->second, {{0}, totalDFI}); if (!res) { diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index 27bc4a1fea..fca94defc7 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -449,13 +449,15 @@ def run_test(self): assert_raises_rpc_error(-5, "Identifier must be a positive integer", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/not_a_number/payback_dfi':'true'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'not_a_number'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'unrecognised'}}) - assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) - assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) assert_raises_rpc_error(-32600, "ATTRIBUTES: No such loan token (5)", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'true'}}) assert_raises_rpc_error(-5, "DFIP2201 actve value must be either \"true\" or \"false\"", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/active':'not_a_bool'}}) - assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'not_a_number'}}) - assert_raises_rpc_error(-5, "Amount must be a float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': 'not_a_number'}}) - assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '10'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'-1'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': 'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '-1'}}) + assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '1.00000001'}}) # Setup for loan related tests oracle_address = self.nodes[0].getnewaddress("", "legacy") From 722f1efe5f54eebba1e1234f3fe90e2922c3e57e Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Wed, 19 Jan 2022 07:42:04 +0000 Subject: [PATCH 4/4] Rename error message --- src/masternodes/govvariables/attributes.cpp | 2 +- test/functional/feature_setgov.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 6846bb94f5..36851f3b9c 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -42,7 +42,7 @@ static ResVal VerifyInt32(const std::string& str) { static ResVal VerifyFloat(const std::string& str) { CAmount amount = 0; if (!ParseFixedPoint(str, 8, &amount) || amount < 0) { - return Res::Err("Amount must be a positive float"); + return Res::Err("Amount must be a positive value"); } return {amount, Res::Ok()}; } diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index fca94defc7..d56ee340f2 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -449,14 +449,14 @@ def run_test(self): assert_raises_rpc_error(-5, "Identifier must be a positive integer", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/not_a_number/payback_dfi':'true'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'not_a_number'}}) assert_raises_rpc_error(-5, 'Payback DFI value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'unrecognised'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi_fee_pct':'-1'}}) assert_raises_rpc_error(-32600, "ATTRIBUTES: No such loan token (5)", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'true'}}) assert_raises_rpc_error(-5, "DFIP2201 actve value must be either \"true\" or \"false\"", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/active':'not_a_bool'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'-1'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'not_a_number'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': 'not_a_number'}}) - assert_raises_rpc_error(-5, "Amount must be a positive float", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '-1'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'-1'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/minswap':'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': 'not_a_number'}}) + assert_raises_rpc_error(-5, "Amount must be a positive value", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '-1'}}) assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2201/premium': '1.00000001'}}) # Setup for loan related tests