diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 090781b9dde..9e1fb65da45 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -94,7 +94,7 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value return Res::Err("Unsupported version"); } - if (keys.size() != 4 || keys[1].empty() || keys[2].empty() || keys[3].empty()) { + if (keys.size() < 4 || keys[1].empty() || keys[2].empty() || keys[3].empty()) { return Res::Err("Incorrect key for . Object of ['//ID/','value'] expected"); } @@ -135,6 +135,8 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value UniValue univalue; auto typeKey = itype->second; + uint32_t typeKeyId = 0; + CAttributeValue attribValue; if (type == AttributeTypes::Token) { @@ -149,6 +151,28 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value return std::move(res); } attribValue = *res.val; + } else if (typeKey == TokenKeys::LoanPayback + || typeKey == TokenKeys::LoanPaybackFeePCT) { + if (keys.size() != 5 || keys[4].empty()) { + return Res::Err("Exact 5 keys are required {%d}", keys.size()); + } + auto id = VerifyInt32(keys[4]); + if (!id) { + return std::move(id); + } + typeKeyId = *id.val; + if (typeKey == TokenKeys::LoanPayback) { + if (value != "true" && value != "false") { + return Res::Err("Payback token value must be either \"true\" or \"false\""); + } + attribValue = value == "true"; + } else { + auto res = VerifyPct(value); + if (!res) { + return std::move(res); + } + attribValue = *res.val; + } } else { return Res::Err("Unrecognised key"); } @@ -191,7 +215,7 @@ Res ATTRIBUTES::ProcessVariable(const std::string& key, const std::string& value } if (applyVariable) { - return applyVariable(CDataStructureV0{type, typeId, typeKey}, attribValue); + return applyVariable(CDataStructureV0{type, typeId, typeKey, typeKeyId}, attribValue); } return Res::Ok(); } @@ -211,6 +235,17 @@ Res ATTRIBUTES::Import(const UniValue & val) { if (attrV0->type == AttributeTypes::Live) { return Res::Err("Live attribute cannot be set externally"); } + // applay DFI via old keys + if (attrV0->IsExtendedSize() && attrV0->keyId == 0) { + auto newAttr = *attrV0; + if (attrV0->key == TokenKeys::LoanPayback) { + newAttr.key = TokenKeys::PaybackDFI; + } else { + newAttr.key = TokenKeys::PaybackDFIFeePCT; + } + attributes[newAttr] = value; + return Res::Ok(); + } } attributes[attribute] = value; return Res::Ok(); @@ -241,6 +276,10 @@ UniValue ATTRIBUTES::Export() const { id, displayKeys.at(attrV0->type).at(attrV0->key)); + if (attrV0->IsExtendedSize()) { + key = KeyBuilder(key, attrV0->keyId); + } + if (auto bool_val = boost::get(&attribute.second)) { ret.pushKV(key, *bool_val ? "true" : "false"); } else if (auto amount = boost::get(&attribute.second)) { @@ -248,6 +287,11 @@ UniValue ATTRIBUTES::Export() const { ret.pushKV(key, KeyBuilder(uvalue.get_real())); } else if (auto balances = boost::get(&attribute.second)) { ret.pushKV(key, AmountsToJSON(balances->balances)); + } else if (auto paybacks = boost::get(&attribute.second)) { + UniValue result(UniValue::VOBJ); + result.pushKV("paybackfees", AmountsToJSON(paybacks->tokensFee.balances)); + result.pushKV("paybacktokens", AmountsToJSON(paybacks->tokensPayback.balances)); + ret.pushKV(key, result); } } catch (const std::out_of_range&) { // Should not get here, that's mean maps are mismatched @@ -274,6 +318,19 @@ Res ATTRIBUTES::Validate(const CCustomCSView & view) const if (!view.GetLoanTokenByID(DCT_ID{tokenId})) { return Res::Err("No such loan token (%d)", tokenId); } + } else if (attrV0->key == TokenKeys::LoanPayback + || attrV0->key == TokenKeys::LoanPaybackFeePCT) { + if (view.GetLastHeight() < Params().GetConsensus().FortCanningRoadHeight) { + return Res::Err("Cannot be set before FortCanningRoad"); + } + uint32_t loanTokenId = attrV0->typeId; + if (!view.GetLoanTokenByID(DCT_ID{loanTokenId})) { + return Res::Err("No such loan token (%d)", loanTokenId); + } + uint32_t tokenId = attrV0->keyId; + if (!view.GetToken(DCT_ID{tokenId})) { + return Res::Err("No such token (%d)", tokenId); + } } else { return Res::Err("Unsupported key"); } diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index bb97f1edc90..7c7c26d069b 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -27,6 +27,7 @@ enum ParamIDs : uint8_t { enum EconomyKeys : uint8_t { PaybackDFITokens = 'a', + PaybackTokens = 'b', }; enum DFIP2201Keys : uint8_t { @@ -36,8 +37,10 @@ enum DFIP2201Keys : uint8_t { }; enum TokenKeys : uint8_t { - PaybackDFI = 'a', - PaybackDFIFeePCT = 'b', + PaybackDFI = 'a', + PaybackDFIFeePCT = 'b', + LoanPayback = 'c', + LoanPaybackFeePCT = 'd', }; enum PoolKeys : uint8_t { @@ -49,6 +52,7 @@ struct CDataStructureV0 { uint8_t type; uint32_t typeId; uint8_t key; + uint32_t keyId; ADD_SERIALIZE_METHODS; @@ -57,10 +61,21 @@ struct CDataStructureV0 { READWRITE(type); READWRITE(typeId); READWRITE(key); + if (IsExtendedSize()) { + READWRITE(keyId); + } else { + keyId = 0; + } + } + + bool IsExtendedSize() const { + return type == AttributeTypes::Token + && (key == TokenKeys::LoanPayback + || key == TokenKeys::LoanPaybackFeePCT); } bool operator<(const CDataStructureV0& o) const { - return std::tie(type, typeId, key) < std::tie(o.type, o.typeId, o.key); + return std::tie(type, typeId, key, keyId) < std::tie(o.type, o.typeId, o.key, o.keyId); } }; @@ -74,8 +89,21 @@ struct CDataStructureV1 { bool operator<(const CDataStructureV1& o) const { return false; } }; +struct CTokenPayback { + CBalances tokensFee; + CBalances tokensPayback; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(tokensFee); + READWRITE(tokensPayback); + } +}; + using CAttributeType = boost::variant; -using CAttributeValue = boost::variant; +using CAttributeValue = boost::variant; class ATTRIBUTES : public GovVariable, public AutoRegistrator { @@ -136,6 +164,8 @@ class ATTRIBUTES : public GovVariable, public AutoRegistrator loans; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(vaultId); + READWRITE(from); + READWRITE(loans); + } +}; + class CLoanView : public virtual CStorageView { public: using CLoanSetCollateralTokenImpl = CLoanSetCollateralTokenImplementation; diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 6dc9ec5f5af..3124aef572e 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -76,6 +76,7 @@ std::string ToString(CustomTxType type) { case CustomTxType::WithdrawFromVault: return "WithdrawFromVault"; case CustomTxType::TakeLoan: return "TakeLoan"; case CustomTxType::PaybackLoan: return "PaybackLoan"; + case CustomTxType::PaybackLoanV2: return "PaybackLoanV2"; case CustomTxType::AuctionBid: return "AuctionBid"; case CustomTxType::Reject: return "Reject"; case CustomTxType::None: return "None"; @@ -157,6 +158,7 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { case CustomTxType::WithdrawFromVault: return CWithdrawFromVaultMessage{}; case CustomTxType::TakeLoan: return CLoanTakeLoanMessage{}; case CustomTxType::PaybackLoan: return CLoanPaybackLoanMessage{}; + case CustomTxType::PaybackLoanV2: return CLoanPaybackLoanV2Message{}; case CustomTxType::AuctionBid: return CAuctionBidMessage{}; case CustomTxType::Reject: return CCustomTxMessageNone{}; case CustomTxType::None: return CCustomTxMessageNone{}; @@ -221,6 +223,13 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return Res::Ok(); } + Res isPostFortCanningRoadFork() const { + if(static_cast(height) < consensus.FortCanningRoadHeight) { + return Res::Err("called before FortCanningRoad height"); + } + return Res::Ok(); + } + template Res serialize(T& obj) const { CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); @@ -543,6 +552,11 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return !res ? res : serialize(obj); } + Res operator()(CLoanPaybackLoanV2Message& obj) const { + auto res = isPostFortCanningRoadFork(); + return !res ? res : serialize(obj); + } + Res operator()(CAuctionBidMessage& obj) const { auto res = isPostFortCanningFork(); return !res ? res : serialize(obj); @@ -2647,7 +2661,11 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!collateralsLoans) return std::move(collateralsLoans); + if (collateralsLoans.val->ratio() < scheme->ratio) + return Res::Err("Vault does not have enough collateralization ratio defined by loan scheme - %d < %d", collateralsLoans.val->ratio(), scheme->ratio); + uint64_t totalCollaterals = 0; + for (auto& col : collateralsLoans.val->collaterals) if (col.nTokenId == DCT_ID{0} || (tokenDUSD && col.nTokenId == tokenDUSD->first)) @@ -2661,9 +2679,6 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return static_cast(height) < consensus.FortCanningRoadHeight ? Res::Err("At least 50%% of the minimum required collateral must be in DFI") : Res::Err("At least 50%% of the minimum required collateral must be in DFI or DUSD"); } - - if (collateralsLoans.val->ratio() < scheme->ratio) - return Res::Err("Vault does not have enough collateralization ratio defined by loan scheme - %d < %d", collateralsLoans.val->ratio(), scheme->ratio); } } else @@ -2764,7 +2779,11 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!collateralsLoans) return std::move(collateralsLoans); + if (collateralsLoans.val->ratio() < scheme->ratio) + return Res::Err("Vault does not have enough collateralization ratio defined by loan scheme - %d < %d", collateralsLoans.val->ratio(), scheme->ratio); + uint64_t totalCollaterals = 0; + for (auto& col : collateralsLoans.val->collaterals) if (col.nTokenId == DCT_ID{0} || (tokenDUSD && col.nTokenId == tokenDUSD->first)) @@ -2778,15 +2797,36 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return static_cast(height) < consensus.FortCanningRoadHeight ? Res::Err("At least 50%% of the minimum required collateral must be in DFI when taking a loan.") : Res::Err("At least 50%% of the minimum required collateral must be in DFI or DUSD when taking a loan."); } - - if (collateralsLoans.val->ratio() < scheme->ratio) - return Res::Err("Vault does not have enough collateralization ratio defined by loan scheme - %d < %d", collateralsLoans.val->ratio(), scheme->ratio); } - return Res::Ok(); } Res operator()(const CLoanPaybackLoanMessage& obj) const { + std::map loans; + for (auto& balance: obj.amounts.balances) { + CBalances amounts; + auto id = balance.first; + auto amount = balance.second; + + amounts.Add({id, amount}); + if (id == DCT_ID{0}) + { + auto tokenDUSD = mnview.GetToken("DUSD"); + if (tokenDUSD) + loans[tokenDUSD->first] = amounts; + } + else + loans[id] = amounts; + } + return (*this)( + CLoanPaybackLoanV2Message{ + obj.vaultId, + obj.from, + loans + }); + } + + Res operator()(const CLoanPaybackLoanV2Message& obj) const { auto res = CheckCustomTx(); if (!res) return res; @@ -2801,158 +2841,218 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!mnview.GetVaultCollaterals(obj.vaultId)) return Res::Err("Vault with id %s has no collaterals", obj.vaultId.GetHex()); + auto loanAmounts = mnview.GetLoanTokens(obj.vaultId); + if (!loanAmounts) + return Res::Err("There are no loans on this vault (%s)!", obj.vaultId.GetHex()); + if (!HasAuth(obj.from)) return Res::Err("tx must have at least one input from token owner"); - if (!IsVaultPriceValid(mnview, obj.vaultId, height)) + if (static_cast(height) < consensus.FortCanningRoadHeight && !IsVaultPriceValid(mnview, obj.vaultId, height)) return Res::Err("Cannot payback loan while any of the asset's price is invalid"); - auto penaltyPct = COIN; - auto allowDFIPayback = false; auto shouldSetVariable = false; - auto tokenDUSD = mnview.GetToken("DUSD"); auto attributes = mnview.GetAttributes(); - if (tokenDUSD && attributes) - { - CDataStructureV0 activeKey{AttributeTypes::Token, tokenDUSD->first.v, TokenKeys::PaybackDFI}; - allowDFIPayback = attributes->GetValue(activeKey, false); - } - for (const auto& kv : obj.amounts.balances) + for (const auto& idx : obj.loans) { - DCT_ID tokenId = kv.first; - auto paybackAmount = kv.second; - CAmount dfiUSDPrice{0}; - - if (height >= Params().GetConsensus().FortCanningHillHeight && kv.first == DCT_ID{0}) - { - if (!allowDFIPayback || !tokenDUSD) - return Res::Err("Payback of DUSD loans with DFI not currently active"); + DCT_ID loanTokenId = idx.first; + auto loanToken = mnview.GetLoanTokenByID(loanTokenId); + if (!loanToken) + return Res::Err("Loan token with id (%s) does not exist!", loanTokenId.ToString()); - // Get DFI price in USD - const CTokenCurrencyPair dfiUsd{"DFI","USD"}; - bool useNextPrice{false}, requireLivePrice{true}; - const auto resVal = mnview.GetValidatedIntervalPrice(dfiUsd, useNextPrice, requireLivePrice); - if (!resVal) - return std::move(resVal); + auto it = loanAmounts->balances.find(loanTokenId); + if (it == loanAmounts->balances.end()) + return Res::Err("There is no loan on token (%s) in this vault!", loanToken->symbol); - // Apply penalty - CDataStructureV0 penaltyKey{AttributeTypes::Token, tokenDUSD->first.v, TokenKeys::PaybackDFIFeePCT}; - penaltyPct -= attributes->GetValue(penaltyKey, COIN / 100); + for (const auto& kv : idx.second.balances) + { + DCT_ID paybackTokenId = kv.first; + auto paybackAmount = kv.second; + CAmount paybackUsdPrice{0}, loanUsdPrice{0}, penaltyPct{COIN}; - dfiUSDPrice = MultiplyAmounts(*resVal.val, penaltyPct); + auto paybackToken = mnview.GetToken(paybackTokenId); + if (!paybackToken) + return Res::Err("Token with id (%s) does not exists", paybackTokenId.ToString()); - // Set tokenId to DUSD - tokenId = tokenDUSD->first; + if (loanTokenId != paybackTokenId) + { + if (!IsVaultPriceValid(mnview, obj.vaultId, height)) + return Res::Err("Cannot payback loan while any of the asset's price is invalid"); - // Calculate the DFI amount in DUSD - paybackAmount = MultiplyAmounts(dfiUSDPrice, kv.second); - if (dfiUSDPrice > COIN && paybackAmount < kv.second) - return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(kv.second), GetDecimaleString(dfiUSDPrice)); - } + if (!attributes) + return Res::Err("Payback is not currently active"); - auto loanToken = mnview.GetLoanTokenByID(tokenId); - if (!loanToken) - return Res::Err("Loan token with id (%s) does not exist!", tokenId.ToString()); + // search in token to token + if (paybackTokenId != DCT_ID{0}) + { + CDataStructureV0 activeKey{AttributeTypes::Token, loanTokenId.v, TokenKeys::LoanPayback, paybackTokenId.v}; + if (!attributes->GetValue(activeKey, false)) + return Res::Err("Payback of loan via %s token is not currently active", paybackToken->symbol); - auto loanAmounts = mnview.GetLoanTokens(obj.vaultId); - if (!loanAmounts) - return Res::Err("There are no loans on this vault (%s)!", obj.vaultId.GetHex()); + CDataStructureV0 penaltyKey{AttributeTypes::Token, loanTokenId.v, TokenKeys::LoanPaybackFeePCT, paybackTokenId.v}; + penaltyPct -= attributes->GetValue(penaltyKey, CAmount{0}); + } + else + { + CDataStructureV0 activeKey{AttributeTypes::Token, loanTokenId.v, TokenKeys::PaybackDFI}; + if (!attributes->GetValue(activeKey, false)) + return Res::Err("Payback of loan via %s token is not currently active", paybackToken->symbol); - auto it = loanAmounts->balances.find(tokenId); - if (it == loanAmounts->balances.end()) - return Res::Err("There is no loan on token (%s) in this vault!", loanToken->symbol); + CDataStructureV0 penaltyKey{AttributeTypes::Token, loanTokenId.v, TokenKeys::PaybackDFIFeePCT}; + penaltyPct -= attributes->GetValue(penaltyKey, COIN / 100); - auto rate = mnview.GetInterestRate(obj.vaultId, tokenId, height); - if (!rate) - return Res::Err("Cannot get interest rate for this token (%s)!", loanToken->symbol); + } - LogPrint(BCLog::LOAN,"CLoanPaybackLoanMessage()->%s->", loanToken->symbol); /* Continued */ - auto subInterest = TotalInterest(*rate, height); - auto subLoan = paybackAmount - subInterest; + // Get token price in USD + const CTokenCurrencyPair tokenUsdPair{paybackToken->symbol,"USD"}; + bool useNextPrice{false}, requireLivePrice{true}; + const auto resVal = mnview.GetValidatedIntervalPrice(tokenUsdPair, useNextPrice, requireLivePrice); + if (!resVal) + return std::move(resVal); - if (paybackAmount < subInterest) - { - subInterest = paybackAmount; - subLoan = 0; - } - else if (it->second - subLoan < 0) - { - subLoan = it->second; - } + paybackUsdPrice = MultiplyAmounts(*resVal.val, penaltyPct); - res = mnview.SubLoanToken(obj.vaultId, CTokenAmount{tokenId, subLoan}); - if (!res) - return res; + // Calculate the DFI amount in DUSD + auto usdAmount = MultiplyAmounts(paybackUsdPrice, kv.second); - LogPrint(BCLog::LOAN,"CLoanPaybackLoanMessage()->%s->", loanToken->symbol); /* Continued */ - res = mnview.EraseInterest(height, obj.vaultId, vault->schemeId, tokenId, subLoan, subInterest); - if (!res) - return res; + if (loanToken->symbol == "DUSD") + { + paybackAmount = usdAmount; + if (paybackUsdPrice > COIN && paybackAmount < kv.second) + return Res::Err("Value/price too high (%s/%s)", GetDecimaleString(kv.second), GetDecimaleString(paybackUsdPrice)); + } + else + { + // Get dToken price in USD + const CTokenCurrencyPair dTokenUsdPair{loanToken->symbol, "USD"}; + bool useNextPrice{false}, requireLivePrice{true}; + const auto resVal = mnview.GetValidatedIntervalPrice(dTokenUsdPair, useNextPrice, requireLivePrice); + if (!resVal) + return std::move(resVal); + + loanUsdPrice = *resVal.val; + + paybackAmount = DivideAmounts(usdAmount, loanUsdPrice); + } + } - if (static_cast(height) >= consensus.FortCanningMuseumHeight && subLoan < it->second) - { - auto newRate = mnview.GetInterestRate(obj.vaultId, tokenId, height); - if (!newRate) + auto rate = mnview.GetInterestRate(obj.vaultId, loanTokenId, height); + if (!rate) return Res::Err("Cannot get interest rate for this token (%s)!", loanToken->symbol); - if (newRate->interestPerBlock == 0) - return Res::Err("Cannot payback this amount of loan for %s, either payback full amount or less than this amount!", loanToken->symbol); - } + LogPrint(BCLog::LOAN,"CLoanPaybackLoanMessage()->%s->", loanToken->symbol); /* Continued */ + auto subInterest = TotalInterest(*rate, height); + auto subLoan = paybackAmount - subInterest; - CalculateOwnerRewards(obj.from); + if (paybackAmount < subInterest) + { + subInterest = paybackAmount; + subLoan = 0; + } + else if (it->second - subLoan < 0) + { + subLoan = it->second; + } - if (height < Params().GetConsensus().FortCanningHillHeight || kv.first != DCT_ID{0}) - { - res = mnview.SubMintedTokens(loanToken->creationTx, subLoan); + res = mnview.SubLoanToken(obj.vaultId, CTokenAmount{loanTokenId, subLoan}); if (!res) return res; - // subtract loan amount first, interest is burning below - LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Sub loan from balance - %lld, height - %d\n", subLoan, height); - res = mnview.SubBalance(obj.from, CTokenAmount{tokenId, subLoan}); + LogPrint(BCLog::LOAN,"CLoanPaybackLoanMessage()->%s->", loanToken->symbol); /* Continued */ + res = mnview.EraseInterest(height, obj.vaultId, vault->schemeId, loanTokenId, subLoan, subInterest); if (!res) return res; - // burn interest Token->USD->DFI->burnAddress - if (subInterest) + if (static_cast(height) >= consensus.FortCanningMuseumHeight && subLoan < it->second) { - LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Swapping %s interest to DFI - %lld, height - %d\n", loanToken->symbol, subInterest, height); - res = SwapToDFIOverUSD(mnview, tokenId, subInterest, obj.from, consensus.burnAddress, height); + auto newRate = mnview.GetInterestRate(obj.vaultId, loanTokenId, height); + if (!newRate) + return Res::Err("Cannot get interest rate for this token (%s)!", loanToken->symbol); + + if (newRate->interestPerBlock == 0) + return Res::Err("Cannot payback this amount of loan for %s, either payback full amount or less than this amount!", loanToken->symbol); } - } - else - { - CAmount subInDFI; - auto subAmount = subLoan + subInterest; - // if DFI payback overpay loan and interest amount - if (paybackAmount > subAmount) + + CalculateOwnerRewards(obj.from); + + if (paybackTokenId == loanTokenId) { - subInDFI = DivideAmounts(subAmount, dfiUSDPrice); - if (MultiplyAmounts(subInDFI, dfiUSDPrice) != subAmount) - subInDFI += 1; + res = mnview.SubMintedTokens(loanToken->creationTx, subLoan); + if (!res) + return res; + + // subtract loan amount first, interest is burning below + LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Sub loan from balance - %lld, height - %d\n", subLoan, height); + res = mnview.SubBalance(obj.from, CTokenAmount{loanTokenId, subLoan}); + if (!res) + return res; + + // burn interest Token->USD->DFI->burnAddress + if (subInterest) + { + LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Swapping %s interest to DFI - %lld, height - %d\n", loanToken->symbol, subInterest, height); + res = SwapToDFIOverUSD(mnview, loanTokenId, subInterest, obj.from, consensus.burnAddress, height); + } } else { - subInDFI = kv.second; - } + CAmount subInToken; + auto subAmount = subLoan + subInterest; + + // if payback overpay loan and interest amount + if (paybackAmount > subAmount) + { + if (loanToken->symbol == "DUSD") + { + subInToken = DivideAmounts(subAmount, paybackUsdPrice); + if (MultiplyAmounts(subInToken, paybackUsdPrice) != subAmount) + subInToken += 1; + } + else + { + auto tempAmount = MultiplyAmounts(subAmount, loanUsdPrice); + + subInToken = DivideAmounts(tempAmount, paybackUsdPrice); + if (DivideAmounts(MultiplyAmounts(subInToken, paybackUsdPrice), loanUsdPrice) != subAmount) + subInToken += 1; + } + } + else + { + subInToken = kv.second; + } - CDataStructureV0 liveKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::PaybackDFITokens}; - auto balances = attributes->GetValue(liveKey, CBalances{}); - auto penaltyDFI = MultiplyAmounts(subInDFI, COIN - penaltyPct); + auto penalty = MultiplyAmounts(subInToken, COIN - penaltyPct); - balances.Add(CTokenAmount{tokenId, subAmount}); - balances.Add(CTokenAmount{DCT_ID{0}, penaltyDFI}); - attributes->attributes[liveKey] = balances; + if (paybackTokenId == DCT_ID{0}) + { + CDataStructureV0 liveKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::PaybackDFITokens}; + auto balances = attributes->GetValue(liveKey, CBalances{}); - shouldSetVariable = true; + balances.Add(CTokenAmount{loanTokenId, subAmount}); + balances.Add(CTokenAmount{paybackTokenId, penalty}); + attributes->attributes[liveKey] = balances; + } + else + { + CDataStructureV0 liveKey{AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::PaybackTokens}; + auto balances = attributes->GetValue(liveKey, CTokenPayback{}); + + balances.tokensPayback.Add(CTokenAmount{loanTokenId, subAmount}); + balances.tokensFee.Add(CTokenAmount{paybackTokenId, penalty}); + attributes->attributes[liveKey] = balances; + } - LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Burning interest and loan in DFI directly - %lld (%lld DFI), height - %d\n", subLoan + subInterest, subInDFI, height); - res = TransferTokenBalance(DCT_ID{0}, subInDFI, obj.from, consensus.burnAddress); - } + shouldSetVariable = true; - if (!res) - return res; + LogPrint(BCLog::LOAN, "CLoanPaybackLoanMessage(): Burning interest and loan in %s directly - %lld (%lld %s), height - %d\n", paybackToken->symbol, subLoan + subInterest, subInToken, paybackToken->symbol, height); + res = TransferTokenBalance(paybackTokenId, subInToken, obj.from, consensus.burnAddress); + } + + if (!res) + return res; + } } return shouldSetVariable ? mnview.SetVariable(*attributes) : Res::Ok(); diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index b0af9d31e28..b2f48d3a45d 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -99,6 +99,7 @@ enum class CustomTxType : uint8_t WithdrawFromVault = 'J', TakeLoan = 'X', PaybackLoan = 'H', + PaybackLoanV2 = 'k', AuctionBid = 'I' }; @@ -152,6 +153,7 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::WithdrawFromVault: case CustomTxType::TakeLoan: case CustomTxType::PaybackLoan: + case CustomTxType::PaybackLoanV2: case CustomTxType::AuctionBid: case CustomTxType::Reject: case CustomTxType::None: @@ -368,6 +370,7 @@ typedef boost::variant< CWithdrawFromVaultMessage, CLoanTakeLoanMessage, CLoanPaybackLoanMessage, + CLoanPaybackLoanV2Message, CAuctionBidMessage > CCustomTxMessage; diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index 4081456c243..13e5b48f5d8 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -1773,6 +1773,8 @@ UniValue getburninfo(const JSONRPCRequest& request) { CAmount dfiPaybackFee{0}; CBalances burntTokens; CBalances dexfeeburn; + CBalances paybackfees; + CBalances paybacktokens; UniValue dfipaybacktokens{UniValue::VARR}; LOCK(cs_main); @@ -1859,11 +1861,18 @@ UniValue getburninfo(const JSONRPCRequest& request) { dfipaybacktokens.push_back(tokenAmountString({balance.first, balance.second})); } } + liveKey = {AttributeTypes::Live, ParamIDs::Economy, EconomyKeys::PaybackTokens}; + auto paybacks = attributes->GetValue(liveKey, CTokenPayback{}); + paybackfees = std::move(paybacks.tokensFee); + paybacktokens = std::move(paybacks.tokensPayback); } result.pushKV("dfipaybackfee", ValueFromAmount(dfiPaybackFee)); result.pushKV("dfipaybacktokens", dfipaybacktokens); + result.pushKV("paybackfees", AmountsToJSON(paybackfees.balances)); + result.pushKV("paybacktokens", AmountsToJSON(paybacktokens.balances)); + CAmount burnt{0}; for (const auto& kv : Params().GetConsensus().newNonUTXOSubsidies) { if (kv.first == CommunityAccountType::Unallocated || kv.first == CommunityAccountType::IncentiveFunding || diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index 9ff0e4ca904..cd62635cb17 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -439,6 +439,31 @@ class CCustomTxRpcVisitor : public boost::static_visitor } } + void operator()(const CLoanPaybackLoanV2Message& obj) const { + rpcInfo.pushKV("vaultId", obj.vaultId.GetHex()); + rpcInfo.pushKV("from", ScriptToString(obj.from)); + UniValue loans{UniValue::VARR}; + for (auto const & idx : obj.loans) { + UniValue loan{UniValue::VOBJ}; + if (auto dtoken = mnview.GetToken(idx.first)) { + auto dtokenImpl = static_cast(*dtoken); + if (auto dtokenPair = mnview.GetTokenByCreationTx(dtokenImpl.creationTx)) { + loan.pushKV("dToken",dtokenPair->first.ToString()); + } + } + for (auto const & kv : idx.second.balances) { + if (auto token = mnview.GetToken(kv.first)) { + auto tokenImpl = static_cast(*token); + if (auto tokenPair = mnview.GetTokenByCreationTx(tokenImpl.creationTx)) { + loan.pushKV(tokenPair->first.ToString(), ValueFromAmount(kv.second)); + } + } + } + loans.push_back(loan); + } + rpcInfo.pushKV("dToken",loans); + } + void operator()(const CAuctionBidMessage& obj) const { rpcInfo.pushKV("vaultId", obj.vaultId.GetHex()); rpcInfo.pushKV("index", int64_t(obj.index)); diff --git a/src/masternodes/rpc_loan.cpp b/src/masternodes/rpc_loan.cpp index 587abc41d15..11a81827873 100644 --- a/src/masternodes/rpc_loan.cpp +++ b/src/masternodes/rpc_loan.cpp @@ -1123,7 +1123,17 @@ UniValue paybackloan(const JSONRPCRequest& request) { { {"vaultId", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Id of vault used for loan"}, {"from", RPCArg::Type::STR, RPCArg::Optional::NO, "Address containing repayment tokens. If \"from\" value is: \"*\" (star), it's means auto-selection accounts from wallet."}, - {"amounts", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount in amount@token format."}, + {"amounts", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Amount in amount@token format."}, + {"loans", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, "A json array of json objects", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"dToken", RPCArg::Type::STR, RPCArg::Optional::NO, "The dTokens's symbol, id or creation tx"}, + {"amounts", RPCArg::Type::STR, RPCArg::Optional::NO, "Amount in amount@token format."}, + }, + }, + }, + }, }, }, {"inputs", RPCArg::Type::ARR, RPCArg::Optional::OMITTED_NAMED_ARG, @@ -1156,70 +1166,106 @@ UniValue paybackloan(const JSONRPCRequest& request) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameters, argument 1 must be non-null and expected as object at least with " "{\"vaultId\",\"amounts\"}"); - UniValue metaObj = request.params[0].get_obj(); - UniValue const & txInputs = request.params[1]; - - CLoanPaybackLoanMessage loanPayback; - if (!metaObj["vaultId"].isNull()) - loanPayback.vaultId = uint256S(metaObj["vaultId"].getValStr()); - else + if (metaObj["vaultId"].isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"vaultId\" must be non-null"); + auto vaultId = uint256S(metaObj["vaultId"].getValStr()); - if (!metaObj["amounts"].isNull()) - loanPayback.amounts = DecodeAmounts(pwallet->chain(), metaObj["amounts"], ""); - else - throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"amounts\" must not be null"); - - if (metaObj["from"].isNull()) { + if (metaObj["from"].isNull()) throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"from\" must not be null"); + auto fromStr = metaObj["from"].getValStr(); + + // Check amounts or/and loans + bool hasAmounts = !metaObj["amounts"].isNull(); + bool hasLoans = !metaObj["loans"].isNull(); + int targetHeight; + { + LOCK(cs_main); + targetHeight = ::ChainActive().Height() + 1; + } + bool isFCR = targetHeight >= Params().GetConsensus().FortCanningRoadHeight; + CBalances amounts; + if (hasAmounts){ + if(hasLoans) + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"amounts\" and \"loans\" cannot be set at the same time"); + else + amounts = DecodeAmounts(pwallet->chain(), metaObj["amounts"], ""); + } + else if(!isFCR) + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"amounts\" must not be null"); + else if(!hasLoans) + throw JSONRPCError(RPC_INVALID_PARAMETER,"Invalid parameters, argument \"amounts\" and \"loans\" cannot be empty at the same time"); + + std::map loans; + UniValue array {UniValue::VARR}; + if(hasLoans) { + try { + array = metaObj["loans"].get_array(); + for (unsigned int i=0; iGetTokenGuessId(tokenStr, id); + if (!token) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Token %s does not exist!", tokenStr)); + + if (!token->IsLoanToken()) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Token %s is not a loan token!", tokenStr)); + + auto loanToken = pcustomcsview->GetLoanTokenByID(id); + if (!loanToken) + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Can't find %s loan token!", tokenStr)); + loans[id] = DecodeAmounts(pwallet->chain(), obj["amounts"], ""); + } + }catch(std::runtime_error& e) { + throw JSONRPCError(RPC_INVALID_PARAMETER, e.what()); + } } - auto fromStr = metaObj["from"].getValStr(); + CScript from; if (fromStr == "*") { - auto selectedAccounts = SelectAccountsByTargetBalances(GetAllMineAccounts(pwallet), loanPayback.amounts, SelectionPie); + CBalances balances; + for (const auto& amounts : loans) + balances.AddBalances(amounts.second.balances); + + if (loans.empty()) + balances = amounts; + + auto selectedAccounts = SelectAccountsByTargetBalances(GetAllMineAccounts(pwallet), balances, SelectionPie); for (auto& account : selectedAccounts) { - auto it = loanPayback.amounts.balances.begin(); - while (it != loanPayback.amounts.balances.end()) { - if (account.second.balances[it->first] < it->second) { + auto it = amounts.balances.begin(); + while (it != amounts.balances.end()) { + if (account.second.balances[it->first] < it->second) break; - } it++; } - if (it == loanPayback.amounts.balances.end()) { - loanPayback.from = account.first; + if (it == amounts.balances.end()) { + from = account.first; break; } } - if (loanPayback.from.empty()) { + if (from.empty()) throw JSONRPCError(RPC_INVALID_REQUEST, "Not enough tokens on account, call sendtokenstoaddress to increase it.\n"); - } - } else { - loanPayback.from = DecodeScript(fromStr); - } + } else + from = DecodeScript(metaObj["from"].getValStr()); - if (!::IsMine(*pwallet, loanPayback.from)) + if (!::IsMine(*pwallet, from)) throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Address (%s) is not owned by the wallet", metaObj["from"].getValStr())); - - int targetHeight; - { - LOCK(cs_main); - targetHeight = ::ChainActive().Height() + 1; - // Get vault if exists, vault owner used as auth. - auto vault = pcustomcsview->GetVault(loanPayback.vaultId); - if (!vault) { - throw JSONRPCError(RPC_INVALID_PARAMETER,"Cannot find existing vault with id " + loanPayback.vaultId.GetHex()); - } - } + strprintf("Address (%s) is not owned by the wallet", metaObj["from"].getValStr())); CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); - metadata << static_cast(CustomTxType::PaybackLoan) - << loanPayback; + + if (!hasAmounts) + metadata << static_cast(CustomTxType::PaybackLoanV2) + << CLoanPaybackLoanV2Message{vaultId, from, loans}; + else + metadata << static_cast(CustomTxType::PaybackLoan) + << CLoanPaybackLoanMessage{vaultId, from, amounts}; CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); @@ -1228,7 +1274,8 @@ UniValue paybackloan(const JSONRPCRequest& request) { CMutableTransaction rawTx(txVersion); CTransactionRef optAuthTx; - std::set auths{loanPayback.from}; + std::set auths{from}; + UniValue const & txInputs = request.params[1]; rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false, optAuthTx, txInputs); rawTx.vout.emplace_back(0, scriptMeta); diff --git a/test/functional/feature_loan_payback_dfi.py b/test/functional/feature_loan_payback_dfi.py index 4d85026a916..07565fbf7f3 100755 --- a/test/functional/feature_loan_payback_dfi.py +++ b/test/functional/feature_loan_payback_dfi.py @@ -138,7 +138,7 @@ def run_test(self): }) # Should not be able to payback loan before DFI payback enabled - assert_raises_rpc_error(-32600, "Payback of DUSD loans with DFI not currently active", self.nodes[0].paybackloan, { + assert_raises_rpc_error(-32600, "Payback of loan via DFI token is not currently active", self.nodes[0].paybackloan, { 'vaultId': vaultId, 'from': account0, 'amounts': "1@DFI" @@ -152,7 +152,7 @@ def run_test(self): self.nodes[0].generate(1) # Should not be able to payback loan before DFI payback enabled - assert_raises_rpc_error(-32600, "Payback of DUSD loans with DFI not currently active", self.nodes[0].paybackloan, { + assert_raises_rpc_error(-32600, "Payback of loan via DFI token is not currently active", self.nodes[0].paybackloan, { 'vaultId': vaultId, 'from': account0, 'amounts': "1@DFI" diff --git a/test/functional/feature_loan_payback_dfi_v2.py b/test/functional/feature_loan_payback_dfi_v2.py new file mode 100755 index 00000000000..ae7188f54d5 --- /dev/null +++ b/test/functional/feature_loan_payback_dfi_v2.py @@ -0,0 +1,698 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-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 - payback loan dfi.""" + +from test_framework.test_framework import DefiTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error +from test_framework.authproxy import JSONRPCException + +import calendar +import time +from decimal import Decimal, ROUND_UP + + +class PaybackDFILoanTest (DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [ + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-bayfrontgardensheight=1', '-eunosheight=50', + '-fortcanningheight=50', '-fortcanninghillheight=50', '-fortcanningroadheight=50', '-simulatemainnet', '-txindex=1', '-jellyfish_regtest=1'] + ] + self.symbolDFI = "DFI" + self.symbolBTC = "BTC" + self.symboldUSD = "DUSD" + self.symbolTSLA = "TSLA" + + def create_tokens(self): + self.nodes[0].createtoken({ + "symbol": self.symbolBTC, + "name": "BTC token", + "isDAT": True, + "collateralAddress": self.account0 + }) + self.nodes[0].generate(1) + self.idDFI = list(self.nodes[0].gettoken(self.symbolDFI).keys())[0] + self.idBTC = list(self.nodes[0].gettoken(self.symbolBTC).keys())[0] + self.nodes[0].utxostoaccount({self.account0: "6000000@" + self.symbolDFI}) + self.nodes[0].generate(1) + + + def setup_oracles(self): + self.oracle_address1 = self.nodes[0].getnewaddress("", "legacy") + price_feeds1 = [ + {"currency": "USD", "token": "DFI"}, + {"currency": "USD", "token": "BTC"}, + {"currency": "USD", "token": "TSLA"} + ] + self.oracle_id1 = self.nodes[0].appointoracle( + self.oracle_address1, price_feeds1, 10) + self.nodes[0].generate(1) + + # feed oracle + oracle1_prices = [ + {"currency": "USD", "tokenAmount": "0.005@TSLA"}, + {"currency": "USD", "tokenAmount": "4@DFI"}, + {"currency": "USD", "tokenAmount": "50000@BTC"} + ] + timestamp = calendar.timegm(time.gmtime()) + self.nodes[0].setoracledata(self.oracle_id1, timestamp, oracle1_prices) + self.nodes[0].generate(120) # make prices active + + def setup_loan_tokens(self): + self.nodes[0].setcollateraltoken({ + 'token': self.idDFI, + 'factor': 1, + 'fixedIntervalPriceId': "DFI/USD" + }) + self.nodes[0].generate(1) + + self.nodes[0].setcollateraltoken({ + 'token': self.idBTC, + 'factor': 1, + 'fixedIntervalPriceId': "BTC/USD"}) + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': self.symbolTSLA, + 'name': "Tesla stock token", + 'fixedIntervalPriceId': "TSLA/USD", + 'mintable': True, + 'interest': 1 + }) + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': self.symboldUSD, + 'name': "DUSD stable token", + 'fixedIntervalPriceId': "DUSD/USD", + 'mintable': True, + 'interest': 1 + }) + self.nodes[0].generate(1) + self.nodes[0].minttokens("70000000@DUSD") + self.nodes[0].minttokens("500@BTC") + self.nodes[0].minttokens("5000000000@TSLA") + self.nodes[0].generate(1) + self.iddUSD = list(self.nodes[0].gettoken(self.symboldUSD).keys())[0] + self.idTSLA = list(self.nodes[0].gettoken(self.symbolTSLA).keys())[0] + + def create_fill_addresses(self): + # Fill LM account DFI-DUSD + # 5714285@DFI + # 20000000@DUSD + # 1 DFI = 3.5 DUSD + self.addr_pool_DFI_DUSD = self.nodes[0].getnewaddress("", "legacy") + toAmounts = { self.addr_pool_DFI_DUSD: "5000000@DFI"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + toAmounts = { self.addr_pool_DFI_DUSD: "20000000@DUSD"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill LM account BTC-DUSD + # 400@BTC + # 20000000@DUSD + # 1 BTC = 50000 DUSD + self.addr_pool_BTC_DUSD = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_pool_BTC_DUSD: "400@BTC"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + toAmounts = {self.addr_pool_BTC_DUSD: "20000000@DUSD"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill LM account TSLA-DUSD + # 4000000000@TSLA + # 20000000@DUSD + # 1 TSLA = 0.005 DUSD + self.addr_pool_TSLA_DUSD = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_pool_TSLA_DUSD: "4000000000@TSLA"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + toAmounts = {self.addr_pool_TSLA_DUSD: "20000000@DUSD"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill account DUSD + self.addr_DUSD = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_DUSD: "10000000@DUSD"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill account DFI + self.addr_DFI = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_DFI: "1000000@DFI"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill account BTC + self.addr_BTC = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_BTC: "100@BTC"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Fill account TSLA + self.addr_TSLA = self.nodes[0].getnewaddress("", "legacy") + toAmounts = {self.addr_TSLA: "1000000000@TSLA"} + self.nodes[0].accounttoaccount(self.account0, toAmounts) + self.nodes[0].generate(1) + + # Check balances on each account + account = self.nodes[0].getaccount(self.account0) + assert_equal(account, []) + account = self.nodes[0].getaccount(self.addr_pool_DFI_DUSD) + assert_equal(account, ['5000000.00000000@DFI', '20000000.00000000@DUSD']) + account = self.nodes[0].getaccount(self.addr_pool_BTC_DUSD) + assert_equal(account, ['400.00000000@BTC', '20000000.00000000@DUSD']) + account = self.nodes[0].getaccount(self.addr_pool_TSLA_DUSD) + assert_equal(account, ['4000000000.00000000@TSLA', '20000000.00000000@DUSD']) + account = self.nodes[0].getaccount(self.addr_DFI) + assert_equal(account, ['1000000.00000000@DFI']) + account = self.nodes[0].getaccount(self.addr_BTC) + assert_equal(account, ['100.00000000@BTC']) + account = self.nodes[0].getaccount(self.addr_TSLA) + assert_equal(account, ['1000000000.00000000@TSLA']) + + def setup_poolpairs(self): + poolOwner = self.nodes[0].getnewaddress("", "legacy") + # DFI-DUSD + self.nodes[0].createpoolpair({ + "tokenA": self.iddUSD, + "tokenB": self.idDFI, + "commission": Decimal('0.002'), + "status": True, + "ownerAddress": poolOwner, + "pairSymbol": "DFI-DUSD", + }) + self.nodes[0].generate(1) + self.nodes[0].addpoolliquidity( + {self.addr_pool_DFI_DUSD: ["5000000@" + self.symbolDFI, "20000000@" + self.symboldUSD]}, self.account0) + self.nodes[0].generate(1) + + # BTC-DUSD + self.nodes[0].createpoolpair({ + "tokenA": self.iddUSD, + "tokenB": self.idBTC, + "commission": Decimal('0.002'), + "status": True, + "ownerAddress": poolOwner, + "pairSymbol": "BTC-DUSD", + }) + self.nodes[0].generate(1) + self.nodes[0].addpoolliquidity( + {self.addr_pool_BTC_DUSD: ["400@" + self.symbolBTC, "20000000@" + self.symboldUSD]}, self.account0) + self.nodes[0].generate(1) + + # TSLA-DUSD + self.nodes[0].createpoolpair({ + "tokenA": self.iddUSD, + "tokenB": self.idTSLA, + "commission": Decimal('0.002'), + "status": True, + "ownerAddress": poolOwner, + "pairSymbol": "TSLA-DUSD", + }) + self.nodes[0].generate(1) + self.nodes[0].addpoolliquidity( + {self.addr_pool_TSLA_DUSD: ["4000000000@" + self.symbolTSLA, "20000000@" + self.symboldUSD]}, self.account0) + self.nodes[0].generate(1) + + # check poolpairs and addresses + pool = self.nodes[0].getpoolpair("DFI-DUSD")['4'] + assert_equal(pool['reserveA'], Decimal('20000000')) + assert_equal(pool['reserveB'], Decimal('5000000')) + account = self.nodes[0].getaccount(self.addr_pool_DFI_DUSD) + assert_equal(account, []) + + pool = self.nodes[0].getpoolpair("BTC-DUSD")['5'] + assert_equal(pool['reserveA'], Decimal('20000000')) + assert_equal(pool['reserveB'], Decimal('400')) + account = self.nodes[0].getaccount(self.addr_pool_BTC_DUSD) + assert_equal(account, []) + + pool = self.nodes[0].getpoolpair("TSLA-DUSD")['6'] + assert_equal(pool['reserveA'], Decimal('20000000')) + assert_equal(pool['reserveB'], Decimal('4000000000')) + account = self.nodes[0].getaccount(self.addr_pool_TSLA_DUSD) + assert_equal(account, []) + + def setup_loanschemes(self): + self.nodes[0].createloanscheme(200, 1, 'LOAN200') + self.nodes[0].generate(1) + + def setup(self): + self.nodes[0].generate(150) + self.account0 = self.nodes[0].get_genesis_keys().ownerAuthAddress + self.create_tokens() + self.setup_oracles() + self.setup_loan_tokens() + self.create_fill_addresses() + self.setup_poolpairs() + self.setup_loanschemes() + + def payback_DUSD_with_BTC(self): + self.vaultId1 = self.nodes[0].createvault(self.account0, 'LOAN200') + self.nodes[0].generate(90) + + self.nodes[0].deposittovault(self.vaultId1, self.addr_DFI, "50@DFI") + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ + 'vaultId': self.vaultId1, + 'amounts': "100@" + self.symboldUSD + }) + self.nodes[0].generate(1) + + # Should not be able to payback loan with BTC + try: + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.account0, + 'amounts': "1@BTC" + }) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Loan token with id (1) does not exist!" in errorString) + + def payback_with_DFI_prior_to_atribute_activation(self): + # Should not be able to payback loan before DFI payback enabled + try: + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.account0, + 'amounts': "1@DFI" + }) + except JSONRPCException as e: + errorString = e.error['message'] + 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: params, poolpairs, token,', + self.nodes[0].setgov, {"ATTRIBUTES":{'v0/live/economy/dfi_payback_tokens':'1'}}) + + # Disable loan payback + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.iddUSD + '/payback_dfi':'false'}}) + self.nodes[0].generate(1) + + # Should not be able to payback loan before DFI payback enabled + assert_raises_rpc_error(-32600, "Payback of loan via DFI token is not currently active", self.nodes[0].paybackloan, { + 'vaultId': self.vaultId1, + 'from': self.account0, + 'amounts': "1@DFI" + }) + + def setgov_attribute_to_true_and_payback_with_dfi(self): + # Enable loan payback + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.iddUSD + '/payback_dfi':'true'}}) + self.nodes[0].generate(1) + + vaultBefore = self.nodes[0].getvault(self.vaultId1) + [amountBefore, _] = vaultBefore['loanAmounts'][0].split('@') + + # Partial loan payback in DFI + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.addr_DFI, + 'amounts': "1@DFI" + }) + self.nodes[0].generate(1) + + info = self.nodes[0].getburninfo() + assert_equal(info['dfipaybackfee'], Decimal('0.01000000')) # paybackfee defaults to 1% of total payback -> 0.01 DFI + assert_equal(info['dfipaybacktokens'], ['3.96000000@DUSD']) # 4 - penalty (0.01DFI->0.04USD) + + vaultAfter = self.nodes[0].getvault(self.vaultId1) + [amountAfter, _] = vaultAfter['loanAmounts'][0].split('@') + [interestAfter, _] = vaultAfter['interestAmounts'][0].split('@') + + assert_equal(Decimal(amountAfter) - Decimal(interestAfter), Decimal(amountBefore) - Decimal('3.96')) # no payback fee + + def test_5pct_penalty(self): + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.iddUSD + '/payback_dfi_fee_pct':'0.05'}}) + self.nodes[0].generate(1) + + vaultBefore = self.nodes[0].getvault(self.vaultId1) + [amountBefore, _] = vaultBefore['loanAmounts'][0].split('@') + + # Partial loan payback in DFI + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.addr_DFI, + 'amounts': "1@DFI" + }) + self.nodes[0].generate(1) + + info = self.nodes[0].getburninfo() + assert_equal(info['dfipaybackfee'], Decimal('0.06000000')) + assert_equal(info['dfipaybacktokens'], ['7.76000000@DUSD']) + + vaultAfter = self.nodes[0].getvault(self.vaultId1) + [amountAfter, _] = vaultAfter['loanAmounts'][0].split('@') + [interestAfter, _] = vaultAfter['interestAmounts'][0].split('@') + + assert_equal(Decimal(amountAfter) - Decimal(interestAfter), (Decimal(amountBefore) - (Decimal('3.8')))) # 4$ in DFI - 0.05fee = 3.8$ paid back + + def overpay_loan_in_DFI(self): + vaultBefore = self.nodes[0].getvault(self.vaultId1) + [amountBefore, _] = vaultBefore['loanAmounts'][0].split('@') + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.addr_DFI, + 'amounts': "250@DFI" + }) + self.nodes[0].generate(1) + + info = self.nodes[0].getburninfo() + assert_equal(info['dfipaybackfee'], Decimal('1.27368435')) + assert_equal(info['dfipaybacktokens'], ['100.00001113@DUSD']) # Total loan in vault1 + previous dfipaybacktokens + + attribs = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attribs['v0/live/economy/dfi_payback_tokens'], ['1.27368435@DFI', '100.00001113@DUSD']) + + vaultAfter = self.nodes[0].getvault(self.vaultId1) + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + assert_equal(len(vaultAfter['loanAmounts']), 0) + assert_equal(len(vaultAfter['interestAmounts']), 0) + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), (Decimal(amountBefore) / Decimal('3.8')).quantize(Decimal('1E-8'), rounding=ROUND_UP)) + + def take_new_loan_payback_exact_amount_in_DFI(self): + self.nodes[0].takeloan({ + 'vaultId': self.vaultId1, + 'amounts': "100@" + self.symboldUSD + }) + self.nodes[0].generate(10) + + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId1, + 'from': self.addr_DFI, + 'amounts': "26.31579449@DFI" # 25 DFI + 0.05 penalty + interests + }) + self.nodes[0].generate(1) + + info = self.nodes[0].getburninfo() + assert_equal(info['dfipaybackfee'], Decimal('2.58947407')) + assert_equal(info['dfipaybacktokens'], ['200.00003016@DUSD']) + + vaultAfter = self.nodes[0].getvault(self.vaultId1) + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + assert_equal(len(vaultAfter['loanAmounts']), 0) + assert_equal(len(vaultAfter['interestAmounts']), 0) + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('26.31579449')) + + def new_vault_payback_TSLA_loan_with_DFI(self): + # Payback of loan token other than DUSD + self.vaultId2 = self.nodes[0].createvault(self.account0, 'LOAN200') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(self.vaultId2, self.addr_DFI, "100@DFI") + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ + 'vaultId': self.vaultId2, + 'amounts': "10@" + self.symbolTSLA + }) + self.nodes[0].generate(1) + + #Should not be able to payback loan token other than DUSD with DFI + assert_raises_rpc_error(-32600, "There is no loan on token (DUSD) in this vault!", self.nodes[0].paybackloan, { + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'amounts': "10@DFI" + }) + # Should not be able to payback loan before DFI payback enabled + assert_raises_rpc_error(-32600, "Payback of loan via DFI token is not currently active", self.nodes[0].paybackloan, { + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [{ + 'dToken': self.idTSLA, + 'amounts': "1@DFI" + }] + }) + + def setgov_enable_dfi_payback_and_dfi_fee_pct(self): + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idTSLA + '/payback_dfi':'true'}}) + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idTSLA + '/payback_dfi_fee_pct':'0.01'}}) + self.nodes[0].generate(1) + + def setgov_enable_dTSLA_to_dBTC_payback(self): + self.nodes[0].setgov({ + "ATTRIBUTES":{ + 'v0/token/'+self.idTSLA+'/loan_payback/'+self.idBTC: 'true', + 'v0/token/'+self.idTSLA+'/loan_payback_fee_pct/'+self.idBTC: '0.25' + } + }) + self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/token/'+self.idTSLA+'/loan_payback/'+self.idBTC], 'true') + assert_equal(attributes['v0/token/'+self.idTSLA+'/loan_payback_fee_pct/'+self.idBTC], '0.25') + + + def payback_TSLA_with_1_dfi(self): + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [{ + 'dToken': self.idTSLA, + 'amounts': "0.01@DFI" + }] + }) + self.nodes[0].generate(1) + + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('0.01')) + + def take_dUSD_loan_and_payback_with_1_dfi(self): + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + self.nodes[0].takeloan({ + 'vaultId': self.vaultId2, + 'amounts': "100@" + self.symboldUSD + }) + self.nodes[0].generate(1) + + vaultBefore = self.nodes[0].getvault(self.vaultId2) + [amountBefore, _] = vaultBefore['loanAmounts'][1].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [{ + 'dToken': self.iddUSD, + 'amounts': "1@DFI" + }] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId2) + [amountAfter, _] = vaultAfter['loanAmounts'][1].split('@') + [interestAfter, _] = vaultAfter['interestAmounts'][1].split('@') + assert_equal(Decimal(amountAfter) - Decimal(interestAfter), (Decimal(amountBefore) - Decimal('3.8'))) + + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('1')) + + def payback_TSLA_and_dUSD_with_1_dfi(self): + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + vaultBefore = self.nodes[0].getvault(self.vaultId2) + [amountTSLABefore, _] = vaultBefore['loanAmounts'][0].split('@') + [amountDUSDBefore, _] = vaultBefore['loanAmounts'][1].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [ + { + 'dToken': self.idTSLA, + 'amounts': "0.001@DFI" + }, + { + 'dToken': self.iddUSD, + 'amounts': "1@DFI" + }, + ] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId2) + [amountTSLAAfter, _] = vaultAfter['loanAmounts'][0].split('@') + [interestTSLAAfter, _] = vaultAfter['interestAmounts'][0].split('@') + [amountDUSDAfter, _] = vaultAfter['loanAmounts'][1].split('@') + [interestDUSDAfter, _] = vaultAfter['interestAmounts'][1].split('@') + assert_equal(Decimal(amountTSLAAfter) - Decimal(interestTSLAAfter), (Decimal(amountTSLABefore) - Decimal('0.792'))) + assert_equal(Decimal(amountDUSDAfter) - Decimal(interestDUSDAfter), (Decimal(amountDUSDBefore) - Decimal('3.8'))) + + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('1.001')) + + def payback_TSLA_with_10_dfi(self): + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [{ + 'dToken': self.idTSLA, + 'amounts': "10@DFI" + }] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId2) + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('0.00162627')) + assert_equal(len(vaultAfter['loanAmounts']), 1) + assert_equal(len(vaultAfter['interestAmounts']), 1) + + def payback_dUSD_with_dfi(self): + [balanceDFIBefore, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DFI, + 'loans': [{ + 'dToken': self.iddUSD, + 'amounts': "25@DFI" + }] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId2) + [balanceDFIAfter, _] = self.nodes[0].getaccount(self.addr_DFI)[0].split('@') + + assert_equal(Decimal(balanceDFIBefore) - Decimal(balanceDFIAfter), Decimal('24.31579139')) + assert_equal(len(vaultAfter['loanAmounts']), 0) + assert_equal(len(vaultAfter['interestAmounts']), 0) + + def payback_TSLA_with_1_dBTC(self): + self.vaultId3 = self.nodes[0].createvault(self.account0, 'LOAN200') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(self.vaultId3, self.addr_DFI, "100000@DFI") + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ + 'vaultId': self.vaultId3, + 'amounts': "10000000@" + self.symbolTSLA + }) + self.nodes[0].generate(1) + [balanceBTCBefore, _] = self.nodes[0].getaccount(self.addr_BTC)[0].split('@') + vaultBefore = self.nodes[0].getvault(self.vaultId3) + [amountBefore, _] = vaultBefore['loanAmounts'][0].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId3, + 'from': self.addr_BTC, + 'loans': [{ + 'dToken': self.idTSLA, + 'amounts': "0.5@BTC" + }] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId3) + [amountAfter, _] = vaultAfter['loanAmounts'][0].split('@') + [interestAfter, _] = vaultAfter['interestAmounts'][0].split('@') + assert_equal(Decimal(amountAfter) - Decimal(interestAfter), (Decimal(amountBefore) - Decimal('3750000'))) # add 25% fee for payback with BTC + + [balanceBTCAfter, _] = self.nodes[0].getaccount(self.addr_BTC)[0].split('@') + assert_equal(Decimal(balanceBTCBefore) - Decimal(balanceBTCAfter), Decimal('0.5')) + + def payback_dUSD_with_dUSD(self): + self.nodes[0].takeloan({ + 'vaultId': self.vaultId2, + 'amounts': "100@" + self.symboldUSD + }) + self.nodes[0].generate(1) + + [balanceDUSDBefore, _] = self.nodes[0].getaccount(self.addr_DUSD)[0].split('@') + vaultBefore = self.nodes[0].getvault(self.vaultId2) + [amountBefore, _] = vaultBefore['loanAmounts'][0].split('@') + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId2, + 'from': self.addr_DUSD, + 'loans': [{ + 'dToken': self.iddUSD, + 'amounts': "100@DUSD" + }] + }) + self.nodes[0].generate(1) + + + vaultAfter = self.nodes[0].getvault(self.vaultId2) + [amountAfter, _] = vaultAfter['loanAmounts'][0].split('@') + [interestAfter, _] = vaultAfter['interestAmounts'][0].split('@') + assert_equal(Decimal(amountAfter) - Decimal(interestAfter), (Decimal(amountBefore) - Decimal('100'))) + + [balanceDUSDAfter, _] = self.nodes[0].getaccount(self.addr_DUSD)[0].split('@') + assert_equal(Decimal(balanceDUSDBefore) - Decimal(balanceDUSDAfter), Decimal('100')) + + def payback_TSLA_with_1sat_dBTC(self): + self.vaultId4 = self.nodes[0].createvault(self.account0, 'LOAN200') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(self.vaultId4, self.addr_DFI, "0.00000001@DFI") + self.nodes[0].generate(1) + + [balanceBTCBefore, _] = self.nodes[0].getaccount(self.addr_BTC)[0].split('@') + + self.nodes[0].takeloan({ + 'vaultId': self.vaultId4, + 'amounts': "0.00000001@" + self.symbolTSLA + }) + self.nodes[0].generate(1) + + self.nodes[0].paybackloan({ + 'vaultId': self.vaultId4, + 'from': self.addr_BTC, + 'loans': [{ + 'dToken': self.idTSLA, + 'amounts': "0.00000001@BTC" + }] + }) + self.nodes[0].generate(1) + + vaultAfter = self.nodes[0].getvault(self.vaultId4) + assert_equal(vaultAfter["interestAmounts"], []) + assert_equal(vaultAfter["loanAmounts"], []) + + [balanceBTCAfter, _] = self.nodes[0].getaccount(self.addr_BTC)[0].split('@') + assert_equal(Decimal(balanceBTCBefore) - Decimal(balanceBTCAfter), Decimal('0.00000001')) + + def run_test(self): + self.setup() + + self.payback_DUSD_with_BTC() + self.payback_with_DFI_prior_to_atribute_activation() + self.setgov_attribute_to_false_and_payback() + self.setgov_attribute_to_true_and_payback_with_dfi() + self.test_5pct_penalty() + self.overpay_loan_in_DFI() + self.take_new_loan_payback_exact_amount_in_DFI() + self.new_vault_payback_TSLA_loan_with_DFI() + + self.setgov_enable_dfi_payback_and_dfi_fee_pct() + + self.payback_TSLA_with_1_dfi() + self.take_dUSD_loan_and_payback_with_1_dfi() + self.payback_TSLA_and_dUSD_with_1_dfi() + self.payback_TSLA_with_10_dfi() + self.payback_dUSD_with_dfi() + + self.setgov_enable_dTSLA_to_dBTC_payback() + + self.payback_TSLA_with_1_dBTC() + self.payback_dUSD_with_dUSD() + self.payback_TSLA_with_1sat_dBTC() + +if __name__ == '__main__': + PaybackDFILoanTest().main() diff --git a/test/functional/feature_loan_vault.py b/test/functional/feature_loan_vault.py index 442d7171a11..357506625e9 100755 --- a/test/functional/feature_loan_vault.py +++ b/test/functional/feature_loan_vault.py @@ -424,7 +424,7 @@ def takeloan_with_50pctDFI(self): def withdraw_breaking_50pctDFI_rule(self): try: - self.nodes[0].withdrawfromvault(self.vaults[0], self.accountDFI, "1@DFI") + self.nodes[0].withdrawfromvault(self.vaults[0], self.accountDFI, "0.8@DFI") except JSONRPCException as e: error_str = e.error['message'] assert("At least 50% of the minimum required collateral must be in DFI" in error_str) @@ -593,11 +593,11 @@ def test_50pctDFI_fresh_vault_takeloan_withdraw(self): assert_equal(account, ['0.15000000@BTC', '1.00000000@TSLA']) def test_50pctDFI_rule_after_BTC_price_increase(self): - # BTC doubles in price - oracle_prices = [{"currency": "USD", "tokenAmount": "1@DFI"}, {"currency": "USD", "tokenAmount": "1@TSLA"}, {"currency": "USD", "tokenAmount": "2@BTC"}] + # BTC triplicates in price + oracle_prices = [{"currency": "USD", "tokenAmount": "1@DFI"}, {"currency": "USD", "tokenAmount": "1@TSLA"}, {"currency": "USD", "tokenAmount": "3@BTC"}] timestamp = calendar.timegm(time.gmtime()) self.nodes[0].setoracledata(self.oracles[0], timestamp, oracle_prices) - self.nodes[0].generate(20) + self.nodes[0].generate(240) # Should be able to withdraw part of BTC after BTC appreciation in price self.nodes[0].withdrawfromvault(self.vaults[4], self.owner_addresses[2], "0.5@BTC") @@ -605,7 +605,7 @@ def test_50pctDFI_rule_after_BTC_price_increase(self): # Should not be able to withdraw if DFI lower than 50% of collateralized loan value try: - self.nodes[0].withdrawfromvault(self.vaults[4], self.accountDFI, "0.26@DFI") + self.nodes[0].withdrawfromvault(self.vaults[4], self.accountDFI, "0.25@DFI") except JSONRPCException as e: errorString = e.error['message'] assert("At least 50% of the minimum required collateral must be in DFI" in errorString) diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index d56ee340f25..ed756f0d384 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -21,8 +21,8 @@ def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True self.extra_args = [ - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-subsidytest=1'], - ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-subsidytest=1']] + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1140', '-subsidytest=1'], + ['-txnotokens=0', '-amkheight=50', '-bayfrontheight=50', '-eunosheight=200', '-fortcanningheight=400', '-fortcanninghillheight=1110', '-fortcanningroadheight=1140', '-subsidytest=1']] def run_test(self): @@ -445,7 +445,7 @@ def run_test(self): 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: 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, "Unrecognised key argument provided, valid keys are: loan_payback, 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, "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'}}) @@ -523,5 +523,22 @@ def run_test(self): 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'}) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/5/loan_payback/1': 'true', 'v0/token/5/loan_payback/2': 'true', 'v0/token/5/loan_payback_fee_pct/1': '0.25'}}) + self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/token/5/loan_payback/1'], 'true') + assert_equal(attributes['v0/token/5/loan_payback/2'], 'true') + assert_equal(attributes['v0/token/5/loan_payback_fee_pct/1'], '0.25') + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/5/loan_payback/0': 'true', 'v0/token/5/loan_payback_fee_pct/0': '0.33'}}) + self.nodes[0].generate(1) + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + # dfi keys are set + assert_equal(attributes['v0/token/5/payback_dfi'], 'true') + assert_equal(attributes['v0/token/5/payback_dfi_fee_pct'], '0.33') + # no new keys for DFI + assert('v0/token/5/loan_payback/0' not in attributes) + assert('v0/token/5/loan_payback_fee_pct/0' not in attributes) + if __name__ == '__main__': GovsetTest ().main () diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 3fdb9867719..6cb085ed407 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -164,6 +164,7 @@ 'feature_loan_setloantoken.py', 'feature_loan_basics.py', 'feature_loan_payback_dfi.py', + 'feature_loan_payback_dfi_v2.py', 'feature_loan_get_interest.py', 'feature_loan_listauctions.py', 'feature_loan_auctions.py',