diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 5fe6d36ae1..e1095c98d2 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -59,6 +59,7 @@ const std::map& ATTRIBUTES::allowedTypes() { {"poolpairs", AttributeTypes::Poolpairs}, {"token", AttributeTypes::Token}, {"gov", AttributeTypes::Governance}, + {"consortium", AttributeTypes::Consortium}, }; return types; } @@ -72,6 +73,7 @@ const std::map& ATTRIBUTES::displayTypes() { {AttributeTypes::Poolpairs, "poolpairs"}, {AttributeTypes::Token, "token"}, {AttributeTypes::Governance,"gov"}, + {AttributeTypes::Consortium,"consortium"} }; return types; } @@ -161,6 +163,13 @@ const std::map>& ATTRIBUTES::allowedKeys {"loan_minting_interest", TokenKeys::LoanMintingInterest}, } }, + { + AttributeTypes::Consortium, { + {"members", ConsortiumKeys::MemberValues}, + {"mint_limit", ConsortiumKeys::MintLimit}, + {"mint_limit_daily", ConsortiumKeys::DailyMintLimit}, + } + }, { AttributeTypes::Poolpairs, { {"token_a_fee_pct", PoolKeys::TokenAFeePCT}, @@ -185,6 +194,7 @@ const std::map>& ATTRIBUTES::allowedKeys {"mn-setoperatoraddress", DFIPKeys::MNSetOperatorAddress}, {"mn-setowneraddress", DFIPKeys::MNSetOwnerAddress}, {"gov", DFIPKeys::GovernanceEnabled}, + {"consortium", DFIPKeys::ConsortiumEnabled}, {"members", DFIPKeys::Members}, } }, @@ -230,6 +240,13 @@ const std::map>& ATTRIBUTES::displayKeys {TokenKeys::Epitaph, "epitaph"}, } }, + { + AttributeTypes::Consortium, { + {ConsortiumKeys::MemberValues, "members"}, + {ConsortiumKeys::MintLimit, "mint_limit"}, + {ConsortiumKeys::DailyMintLimit,"mint_limit_daily"}, + } + }, { AttributeTypes::Poolpairs, { {PoolKeys::TokenAFeePCT, "token_a_fee_pct"}, @@ -254,6 +271,7 @@ const std::map>& ATTRIBUTES::displayKeys {DFIPKeys::MNSetOperatorAddress, "mn-setoperatoraddress"}, {DFIPKeys::MNSetOwnerAddress, "mn-setowneraddress"}, {DFIPKeys::GovernanceEnabled, "gov"}, + {DFIPKeys::ConsortiumEnabled, "consortium"}, {DFIPKeys::Members, "members"}, } }, @@ -270,6 +288,8 @@ const std::map>& ATTRIBUTES::displayKeys {EconomyKeys::DFIP2206FMinted, "dfip2206f_minted"}, {EconomyKeys::NegativeInt, "negative_interest"}, {EconomyKeys::NegativeIntCurrent, "negative_interest_current"}, + {EconomyKeys::ConsortiumMinted, "consortium"}, + {EconomyKeys::ConsortiumMembersMinted, "consortium_members"}, {EconomyKeys::BatchRoundingExcess, "batch_rounding_excess"}, {EconomyKeys::ConsolidatedInterest, "consolidated_interest"}, {EconomyKeys::Loans, "loans"}, @@ -464,6 +484,64 @@ static bool VerifyToken(const CCustomCSView& view, const uint32_t id) { return view.GetToken(DCT_ID{id}).has_value(); } +static ResVal VerifyConsortiumMember(const std::string& str) { + UniValue values(UniValue::VOBJ); + CConsortiumMembers members; + + if (!values.read(str)) + return Res::Err("Not a valid consortium member object!"); + + for (const auto &key : values.getKeys()) + { + UniValue value(values[key].get_obj()); + CConsortiumMember member; + + member.status = 0; + + member.name = trim_all_ws(value["name"].getValStr()).substr(0, CConsortiumMember::MAX_CONSORTIUM_MEMBERS_STRING_LENGTH); + if (member.name.size() < CConsortiumMember::MIN_CONSORTIUM_MEMBERS_STRING_LENGTH) { + return Res::Err("Member name too short, must be at least %d chars long", int(CConsortiumMember::MIN_CONSORTIUM_MEMBERS_STRING_LENGTH)); + } + + if (!value["ownerAddress"].isNull()) { + const auto dest = DecodeDestination(value["ownerAddress"].getValStr()); + if (!IsValidDestination(dest)) { + return Res::Err("Invalid ownerAddress in consortium member data"); + } + member.ownerAddress = GetScriptForDestination(dest); + } else { + return Res::Err("Empty ownerAddress in consortium member data!"); + } + + member.backingId = trim_all_ws(value["backingId"].getValStr()).substr(0, CConsortiumMember::MAX_CONSORTIUM_MEMBERS_STRING_LENGTH); + if (!AmountFromValue(value["mintLimit"], member.mintLimit) || !member.mintLimit) { + return Res::Err("Mint limit is an invalid amount"); + } + + if (!AmountFromValue(value["dailyMintLimit"], member.dailyMintLimit) || !member.dailyMintLimit) { + return Res::Err("Daily mint limit is an invalid amount"); + } + + if (!value["status"].isNull()) + { + uint32_t tmp; + + if (ParseUInt32(value["status"].getValStr(), &tmp)) { + if (tmp > 1) { + return Res::Err("Status can be either 0 or 1"); + } + member.status = static_cast(tmp); + } else { + return Res::Err("Status must be a positive number!"); + } + } + + members[key] = member; + } + + return {members, Res::Ok()}; +} + static inline void rtrim(std::string& s, unsigned char remove) { s.erase(std::find_if(s.rbegin(), s.rend(), [&remove](unsigned char ch) { return ch != remove; @@ -492,6 +570,13 @@ const std::maptype == AttributeTypes::Consortium && attrV0->key == ConsortiumKeys::MemberValues) { + if (auto attrValue = std::get_if(&value)) { + auto members = GetValue(*attrV0, CConsortiumMembers{}); + + for (auto const & member : *attrValue) + { + for (auto const & tmp : members) + if (tmp.first != member.first && tmp.second.ownerAddress == member.second.ownerAddress) + return Res::Err("Cannot add a member with an owner address of a existing consortium member!"); + + members[member.first] = member.second; + } + SetValue(*attrV0, members); + + return Res::Ok(); + } else + return Res::Err("Invalid member data"); } } SetValue(attribute, value); @@ -1069,9 +1173,9 @@ UniValue ATTRIBUTES::ExportFiltered(GovVarsFilter filter, const std::string &pre } else if (const auto number = std::get_if(&attribute.second)) { ret.pushKV(key, KeyBuilder(*number)); } else if (const auto amount = std::get_if(&attribute.second)) { - if ((attrV0->type == AttributeTypes::Param && - (attrV0->typeId == ParamIDs::DFIP2203 || attrV0->typeId == ParamIDs::DFIP2206F) && - (attrV0->key == DFIPKeys::BlockPeriod || attrV0->key == DFIPKeys::StartBlock))) { + if (attrV0->type == AttributeTypes::Param && + (attrV0->typeId == DFIP2203 || attrV0->typeId == DFIP2206F) && + (attrV0->key == DFIPKeys::BlockPeriod || attrV0->key == DFIPKeys::StartBlock)) { ret.pushKV(key, KeyBuilder(*amount)); } else { auto decimalStr = GetDecimaleString(*amount); @@ -1109,6 +1213,47 @@ UniValue ATTRIBUTES::ExportFiltered(GovVarsFilter filter, const std::string &pre ret.pushKV(KeyBuilder(poolkey, "total_swap_a"), ValueFromUint(dexTokenA.swaps)); ret.pushKV(KeyBuilder(poolkey, "total_swap_b"), ValueFromUint(dexTokenB.swaps)); } + } else if (auto members = std::get_if(&attribute.second)) { + UniValue result(UniValue::VOBJ); + for (const auto& [id, member] : *members) + { + UniValue elem(UniValue::VOBJ); + elem.pushKV("name", member.name); + elem.pushKV("ownerAddress", ScriptToString(member.ownerAddress)); + elem.pushKV("backingId", member.backingId); + elem.pushKV("mintLimit", ValueFromAmount(member.mintLimit)); + elem.pushKV("dailyMintLimit", ValueFromAmount(member.dailyMintLimit)); + elem.pushKV("status", member.status); + result.pushKV(id, elem); + } + ret.pushKV(key, result.write()); + } else if (auto consortiumMinted = std::get_if(&attribute.second)) { + for (const auto& token : *consortiumMinted) + { + auto& minted = token.second.minted; + auto& burnt = token.second.burnt; + + auto tokenKey = KeyBuilder(key, token.first.v); + ret.pushKV(KeyBuilder(tokenKey, "minted"), ValueFromAmount(minted)); + ret.pushKV(KeyBuilder(tokenKey, "burnt"), ValueFromAmount(burnt)); + ret.pushKV(KeyBuilder(tokenKey, "supply"), ValueFromAmount(minted - burnt)); + } + } else if (auto membersMinted = std::get_if(&attribute.second)) { + for (const auto& token : *membersMinted) + { + for (const auto& member : token.second) + { + auto& minted = member.second.minted; + auto& burnt = member.second.burnt; + + auto tokenKey = KeyBuilder(key, token.first.v); + auto memberKey = KeyBuilder(tokenKey, member.first); + ret.pushKV(KeyBuilder(memberKey, "minted"), ValueFromAmount(minted)); + ret.pushKV(KeyBuilder(memberKey, "daily_minted"), KeyBuilder(member.second.dailyMinted.first, ValueFromAmount(member.second.dailyMinted.second).getValStr())); + ret.pushKV(KeyBuilder(memberKey, "burnt"), ValueFromAmount(burnt)); + ret.pushKV(KeyBuilder(memberKey, "supply"), ValueFromAmount(minted - burnt)); + } + } } else if (const auto splitValues = std::get_if(&attribute.second)) { std::string keyValue; for (auto it{splitValues->begin()}; it != splitValues->end(); ++it) { @@ -1261,6 +1406,50 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const } break; + case AttributeTypes::Consortium: + switch (attrV0->key) { + case ConsortiumKeys::MemberValues: { + if (view.GetLastHeight() < Params().GetConsensus().GrandCentralHeight) + return Res::Err("Cannot be set before GrandCentral"); + + if (!view.GetToken(DCT_ID{attrV0->typeId})) + return Res::Err("No such token (%d)", attrV0->typeId); + + const auto members = std::get_if(&value); + if (!members) { + return Res::Err("Unexpected value"); + } + + CDataStructureV0 maxLimitKey{AttributeTypes::Consortium, attrV0->typeId, ConsortiumKeys::MintLimit}; + const auto maxLimit = GetValue(maxLimitKey, CAmount{0}); + + CDataStructureV0 dailyLimitKey{AttributeTypes::Consortium, attrV0->typeId, ConsortiumKeys::DailyMintLimit}; + const auto dailyLimit = GetValue(dailyLimitKey, CAmount{0}); + + for (const auto& [id, member] : *members) { + if (member.mintLimit > maxLimit && maxLimit != -1 * COIN) { + return Res::Err("Mint limit higher than global mint limit"); + } + + if (member.dailyMintLimit > dailyLimit && dailyLimit != -1 * COIN) { + return Res::Err("Daily mint limit higher than daily global mint limit"); + } + } + break; + } + case ConsortiumKeys::MintLimit: + case ConsortiumKeys::DailyMintLimit: + if (view.GetLastHeight() < Params().GetConsensus().GrandCentralHeight) + return Res::Err("Cannot be set before GrandCentral"); + + if (!view.GetToken(DCT_ID{attrV0->typeId})) + return Res::Err("No such token (%d)", attrV0->typeId); + break; + default: + return Res::Err("Unsupported key"); + } + break; + case AttributeTypes::Oracles: if (view.GetLastHeight() < Params().GetConsensus().FortCanningCrunchHeight) { return Res::Err("Cannot be set before FortCanningCrunch"); diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index 0bd66f97b2..4751028940 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -21,7 +21,8 @@ enum AttributeTypes : uint8_t { Token = 't', Poolpairs = 'p', Locks = 'L', - Governance= 'g' + Governance= 'g', + Consortium = 'c', }; enum ParamIDs : uint8_t { @@ -57,10 +58,12 @@ enum EconomyKeys : uint8_t { DexTokens = 'i', NegativeInt = 'j', NegativeIntCurrent = 'k', - BatchRoundingExcess = 'l', // Extra added to loan amounts on auction creation due to round errors. - ConsolidatedInterest = 'm', // Amount added to loan amounts after auction with no bids. - PaybackDFITokensPrincipal = 'n', // Same as PaybackDFITokens but without interest. - Loans = 'o', + ConsortiumMinted = 'l', + ConsortiumMembersMinted = 'm', + BatchRoundingExcess = 'n', // Extra added to loan amounts on auction creation due to round errors. + ConsolidatedInterest = 'o', // Amount added to loan amounts after auction with no bids. + PaybackDFITokensPrincipal = 'p', // Same as PaybackDFITokens but without interest. + Loans = 'q', }; enum DFIPKeys : uint8_t { @@ -77,8 +80,9 @@ enum DFIPKeys : uint8_t { MNSetRewardAddress = 'l', MNSetOperatorAddress = 'm', MNSetOwnerAddress = 'n', - GovernanceEnabled = 'o', + ConsortiumEnabled = 'o', Members = 'p', + GovernanceEnabled = 'q', }; enum GovernanceKeys : uint8_t { @@ -115,6 +119,12 @@ enum TokenKeys : uint8_t { LoanPaybackCollateral = 'p', }; +enum ConsortiumKeys : uint8_t { + MemberValues = 'a', + MintLimit = 'b', + DailyMintLimit = 'c', +}; + enum PoolKeys : uint8_t { TokenAFeePCT = 'a', TokenBFeePCT = 'b', @@ -223,12 +233,74 @@ enum FeeDirValues : uint8_t { Out }; -using CDexBalances = std::map; -using OracleSplits = std::map; +struct CConsortiumMember +{ + static const uint16_t MAX_CONSORTIUM_MEMBERS_STRING_LENGTH = 512; + static const uint16_t MIN_CONSORTIUM_MEMBERS_STRING_LENGTH = 3; + enum Status : uint8_t + { + Active = 0, + Disabled = 0x01, + }; + + std::string name; + CScript ownerAddress; + std::string backingId; + CAmount mintLimit; + CAmount dailyMintLimit; + uint8_t status; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(name); + READWRITE(ownerAddress); + READWRITE(backingId); + READWRITE(mintLimit); + READWRITE(dailyMintLimit); + READWRITE(status); + } +}; + +struct CConsortiumMinted +{ + CAmount minted; + CAmount burnt; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(minted); + READWRITE(burnt); + } +}; + +struct CConsortiumDailyMinted : public CConsortiumMinted +{ + std::pair dailyMinted; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITEAS(CConsortiumMinted, *this); + READWRITE(dailyMinted); + } +}; + +using CDexBalances = std::map; +using OracleSplits = std::map; using DescendantValue = std::pair; -using AscendantValue = std::pair; -using CAttributeType = std::variant; -using CAttributeValue = std::variant, std::set>; +using AscendantValue = std::pair; +using CConsortiumMembers = std::map; +using CConsortiumMembersMinted = std::map>; +using CConsortiumGlobalMinted = std::map; +using CAttributeType = std::variant; +using CAttributeValue = std::variant, std::set, CConsortiumMembers, CConsortiumMembersMinted, CConsortiumGlobalMinted, + int32_t, uint32_t>; void TrackNegativeInterest(CCustomCSView& mnview, const CTokenAmount& amount); @@ -347,6 +419,7 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator& displayTypes(); static const std::map& displayParamsIDs(); static const std::map& displayOracleIDs(); + static const std::map& displayConsortiumIDs(); static const std::map& displayGovernanceIDs(); static const std::map>& displayKeys(); @@ -367,6 +440,7 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator& allowedParamIDs(); static const std::map& allowedLocksIDs(); static const std::map& allowedOracleIDs(); + static const std::map& allowedConsortiumIDs(); static const std::map& allowedGovernanceIDs(); static const std::map>& allowedKeys(); static const std::map CCustomTxVisitor::MintableToken(DCT_ID id, const CTokenImplement if (token.IsPoolShare()) { return Res::Err("can't mint LPS token %s!", id.ToString()); } - + static const auto isMainNet = Params().NetworkIDString() == CBaseChainParams::MAIN; // may be different logic with LPS, so, dedicated check: if (!token.IsMintable() || (isMainNet && mnview.GetLoanTokenByID(id))) { @@ -780,7 +787,8 @@ ResVal CCustomTxVisitor::MintableToken(DCT_ID id, const CTokenImplement if (!token.IsDAT()) { return Res::Err("tx must have at least one input from token owner"); } else if (!HasFoundationAuth()) { // Is a DAT, check founders auth - return Res::Err("token is DAT and tx not from foundation member"); + if (height < static_cast(consensus.GrandCentralHeight)) + return Res::Err("token is DAT and tx not from foundation member"); } } @@ -1274,36 +1282,204 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor Res operator()(const CMintTokensMessage& obj) const { // check auth and increase balance of token's owner - for (const auto& kv : obj.balances) { - const DCT_ID& tokenId = kv.first; - - if (Params().NetworkIDString() == CBaseChainParams::MAIN && height >= static_cast(consensus.FortCanningCrunchHeight) && + for (const auto& [tokenId, amount] : obj.balances) { + if (Params().NetworkIDString() == CBaseChainParams::MAIN && + height >= static_cast(consensus.FortCanningCrunchHeight) && mnview.GetLoanTokenByID(tokenId)) { return Res::Err("Loan tokens cannot be minted"); } auto token = mnview.GetToken(tokenId); - if (!token) { + if (!token) return Res::Err("token %s does not exist!", tokenId.ToString()); - } auto mintable = MintableToken(tokenId, *token); - if (!mintable) { + if (!mintable) return std::move(mintable); + + + if (height >= static_cast(consensus.GrandCentralHeight) && token->IsDAT() && !HasFoundationAuth()) + { + auto attributes = mnview.GetAttributes(); + assert(attributes); + + CDataStructureV0 enableKey{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::ConsortiumEnabled}; + if (attributes->GetValue(enableKey, false)) + { + mintable.ok = false; + + CDataStructureV0 membersKey{AttributeTypes::Consortium, tokenId.v, ConsortiumKeys::MemberValues}; + const auto members = attributes->GetValue(membersKey, CConsortiumMembers{}); + + CDataStructureV0 membersMintedKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::ConsortiumMembersMinted}; + auto membersBalances = attributes->GetValue(membersMintedKey, CConsortiumMembersMinted{}); + + const auto dailyInterval = height / consensus.blocksPerDay() * consensus.blocksPerDay(); + + for (auto const& [key, member] : members) + { + if (HasAuth(member.ownerAddress)) + { + if (member.status != CConsortiumMember::Status::Active) + return Res::Err("Cannot mint token, not an active member of consortium for %s!", token->symbol); + + auto add = SafeAdd(membersBalances[tokenId][key].minted, amount); + if (!add) + return (std::move(add)); + membersBalances[tokenId][key].minted = add; + + if (dailyInterval == membersBalances[tokenId][key].dailyMinted.first) { + add = SafeAdd(membersBalances[tokenId][key].dailyMinted.second, amount); + if (!add) + return (std::move(add)); + membersBalances[tokenId][key].dailyMinted.second = add; + } else { + membersBalances[tokenId][key].dailyMinted.first = dailyInterval; + membersBalances[tokenId][key].dailyMinted.second = amount; + } + + if (membersBalances[tokenId][key].minted > member.mintLimit) + return Res::Err("You will exceed your maximum mint limit for %s token by minting this amount!", token->symbol); + + if (membersBalances[tokenId][key].dailyMinted.second > member.dailyMintLimit) { + return Res::Err("You will exceed your daily mint limit for %s token by minting this amount", token->symbol); + } + + *mintable.val = member.ownerAddress; + mintable.ok = true; + break; + } + } + + if (!mintable) + return Res::Err("You are not a foundation or consortium member and cannot mint this token!"); + + CDataStructureV0 maxLimitKey{AttributeTypes::Consortium, tokenId.v, ConsortiumKeys::MintLimit}; + const auto maxLimit = attributes->GetValue(maxLimitKey, CAmount{0}); + + CDataStructureV0 dailyLimitKey{AttributeTypes::Consortium, tokenId.v, ConsortiumKeys::DailyMintLimit}; + const auto dailyLimit = attributes->GetValue(dailyLimitKey, CAmount{0}); + + CDataStructureV0 consortiumMintedKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::ConsortiumMinted}; + auto globalBalances = attributes->GetValue(consortiumMintedKey, CConsortiumGlobalMinted{}); + + auto add = SafeAdd(globalBalances[tokenId].minted, amount); + if (!add) + return (std::move(add)); + + globalBalances[tokenId].minted = add; + + if (maxLimit != -1 * COIN && globalBalances[tokenId].minted > maxLimit) + return Res::Err("You will exceed global maximum consortium mint limit for %s token by minting this amount!", token->symbol); + + CAmount totalDaily{}; + for (const auto& [key, value] : membersBalances[tokenId]) { + if (value.dailyMinted.first == dailyInterval) { + totalDaily += value.dailyMinted.second; + } + } + + if (dailyLimit != -1 * COIN && totalDaily > dailyLimit) + return Res::Err("You will exceed global daily maximum consortium mint limit for %s token by minting this amount.", token->symbol); + + attributes->SetValue(consortiumMintedKey, globalBalances); + attributes->SetValue(membersMintedKey, membersBalances); + + auto saved = mnview.SetVariable(*attributes); + if (!saved) + return saved; + } + else + return Res::Err("You are not a foundation member and cannot mint this token!"); } - auto minted = mnview.AddMintedTokens(tokenId, kv.second); - if (!minted) { + auto minted = mnview.AddMintedTokens(tokenId, amount); + if (!minted) return minted; - } CalculateOwnerRewards(*mintable.val); - auto res = mnview.AddBalance(*mintable.val, CTokenAmount{tokenId, kv.second}); - if (!res) { + auto res = mnview.AddBalance(*mintable.val, CTokenAmount{tokenId, amount}); + if (!res) return res; + } + + return Res::Ok(); + } + + Res operator()(const CBurnTokensMessage& obj) const { + if (obj.amounts.balances.empty()) { + return Res::Err("tx must have balances to burn"); + } + + for (const auto& [tokenId, amount] : obj.amounts.balances) + { + // check auth + if (!HasAuth(obj.from)) + return Res::Err("tx must have at least one input from account owner"); + + auto subMinted = mnview.SubMintedTokens(tokenId, amount); + if (!subMinted) + return subMinted; + + if (obj.burnType != CBurnTokensMessage::BurnType::TokenBurn) + return Res::Err("Currently only burn type 0 - TokenBurn is supported!"); + + CScript ownerAddress; + + if (auto address = std::get_if(&obj.context); address && !address->empty()) + ownerAddress = *address; + else ownerAddress = obj.from; + + auto attributes = mnview.GetAttributes(); + if (!attributes) + return Res::Err("Cannot read from attributes gov variable!"); + + CDataStructureV0 membersKey{AttributeTypes::Consortium, tokenId.v, ConsortiumKeys::MemberValues}; + const auto members = attributes->GetValue(membersKey, CConsortiumMembers{}); + CDataStructureV0 membersMintedKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::ConsortiumMembersMinted}; + auto membersBalances = attributes->GetValue(membersMintedKey, CConsortiumMembersMinted{}); + CDataStructureV0 consortiumMintedKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::ConsortiumMinted}; + auto globalBalances = attributes->GetValue(consortiumMintedKey, CConsortiumGlobalMinted{}); + + bool setVariable = false; + for (auto const& tmp : members) + if (tmp.second.ownerAddress == ownerAddress) + { + auto add = SafeAdd(membersBalances[tokenId][tmp.first].burnt, amount); + if (!add) + return (std::move(add)); + + membersBalances[tokenId][tmp.first].burnt = add; + + add = SafeAdd(globalBalances[tokenId].burnt, amount); + if (!add) + return (std::move(add)); + + globalBalances[tokenId].burnt = add; + + setVariable = true; + break; + } + + if (setVariable) + { + attributes->SetValue(membersMintedKey, membersBalances); + attributes->SetValue(consortiumMintedKey, globalBalances); + + auto saved = mnview.SetVariable(*attributes); + if (!saved) + return saved; } + + CalculateOwnerRewards(obj.from); + + auto res = TransferTokenBalance(tokenId, amount, obj.from, consensus.burnAddress); + if (!res) + return res; } + return Res::Ok(); + } Res operator()(const CCreatePoolPairMessage& obj) const { diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 2d9cc29953..324c4ab9df 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -83,10 +83,11 @@ enum class CustomTxType : uint8_t ResignMasternode = 'R', UpdateMasternode = 'm', // custom tokens: - CreateToken = 'T', - MintToken = 'M', - UpdateToken = 'N', // previous type, only DAT flag triggers - UpdateTokenAny = 'n', // new type of token's update with any flags/fields possible + CreateToken = 'T', + MintToken = 'M', + BurnToken = 'F', + UpdateToken = 'N', // previous type, only DAT flag triggers + UpdateTokenAny = 'n', // new type of token's update with any flags/fields possible //poolpair CreatePoolPair = 'p', UpdatePoolPair = 'u', @@ -156,6 +157,7 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::UpdateMasternode: case CustomTxType::CreateToken: case CustomTxType::MintToken: + case CustomTxType::BurnToken: case CustomTxType::UpdateToken: case CustomTxType::UpdateTokenAny: case CustomTxType::CreatePoolPair: @@ -321,6 +323,28 @@ struct CMintTokensMessage : public CBalances { } }; +struct CBurnTokensMessage { + enum BurnType : uint8_t + { + TokenBurn = 0, + }; + + CBalances amounts; + CScript from; + BurnType burnType; + std::variant context; + + ADD_SERIALIZE_METHODS; + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(amounts); + READWRITE(from); + READWRITE(static_cast(burnType)); + READWRITE(context); + } +}; + + struct CCreatePoolPairMessage { CPoolPairMessage poolPair; std::string pairSymbol; @@ -365,6 +389,7 @@ using CCustomTxMessage = std::variant< CUpdateTokenPreAMKMessage, CUpdateTokenMessage, CMintTokensMessage, + CBurnTokensMessage, CCreatePoolPairMessage, CUpdatePoolPairMessage, CPoolSwapMessage, diff --git a/src/masternodes/mn_rpc.h b/src/masternodes/mn_rpc.h index c2eabb9802..992da82a6a 100644 --- a/src/masternodes/mn_rpc.h +++ b/src/masternodes/mn_rpc.h @@ -77,6 +77,7 @@ CAccounts SelectAccountsByTargetBalances(const CAccounts& accounts, const CBalan void execTestTx(const CTransaction& tx, uint32_t height, CTransactionRef optAuthTx = {}); CScript CreateScriptForHTLC(const JSONRPCRequest& request, uint32_t &blocks, std::vector& image); CPubKey PublickeyFromString(const std::string &pubkey); +std::optional AmIFounder(CWallet* const pwallet); std::optional GetFuturesBlock(const uint32_t typeId); #endif // DEFI_MASTERNODES_MN_RPC_H diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index c627e84539..618e2c0470 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -1809,6 +1809,7 @@ UniValue getburninfo(const JSONRPCRequest& request) { CAmount burnt{0}; CBalances burntTokens; + CBalances consortiumTokens; CBalances dexfeeburn; CBalances paybackfees; CBalances paybackFee; @@ -1899,6 +1900,16 @@ UniValue getburninfo(const JSONRPCRequest& request) { } return true; } + + // token burn with burnToken tx + if (value.category == uint8_t(CustomTxType::BurnToken)) + { + for (auto const & diff : value.diff) { + consortiumTokens.Add({diff.first, diff.second}); + } + return true; + } + // Token burn for (auto const & diff : value.diff) { burntTokens.Add({diff.first, diff.second}); @@ -1913,6 +1924,7 @@ UniValue getburninfo(const JSONRPCRequest& request) { result.pushKV("amount", ValueFromAmount(burntDFI)); result.pushKV("tokens", AmountsToJSON(burntTokens.balances)); + result.pushKV("consortiumtokens", AmountsToJSON(consortiumTokens.balances)); result.pushKV("feeburn", ValueFromAmount(burntFee)); result.pushKV("auctionburn", ValueFromAmount(auctionFee)); result.pushKV("paybackburn", AmountsToJSON(paybackFee.balances)); diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index 0dad4a6fa4..896fd151e1 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -57,6 +57,14 @@ class CCustomTxRpcVisitor rpcInfo.pushKV("availablePairs", availablePairs); } + UniValue tokenBalances(const CBalances& balances) const { + UniValue info(UniValue::VOBJ); + for (const auto& kv : balances.balances) { + info.pushKV(kv.first.ToString(), ValueFromAmount(kv.second)); + } + return info; + } + public: CCustomTxRpcVisitor(const CTransaction& tx, uint32_t height, CCustomCSView& mnview, UniValue& rpcInfo) : height(height), rpcInfo(rpcInfo), mnview(mnview), tx(tx) { @@ -105,14 +113,25 @@ class CCustomTxRpcVisitor } void operator()(const CMintTokensMessage& obj) const { - for (auto const & kv : obj.balances) { - if (auto token = mnview.GetToken(kv.first)) { - auto tokenImpl = static_cast(*token); - if (auto tokenPair = mnview.GetTokenByCreationTx(tokenImpl.creationTx)) { - rpcInfo.pushKV(tokenPair->first.ToString(), ValueFromAmount(kv.second)); - } - } + rpcInfo.pushKVs(tokenBalances(obj)); + } + + void operator()(const CBurnTokensMessage& obj) const { + rpcInfo.pushKVs(tokenBalances(obj.amounts)); + rpcInfo.pushKV("from", ScriptToString(obj.from)); + std::string type; + switch (obj.burnType) + { + case CBurnTokensMessage::BurnType::TokenBurn: + type = "TokenBurn"; + break; + default: + type = "Unexpected"; } + rpcInfo.pushKV("type", type); + + if (auto addr = std::get_if(&obj.context); !addr->empty()) + rpcInfo.pushKV("context", ScriptToString(*addr)); } void operator()(const CLiquidityMessage& obj) const { diff --git a/src/masternodes/rpc_tokens.cpp b/src/masternodes/rpc_tokens.cpp index 4738247a51..a278704e85 100644 --- a/src/masternodes/rpc_tokens.cpp +++ b/src/masternodes/rpc_tokens.cpp @@ -687,22 +687,51 @@ UniValue minttokens(const JSONRPCRequest& request) { // auth std::set auths; - bool needFoundersAuth = false; + auto needFoundersAuth{false}; if (txInputs.isNull() || txInputs.empty()) { - LOCK(cs_main); - for (auto const & kv : minted.balances) { - auto token = pcustomcsview->GetToken(kv.first); + LOCK(cs_main); // needed for coins tip + for (auto const & [id, amount] : minted.balances) { + const auto token = pcustomcsview->GetToken(id); if (!token) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Token %s does not exist!", kv.first.ToString())); + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Token %s does not exist!", id.ToString())); } + if (token->IsDAT()) { - needFoundersAuth = true; + auto found{false}; + auto attributes = pcustomcsview->GetAttributes(); + + if (attributes) { + CDataStructureV0 enableKey{AttributeTypes::Param, ParamIDs::Feature, DFIPKeys::ConsortiumEnabled}; + if (attributes->GetValue(enableKey, false)) + { + CDataStructureV0 membersKey{AttributeTypes::Consortium, id.v, ConsortiumKeys::MemberValues}; + auto members = attributes->GetValue(membersKey, CConsortiumMembers{}); + + for (auto const& member : members) { + if (IsMineCached(*pwallet, member.second.ownerAddress)) { + auths.insert(member.second.ownerAddress); + found = true; + } + } + } + } + + if (!found) { + needFoundersAuth = true; + } } // Get token owner auth if present const Coin& authCoin = ::ChainstateActive().CoinsTip().AccessCoin(COutPoint(token->creationTx, 1)); // always n=1 output - auths.insert(authCoin.out.scriptPubKey); + if (IsMineCached(*pwallet, authCoin.out.scriptPubKey)) { + auths.insert(authCoin.out.scriptPubKey); + } } } + + if (needFoundersAuth && !AmIFounder(pwallet)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Need foundation or consortium member authorization!"); + } + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, needFoundersAuth, optAuthTx, txInputs); CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); @@ -734,6 +763,106 @@ UniValue minttokens(const JSONRPCRequest& request) { return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); } +UniValue burntokens(const JSONRPCRequest& request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{"burntokens", + "\nCreates (and submits to local node and network) a transaction burning your token (for accounts and/or UTXOs). \n" + "The second optional argument (may be empty array) is an array of specific UTXOs to spend. One of UTXO's must belong to the token's owner (collateral) address" + + HelpRequiringPassphrase(pwallet) + "\n", + { + {"metadata", RPCArg::Type::OBJ, RPCArg::Optional::NO, "", + { + {"amounts", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount as json string, or array. Example: '[ \"amount@token\" ]'"}, + {"from", RPCArg::Type::STR, RPCArg::Optional::NO, "Address containing tokens to be burned."}, + {"context", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Additional data necessary for specific burn type"}, + } + }, + {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, + "A json array of json objects. Provide it if you want to spent specific UTXOs", + { + {"", 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("burntokens", "'{\"amounts\":\"10@symbol\",\"from\":\"address\"}'") + + HelpExampleCli("burntokens", "'{\"amounts\":\"10@symbol\",\"from\":\"address\",\"context\":\"consortium_member_address\"}'") + + HelpExampleCli("burntokens", "'{\"amounts\":\"10@symbol\",\"from\":\"address\"}' '[{\"txid\":\"id\",\"vout\":0}]'") + + HelpExampleRpc("burntokens", "'{\"amounts\":\"10@symbol\",\"from\":\"address\"}' '[{\"txid\":\"id\",\"vout\":0}]'") + }, + }.Check(request); + + if (pwallet->chain().isInitialBlockDownload()) { + throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, + "Cannot burn tokens while still in Initial Block Download"); + } + pwallet->BlockUntilSyncedToCurrentChain(); + + CBurnTokensMessage burnedTokens; + UniValue metaObj = request.params[0].get_obj(); + + if (!metaObj["amounts"].isNull()) + burnedTokens.amounts = DecodeAmounts(pwallet->chain(), metaObj["amounts"].getValStr(), ""); + else + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"amounts\" must not be null"); + if (!metaObj["from"].isNull()) + burnedTokens.from = DecodeScript(metaObj["from"].getValStr()); + else + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"from\" must not be null"); + + burnedTokens.burnType = CBurnTokensMessage::BurnType::TokenBurn; + if (!metaObj["context"].isNull()) + burnedTokens.context = DecodeScript(metaObj["context"].getValStr()); + + UniValue const & txInputs = request.params[2]; + + int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; + + std::set auths{burnedTokens.from}; + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + CTransactionRef optAuthTx; + + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, txInputs); + + CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + metadata << static_cast(CustomTxType::BurnToken) + << burnedTokens; + + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(metadata); + + rawTx.vout.push_back(CTxOut(0, scriptMeta)); + + CCoinControl coinControl; + + // Set change to auth address if there's only one auth address + if (auths.size() == 1) { + CTxDestination dest; + ExtractDestination(*auths.cbegin(), dest); + if (IsValidDestination(dest)) { + coinControl.destChange = dest; + } + } + + // fund + fund(rawTx, pwallet, optAuthTx, &coinControl); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} + UniValue decodecustomtx(const JSONRPCRequest& request) { RPCHelpMan{"decodecustomtx", @@ -824,6 +953,7 @@ static const CRPCCommand commands[] = {"tokens", "gettoken", &gettoken, {"key" }}, {"tokens", "getcustomtx", &getcustomtx, {"txid", "blockhash"}}, {"tokens", "minttokens", &minttokens, {"amounts", "inputs"}}, + {"tokens", "burntokens", &burntokens, {"metadata", "inputs"}}, {"tokens", "decodecustomtx", &decodecustomtx, {"hexstring", "iswitness"}}, }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index d9e341b9d2..0985c96f13 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -190,6 +190,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "listtokens", 1, "verbose" }, { "minttokens", 0, "amounts" }, { "minttokens", 1, "inputs"}, + { "burntokens", 0, "metadata" }, + { "burntokens", 1, "inputs"}, { "utxostoaccount", 0, "amounts" }, { "utxostoaccount", 1, "inputs" }, { "sendutxosfrom", 2, "amount" }, diff --git a/test/functional/feature_consortium.py b/test/functional/feature_consortium.py new file mode 100755 index 0000000000..ec1de909cd --- /dev/null +++ b/test/functional/feature_consortium.py @@ -0,0 +1,497 @@ +#!/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 Loan - loan basics.""" + +from test_framework.test_framework import DefiTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from decimal import Decimal + +class ConsortiumTest (DefiTestFramework): + def set_test_params(self): + self.num_nodes = 4 + self.setup_clean_chain = True + self.extra_args = [ + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1', '-fortcanningheight=50', '-eunosheight=50', '-fortcanninghillheight=50', '-fortcanningparkheight=50', '-fortcanningroadheight=50', '-fortcanningcrunchheight=50', '-fortcanningspringheight=50', '-fortcanninggreatworldheight=250', '-grandcentralheight=254', '-txindex=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1', '-fortcanningheight=50', '-eunosheight=50', '-fortcanninghillheight=50', '-fortcanningparkheight=50', '-fortcanningroadheight=50', '-fortcanningcrunchheight=50', '-fortcanningspringheight=50', '-fortcanninggreatworldheight=250', '-grandcentralheight=254', '-txindex=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1', '-fortcanningheight=50', '-eunosheight=50', '-fortcanninghillheight=50', '-fortcanningparkheight=50', '-fortcanningroadheight=50', '-fortcanningcrunchheight=50', '-fortcanningspringheight=50', '-fortcanninggreatworldheight=250', '-grandcentralheight=254', '-txindex=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1', '-fortcanningheight=50', '-eunosheight=50', '-fortcanninghillheight=50', '-fortcanningparkheight=50', '-fortcanningroadheight=50', '-fortcanningcrunchheight=50', '-fortcanningspringheight=50', '-fortcanninggreatworldheight=250', '-grandcentralheight=254', '-txindex=1']] + + def run_test(self): + + print("Generating initial chain...") + self.nodes[0].generate(100) + self.sync_blocks() + + account0 = self.nodes[0].get_genesis_keys().ownerAuthAddress + account1 = self.nodes[1].getnewaddress("", "legacy") + account2 = self.nodes[2].get_genesis_keys().ownerAuthAddress + account3 = self.nodes[3].get_genesis_keys().ownerAuthAddress + + self.nodes[1].generate(150) + self.sync_blocks() + + symbolBTC = "BTC" + symbolDOGE = "DOGE" + + self.nodes[0].createtoken({ + "symbol": symbolBTC, + "name": "BTC token", + "isDAT": True, + "collateralAddress": account0 + }) + + self.nodes[0].createtoken({ + "symbol": symbolDOGE, + "name": "DOGE token", + "isDAT": True, + "collateralAddress": account0 + }) + + self.nodes[0].sendtoaddress(account1, 10) + self.nodes[0].sendtoaddress(account2, 10) + self.nodes[0].sendtoaddress(account3, 10) + + self.nodes[0].generate(1) + self.sync_blocks() + + idBTC = list(self.nodes[0].gettoken(symbolBTC).keys())[0] + idDOGE = list(self.nodes[0].gettoken(symbolDOGE).keys())[0] + + assert_raises_rpc_error(-32600, "called before GrandCentral height", self.nodes[0].burntokens, { + 'amounts': "1@" + symbolBTC, + 'from': account0, + }) + + + self.nodes[0].generate(254 - self.nodes[0].getblockcount()) + self.sync_blocks() + + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolBTC]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolDOGE]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[3].minttokens, ["1@" + symbolBTC]) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/feature/consortium' : 'true'}}) + + self.nodes[0].generate(1) + self.sync_blocks() + + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolBTC]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolDOGE]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[3].minttokens, ["1@" + symbolBTC]) + + # Set global mint limits + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/mint_limit' : '10', 'v0/consortium/' + idBTC + '/mint_limit_daily' : '10'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + # Verify mint_limit set + attributes = self.nodes[0].getgov("ATTRIBUTES")["ATTRIBUTES"] + assert_equal(attributes['v0/consortium/' + idBTC + '/mint_limit'], '10') + assert_equal(attributes['v0/consortium/' + idBTC + '/mint_limit_daily'], '10') + + # Test setting member mint limit hight than global mint + assert_raises_rpc_error(-32600, "Mint limit higher than global mint limit", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"01":{"name":"account2BTC", \ + "ownerAddress":"' + account2 + '", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":10.00000000, \ + "mintLimit":11.00000000}}'}}) + + # Test setting member mint limit hight than global mint + assert_raises_rpc_error(-32600, "Daily mint limit higher than daily global mint limit", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"01":{"name":"account2BTC", \ + "ownerAddress":"' + account2 + '", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":11.00000000, \ + "mintLimit":10.00000000}}'}}) + + # Set consortium members + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"01":{"name":"account2BTC", \ + "ownerAddress":"' + account2 + '", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":10.00000000, \ + "mintLimit":10.00000000}, \ + "02":{"name":"account3BTC", \ + "ownerAddress":"' + account3 + '", \ + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", \ + "dailyMintLimit":4.00000000, \ + "mintLimit":4.00000000}}'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + assert_raises_rpc_error(-32600, "Cannot add a member with an owner address of a existing consortium member", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"03":{"name":"test", \ + "ownerAddress":"' + account2 +'", \ + "backingId":"7cb2f6954291d81d2270c9a6a52442b3f8c637b1ec793c731cb5f5a8f7fb9b9d", \ + "dailyMintLimit":10.00000000, \ + "mintLimit":10.00000000}}'}}) + + attribs = self.nodes[2].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/consortium/' + idBTC + '/members'], '{"01":{"name":"account2BTC","ownerAddress":"' + account2 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":10.00000000,"status":0},"02":{"name":"account3BTC","ownerAddress":"' + account3 +'","backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5","mintLimit":4.00000000,"dailyMintLimit":4.00000000,"status":0}}') + assert_equal(attribs['v0/consortium/' + idBTC + '/mint_limit'], '10') + + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolDOGE]) + + self.nodes[2].minttokens(["1@" + symbolBTC]) + self.nodes[2].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[2].getaccount(account2)[0], '1.00000000@' + symbolBTC) + attribs = self.nodes[2].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('1.00000000')) + + # Set global mint limits + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idDOGE + '/mint_limit' : '6', 'v0/consortium/' + idDOGE + '/mint_limit_daily' : '6'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + # Create consortium members + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idDOGE + '/members' : '{"01":{"name":"account2DOGE", \ + "ownerAddress":"' + account2 +'", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":5.00000000, \ + "mintLimit":5.00000000}}'}}) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idDOGE + '/members' : '{"02":{"name":"account1DOGE", \ + "ownerAddress":"' + account1 +'", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":5.00000000, \ + "mintLimit":5.00000000}}'}}) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idDOGE + '/mint_limit' : '6', 'v0/consortium/' + idDOGE + '/mint_limit_daily' : '6'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + attribs = self.nodes[2].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/consortium/' + idBTC + '/members'], '{"01":{"name":"account2BTC","ownerAddress":"' + account2 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":10.00000000,"status":0},"02":{"name":"account3BTC","ownerAddress":"' + account3 +'","backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5","mintLimit":4.00000000,"dailyMintLimit":4.00000000,"status":0}}') + assert_equal(attribs['v0/consortium/' + idBTC + '/mint_limit'], '10') + + assert_equal(attribs['v0/consortium/' + idDOGE + '/members'], '{"01":{"name":"account2DOGE","ownerAddress":"' + account2 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":5.00000000,"status":0},"02":{"name":"account1DOGE","ownerAddress":"' + account1 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":5.00000000,"status":0}}') + assert_equal(attribs['v0/consortium/' + idDOGE + '/mint_limit'], '6') + assert_equal(attribs['v0/consortium/' + idDOGE + '/mint_limit_daily'], '6') + + self.nodes[2].minttokens(["2@" + symbolDOGE]) + self.nodes[2].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[2].getaccount(account2), ['1.00000000@' + symbolBTC, '2.00000000@' + symbolDOGE]) + + attribs = self.nodes[2].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/1.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('2.00000000')) + + assert_raises_rpc_error(-32600, "You will exceed your maximum mint limit for " + symbolDOGE + " token by minting this amount!", self.nodes[2].minttokens, ["3.00000001@" + symbolDOGE]) + + self.nodes[2].burntokens({ + 'amounts': "1@" + symbolDOGE, + 'from': account2, + }) + + self.nodes[2].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[2].getaccount(account2), ['1.00000000@' + symbolBTC, '1.00000000@' + symbolDOGE]) + + attribs = self.nodes[2].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/1.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('1.00000000')) + + self.nodes[2].accounttoaccount(account2, {account0: "1@" + symbolDOGE}) + self.nodes[2].accounttoaccount(account2, {account1: "0.2@" + symbolBTC}) + self.nodes[2].generate(1) + self.sync_blocks() + + self.nodes[0].burntokens({ + 'amounts': "0.5@" + symbolDOGE, + 'from': account0, + 'context': account2 + }) + + self.nodes[0].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[0].getaccount(account2), ['0.80000000@' + symbolBTC]) + assert_equal(self.nodes[0].getaccount(account0), ['0.50000000@' + symbolDOGE]) + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/1.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('0.50000000')) + + self.nodes[0].burntokens({ + 'amounts': "0.5@" + symbolDOGE, + 'from': account0, + }) + + self.nodes[0].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[0].getaccount(account0), []) + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/1.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('0.50000000')) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idDOGE + '/members' : '{"01":{"name":"account2DOGE", \ + "ownerAddress":"' + account2 +'", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "mintLimit":5.00000000, \ + "dailyMintLimit":5.00000000, \ + "status":1}}'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/consortium/' + idDOGE + '/members'], '{"01":{"name":"account2DOGE","ownerAddress":"' + account2 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":5.00000000,"status":1},"02":{"name":"account1DOGE","ownerAddress":"' + account1 +'","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":5.00000000,"status":0}}') + assert_equal(self.nodes[0].getburninfo(), {'address': 'mfburnZSAM7Gs1hpDeNaMotJXSGA7edosG', 'amount': Decimal('0E-8'), 'tokens': [], 'consortiumtokens': ['2.00000000@DOGE'], 'feeburn': Decimal('2.00000000'), 'auctionburn': Decimal('0E-8'), 'paybackburn': [], 'dexfeetokens': [], 'dfipaybackfee': Decimal('0E-8'), 'dfipaybacktokens': [], 'paybackfees': [], 'paybacktokens': [], 'emissionburn': Decimal('4892.89500000'), 'dfip2203': [], 'dfip2206f': []}) + + assert_raises_rpc_error(-32600, "Cannot mint token, not an active member of consortium for DOGE!", self.nodes[2].minttokens, ["1@" + symbolDOGE]) + + # can mint because it is a founder + self.nodes[0].minttokens(["1@" + symbolBTC]) + self.nodes[0].generate(1) + self.sync_blocks() + + self.nodes[2].minttokens(["6@" + symbolBTC]) + self.nodes[2].generate(1) + self.sync_blocks() + + # burn to check that total minted is checked against max limit + self.nodes[2].burntokens({ + 'amounts': "6@" + symbolBTC, + 'from': account2, + }) + self.nodes[2].generate(1) + self.sync_blocks() + + self.nodes[3].minttokens(["2@" + symbolBTC]) + self.nodes[3].generate(1) + self.sync_blocks() + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('9.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('3.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('7.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/7.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/02/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/supply'], Decimal('2.00000000')) + + assert_raises_rpc_error(-32600, "You will exceed your maximum mint limit for " + symbolBTC + " token by minting this amount!", self.nodes[3].minttokens, ["2.00000001@" + symbolBTC]) + assert_raises_rpc_error(-32600, "You will exceed global maximum consortium mint limit for " + symbolBTC + " token by minting this amount!", self.nodes[3].minttokens, ["1.00000001@" + symbolBTC]) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"02":{"name":"account3BTC", \ + "ownerAddress":"' + account3 + '", \ + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", \ + "dailyMintLimit":4.00000000, \ + "mintLimit":6.00000000}}'}}) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/mint_limit' : '20'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + # Test global daily limit + assert_raises_rpc_error(-32600, "You will exceed global daily maximum consortium mint limit for " + symbolBTC + " token by minting this amount.", self.nodes[3].minttokens, ["2@" + symbolBTC]) + + # Increase global daily limit + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/mint_limit_daily' : '12'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + self.nodes[3].minttokens(["2@" + symbolBTC]) + self.nodes[3].generate(1) + self.sync_blocks() + + # Check minted and daily minted increased + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium_members/1/02/minted'], Decimal('4.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/daily_minted'], '144/4.00000000') + + # Test daily limit + assert_raises_rpc_error(-32600, "You will exceed your daily mint limit for " + symbolBTC + " token by minting this amount", self.nodes[3].minttokens, ["2@" + symbolBTC]) + + # Move to next day, day is 144 blocks on regtest. + self.nodes[0].generate(288 - self.nodes[0].getblockcount()) + self.sync_blocks() + + # Mint more tokens + self.nodes[3].minttokens(["2@" + symbolBTC]) + self.nodes[3].generate(1) + self.sync_blocks() + + # Check minted increased and daily minted reset + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium_members/1/02/minted'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/daily_minted'], '288/2.00000000') + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/' + idBTC + '/members' : '{"02":{"name":"account3BTC", \ + "ownerAddress":"' + account3 + '", \ + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", \ + "dailyMintLimit":2.00000000, \ + "mintLimit":8.00000000}}'}}) + + self.nodes[0].generate(1) + self.sync_blocks() + + # Test new daily limit + assert_raises_rpc_error(-32600, "You will exceed your daily mint limit for " + symbolBTC + " token by minting this amount", self.nodes[3].minttokens, ["1@" + symbolBTC]) + + # burn to check that burning from address of a member for different token does not get counted when burning other tokens + self.nodes[1].burntokens({ + 'amounts': "0.1@" + symbolBTC, + 'from': account1, + }) + self.nodes[1].generate(1) + self.sync_blocks() + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('13.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('7.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('7.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/7.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('1.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/minted'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/daily_minted'], '288/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/02/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/supply'], Decimal('6.00000000')) + + # burn to check context for a member from another member that is on different token + self.nodes[1].burntokens({ + 'amounts': "0.1@" + symbolBTC, + 'from': account1, + 'context': account2 + }) + self.nodes[1].generate(1) + self.sync_blocks() + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/consortium/1/minted'], Decimal('13.00000000')) + assert_equal(attribs['v0/live/economy/consortium/1/burnt'], Decimal('6.10000000')) + assert_equal(attribs['v0/live/economy/consortium/1/supply'], Decimal('6.90000000')) + assert_equal(attribs['v0/live/economy/consortium/2/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium/2/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium/2/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/minted'], Decimal('7.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/daily_minted'], '144/7.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/01/burnt'], Decimal('6.10000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/01/supply'], Decimal('0.90000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/minted'], Decimal('2.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/daily_minted'], '144/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/2/01/burnt'], Decimal('1.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/2/01/supply'], Decimal('0.50000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/minted'], Decimal('6.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/daily_minted'], '288/2.00000000') + assert_equal(attribs['v0/live/economy/consortium_members/1/02/burnt'], Decimal('0.00000000')) + assert_equal(attribs['v0/live/economy/consortium_members/1/02/supply'], Decimal('6.00000000')) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/feature/consortium' : 'false'}}) + + self.nodes[0].generate(1) + self.sync_blocks() + + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolBTC]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[2].minttokens, ["1@" + symbolDOGE]) + assert_raises_rpc_error(-5, "Need foundation or consortium member authorization", self.nodes[3].minttokens, ["1@" + symbolBTC]) + + # Test unlimited limit + self.nodes[0].setgov({"ATTRIBUTES": {'v0/consortium/' + idBTC + + '/mint_limit': '-1', 'v0/consortium/' + idBTC + '/mint_limit_daily': '-1'}}) + self.nodes[0].generate(1) + self.sync_blocks() + + self.nodes[0].minttokens(["100000000@" + symbolBTC]) + self.nodes[0].generate(1) + + # Increase limit + self.nodes[0].setgov({"ATTRIBUTES": {'v0/consortium/' + idBTC + '/members': '{"01":{"name":"account2BTC", \ + "ownerAddress":"' + account2 + '", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":100000.00000000, \ + "mintLimit":100000.00000000}, \ + "02":{"name":"account3BTC", \ + "ownerAddress":"' + account3 + '", \ + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", \ + "dailyMintLimit":400000.00000000, \ + "mintLimit":400000.00000000}}'}}) + self.nodes[0].generate(1) + + # Decrease limit + self.nodes[0].setgov({"ATTRIBUTES": {'v0/consortium/' + idBTC + '/members': '{"01":{"name":"account2BTC", \ + "ownerAddress":"' + account2 + '", \ + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", \ + "dailyMintLimit":1.00000000, \ + "mintLimit":1.00000000}, \ + "02":{"name":"account3BTC", \ + "ownerAddress":"' + account3 + '", \ + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", \ + "dailyMintLimit":1.00000000, \ + "mintLimit":1.00000000}}'}}) + +if __name__ == '__main__': + ConsortiumTest().main() diff --git a/test/functional/feature_loan_payback_dfi.py b/test/functional/feature_loan_payback_dfi.py index 127c2f42f0..8695dc48cd 100755 --- a/test/functional/feature_loan_payback_dfi.py +++ b/test/functional/feature_loan_payback_dfi.py @@ -150,7 +150,7 @@ def run_test(self): 'amounts': "1@DFI" }) - assert_raises_rpc_error(-5, 'Unrecognised type argument provided, valid types are: gov, locks, oracles, params, poolpairs, token,', + assert_raises_rpc_error(-5, 'Unrecognised type argument provided, valid types are: consortium, gov, locks, oracles, params, poolpairs, token,', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/live/economy/dfi_payback_tokens':'1'}}) # Disable loan payback diff --git a/test/functional/feature_loan_payback_dfi_v2.py b/test/functional/feature_loan_payback_dfi_v2.py index 797cc49c8c..50f443845d 100755 --- a/test/functional/feature_loan_payback_dfi_v2.py +++ b/test/functional/feature_loan_payback_dfi_v2.py @@ -298,7 +298,7 @@ def payback_with_DFI_prior_to_atribute_activation(self): assert("Payback of loan via DFI token is not currently active" in errorString) def setgov_attribute_to_false_and_payback(self): - assert_raises_rpc_error(-5, 'Unrecognised type argument provided, valid types are: gov, locks, oracles, params, poolpairs, token,', + assert_raises_rpc_error(-5, 'Unrecognised type argument provided, valid types are: consortium, gov, locks, oracles, params, poolpairs, token,', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/live/economy/dfi_payback_tokens':'1'}}) # Disable loan payback diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py old mode 100755 new mode 100644 index 7a041ff554..d63fdb728d --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -24,7 +24,6 @@ def set_test_params(self): ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-fortcanningspringheight=1250', '-grandcentralheight=1300', '-subsidytest=1'], ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1150', '-fortcanningcrunchheight=1200', '-fortcanningspringheight=1250', '-grandcentralheight=1300', '-subsidytest=1']] - def run_test(self): self.setup_tokens() @@ -444,8 +443,9 @@ 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: gov, locks, oracles, params, 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: consortium, gov, locks, oracles, 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: dex_in_fee_pct, dex_out_fee_pct, dfip2203, fixed_interval_price_id, loan_collateral_enabled, loan_collateral_factor, loan_minting_enabled, loan_minting_interest, loan_payback, loan_payback_collateral, loan_payback_fee_pct, payback_dfi, payback_dfi_fee_pct,", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/unrecognised':'true'}}) + assert_raises_rpc_error(-5, "Unrecognised key argument provided, valid keys are: members, mint_limit, mint_limit_daily,", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/5/unrecognised':'true'}}) assert_raises_rpc_error(-5, "Value must be an integer", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/not_a_number/payback_dfi':'true'}}) assert_raises_rpc_error(-5, 'Boolean 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, 'Boolean value must be either "true" or "false"', self.nodes[0].setgov, {"ATTRIBUTES":{'v0/token/5/payback_dfi':'unrecognised'}}) @@ -786,6 +786,13 @@ def run_test(self): assert_raises_rpc_error(-32600, "ATTRIBUTES: Cannot set block period while DFIP2203 is active", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2203/start_block':'0'}}) assert_raises_rpc_error(-5, "Boolean value must be either \"true\" or \"false\"", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2206a/dusd_interest_burn':'not_a_bool'}}) assert_raises_rpc_error(-5, "Boolean value must be either \"true\" or \"false\"", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/dfip2206a/dusd_loan_burn':'not_a_bool'}}) + assert_raises_rpc_error(-32600, "ATTRIBUTES: Cannot be set before GrandCentral", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000, \ + "dailyMintLimit": 1.0000000}}'}}) + assert_raises_rpc_error(-32600, "ATTRIBUTES: Cannot be set before GrandCentral", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/feature/consortium':'true'}}) + assert_raises_rpc_error(-32600, "ATTRIBUTES: Cannot be set before GrandCentral", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/mint_limit':'1000000000'}}) self.nodes[0].setgov({"ATTRIBUTES":{'v0/params/dfip2206a/dusd_interest_burn':'true', 'v0/params/dfip2206a/dusd_loan_burn':'true'}}) self.nodes[0].generate(1) @@ -870,9 +877,48 @@ def run_test(self): assert_raises_rpc_error(-32600, "called before GrandCentral height", self.nodes[0].unsetgov, {'ATTRIBUTES': ['v0/locks/token/4', 'v0/params/dfip2206f/active', 'v0/token/4/fixed_interval_price_id', 'v0/oracles/splits/4000', 'v0/poolpairs/3/token_a_fee_direction']}) assert_raises_rpc_error(-32600, "Cannot be set before GrandCentralHeight", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/params/feature/gov-unset':'true'}}) - # Move to GrandCentral fork + # Move to GrandCentral self.nodes[0].generate(1300 - self.nodes[0].getblockcount()) + assert_raises_rpc_error(-5, "Invalid ownerAddress in consortium member data", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000}}'}}) + assert_raises_rpc_error(-5, "Member name too short, must be more than 3 chars long", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"12", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000}}'}}) + assert_raises_rpc_error(-5, "Mint limit is an invalid amount", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":-10.00000000}}'}}) + assert_raises_rpc_error(-5, "Daily mint limit is an invalid amount", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000, \ + "dailyMintLimit":-10.00000000}}'}}) + assert_raises_rpc_error(-5, "Not a valid consortium member object", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test",} \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000}}'}}) + assert_raises_rpc_error(-5, "Status must be a positive number", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000, \ + "dailyMintLimit":1.00000000, \ + "status":-1}}'}}) + assert_raises_rpc_error(-5, "Status can be either 0 or 1", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", "ownerAddress":"' + owner +'", "backingId":"blablabla", "mintLimit":10.00000000, "dailyMintLimit":1.00000000, "status":2}}'}}) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/4/mint_limit' : '10', 'v0/consortium/4/mint_limit_daily' : '1'}}) + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/consortium/4/members' : '{"01":{"name":"test", \ + "ownerAddress":"' + owner +'", \ + "backingId":"blablabla", \ + "mintLimit":10.00000000, \ + "dailyMintLimit": 1.0000000}}'}}) + self.nodes[0].generate(1) + assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/gov/proposals/fee_burn_pct':'2'}}) assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/gov/proposals/fee_burn_pct':'101%'}}) assert_raises_rpc_error(-5, "Percentage exceeds 100%", self.nodes[0].setgov, {"ATTRIBUTES":{'v0/gov/proposals/cfp_fee':'101%'}}) diff --git a/test/functional/feature_skip_collateral_factor_check.py b/test/functional/feature_skip_collateral_factor_check.py old mode 100644 new mode 100755 diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 8288a7e4cf..b2fd41b6cd 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -192,6 +192,7 @@ 'feature_masternode_operator.py', 'feature_mine_cached.py', 'feature_mempool_dakota.py', + 'feature_consortium.py', 'interface_http.py', 'interface_http_cors.py', 'interface_rpc.py',