diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 8a28e2e83c..4227294687 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -125,19 +125,19 @@ const std::map>& ATTRIBUTES::allowedKeys static const std::map> keys{ { AttributeTypes::Token, { - {"payback_dfi", TokenKeys::PaybackDFI}, - {"payback_dfi_fee_pct", TokenKeys::PaybackDFIFeePCT}, - {"loan_payback", TokenKeys::LoanPayback}, - {"loan_payback_fee_pct", TokenKeys::LoanPaybackFeePCT}, - {"loan_payback_collateral", TokenKeys::LoanPaybackCollateral}, - {"dex_in_fee_pct", TokenKeys::DexInFeePct}, - {"dex_out_fee_pct", TokenKeys::DexOutFeePct}, - {"dfip2203", TokenKeys::DFIP2203Enabled}, - {"fixed_interval_price_id", TokenKeys::FixedIntervalPriceId}, - {"loan_collateral_enabled", TokenKeys::LoanCollateralEnabled}, - {"loan_collateral_factor", TokenKeys::LoanCollateralFactor}, - {"loan_minting_enabled", TokenKeys::LoanMintingEnabled}, - {"loan_minting_interest", TokenKeys::LoanMintingInterest}, + {"payback_dfi", TokenKeys::PaybackDFI}, + {"payback_dfi_fee_pct", TokenKeys::PaybackDFIFeePCT}, + {"loan_payback", TokenKeys::LoanPayback}, + {"loan_payback_fee_pct", TokenKeys::LoanPaybackFeePCT}, + {"loan_payback_collateral", TokenKeys::LoanPaybackCollateral}, + {"dex_in_fee_pct", TokenKeys::DexInFeePct}, + {"dex_out_fee_pct", TokenKeys::DexOutFeePct}, + {"dfip2203", TokenKeys::DFIP2203Enabled}, + {"fixed_interval_price_id", TokenKeys::FixedIntervalPriceId}, + {"loan_collateral_enabled", TokenKeys::LoanCollateralEnabled}, + {"loan_collateral_factor", TokenKeys::LoanCollateralFactor}, + {"loan_minting_enabled", TokenKeys::LoanMintingEnabled}, + {"loan_minting_interest", TokenKeys::LoanMintingInterest}, } }, { @@ -168,22 +168,22 @@ const std::map>& ATTRIBUTES::displayKeys static const std::map> keys{ { AttributeTypes::Token, { - {TokenKeys::PaybackDFI, "payback_dfi"}, - {TokenKeys::PaybackDFIFeePCT, "payback_dfi_fee_pct"}, - {TokenKeys::LoanPayback, "loan_payback"}, - {TokenKeys::LoanPaybackFeePCT, "loan_payback_fee_pct"}, - {TokenKeys::LoanPaybackCollateral, "loan_payback_collateral"}, - {TokenKeys::DexInFeePct, "dex_in_fee_pct"}, - {TokenKeys::DexOutFeePct, "dex_out_fee_pct"}, - {TokenKeys::FixedIntervalPriceId, "fixed_interval_price_id"}, - {TokenKeys::LoanCollateralEnabled, "loan_collateral_enabled"}, - {TokenKeys::LoanCollateralFactor, "loan_collateral_factor"}, - {TokenKeys::LoanMintingEnabled, "loan_minting_enabled"}, - {TokenKeys::LoanMintingInterest, "loan_minting_interest"}, - {TokenKeys::DFIP2203Enabled, "dfip2203"}, - {TokenKeys::Ascendant, "ascendant"}, - {TokenKeys::Descendant, "descendant"}, - {TokenKeys::Epitaph, "epitaph"}, + {TokenKeys::PaybackDFI, "payback_dfi"}, + {TokenKeys::PaybackDFIFeePCT, "payback_dfi_fee_pct"}, + {TokenKeys::LoanPayback, "loan_payback"}, + {TokenKeys::LoanPaybackFeePCT, "loan_payback_fee_pct"}, + {TokenKeys::LoanPaybackCollateral, "loan_payback_collateral"}, + {TokenKeys::DexInFeePct, "dex_in_fee_pct"}, + {TokenKeys::DexOutFeePct, "dex_out_fee_pct"}, + {TokenKeys::FixedIntervalPriceId, "fixed_interval_price_id"}, + {TokenKeys::LoanCollateralEnabled, "loan_collateral_enabled"}, + {TokenKeys::LoanCollateralFactor, "loan_collateral_factor"}, + {TokenKeys::LoanMintingEnabled, "loan_minting_enabled"}, + {TokenKeys::LoanMintingInterest, "loan_minting_interest"}, + {TokenKeys::DFIP2203Enabled, "dfip2203"}, + {TokenKeys::Ascendant, "ascendant"}, + {TokenKeys::Descendant, "descendant"}, + {TokenKeys::Epitaph, "epitaph"}, } }, { @@ -345,19 +345,19 @@ const std::map(const std::string&)>>> parsers{ { AttributeTypes::Token, { - {TokenKeys::PaybackDFI, VerifyBool}, - {TokenKeys::PaybackDFIFeePCT, VerifyPct}, - {TokenKeys::LoanPayback, VerifyBool}, - {TokenKeys::LoanPaybackFeePCT, VerifyPct}, - {TokenKeys::LoanPaybackCollateral, VerifyBool}, - {TokenKeys::DexInFeePct, VerifyPct}, - {TokenKeys::DexOutFeePct, VerifyPct}, - {TokenKeys::FixedIntervalPriceId, VerifyCurrencyPair}, - {TokenKeys::LoanCollateralEnabled, VerifyBool}, - {TokenKeys::LoanCollateralFactor, VerifyPositiveFloat}, - {TokenKeys::LoanMintingEnabled, VerifyBool}, - {TokenKeys::LoanMintingInterest, VerifyFloat}, - {TokenKeys::DFIP2203Enabled, VerifyBool}, + {TokenKeys::PaybackDFI, VerifyBool}, + {TokenKeys::PaybackDFIFeePCT, VerifyPct}, + {TokenKeys::LoanPayback, VerifyBool}, + {TokenKeys::LoanPaybackFeePCT, VerifyPct}, + {TokenKeys::LoanPaybackCollateral, VerifyBool}, + {TokenKeys::DexInFeePct, VerifyPct}, + {TokenKeys::DexOutFeePct, VerifyPct}, + {TokenKeys::FixedIntervalPriceId, VerifyCurrencyPair}, + {TokenKeys::LoanCollateralEnabled, VerifyBool}, + {TokenKeys::LoanCollateralFactor, VerifyPositiveFloat}, + {TokenKeys::LoanMintingEnabled, VerifyBool}, + {TokenKeys::LoanMintingInterest, VerifyFloat}, + {TokenKeys::DFIP2203Enabled, VerifyBool}, } }, { @@ -1209,7 +1209,7 @@ Res ATTRIBUTES::Apply(CCustomCSView & mnview, const uint32_t height) } } } else if (attrV0->key == TokenKeys::LoanCollateralFactor) { - if (height >= Params().GetConsensus().FortCanningEpilogueHeight) { + if (height >= static_cast(Params().GetConsensus().FortCanningEpilogueHeight)) { std::set ratio; mnview.ForEachLoanScheme([&ratio](const std::string &identifier, const CLoanSchemeData &data) { ratio.insert(data.ratio); diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index ab5e162258..2ac5abbc75 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -37,17 +37,17 @@ enum OracleIDs : uint8_t { }; enum EconomyKeys : uint8_t { - PaybackDFITokens = 'a', - PaybackTokens = 'b', - DFIP2203Current = 'c', - DFIP2203Burned = 'd', - DFIP2203Minted = 'e', - DFIP2206FCurrent = 'f', - DFIP2206FBurned = 'g', - DFIP2206FMinted = 'h', - DexTokens = 'i', - NegativeInt = 'j', - NegativeIntCurrent= 'k', + PaybackDFITokens = 'a', + PaybackTokens = 'b', + DFIP2203Current = 'c', + DFIP2203Burned = 'd', + DFIP2203Minted = 'e', + DFIP2206FCurrent = 'f', + DFIP2206FBurned = 'g', + DFIP2206FMinted = 'h', + DexTokens = 'i', + NegativeInt = 'j', + NegativeIntCurrent = 'k', }; enum DFIPKeys : uint8_t { @@ -56,28 +56,28 @@ enum DFIPKeys : uint8_t { MinSwap = 'c', RewardPct = 'd', BlockPeriod = 'e', - DUSDInterestBurn = 'g', - DUSDLoanBurn = 'h', + DUSDInterestBurn = 'g', + DUSDLoanBurn = 'h', StartBlock = 'i', }; enum TokenKeys : uint8_t { - PaybackDFI = 'a', - PaybackDFIFeePCT = 'b', - LoanPayback = 'c', - LoanPaybackFeePCT = 'd', - DexInFeePct = 'e', - DexOutFeePct = 'f', - DFIP2203Enabled = 'g', - FixedIntervalPriceId = 'h', - LoanCollateralEnabled = 'i', - LoanCollateralFactor = 'j', - LoanMintingEnabled = 'k', - LoanMintingInterest = 'l', - Ascendant = 'm', - Descendant = 'n', - Epitaph = 'o', - LoanPaybackCollateral = 'p', + PaybackDFI = 'a', + PaybackDFIFeePCT = 'b', + LoanPayback = 'c', + LoanPaybackFeePCT = 'd', + DexInFeePct = 'e', + DexOutFeePct = 'f', + DFIP2203Enabled = 'g', + FixedIntervalPriceId = 'h', + LoanCollateralEnabled = 'i', + LoanCollateralFactor = 'j', + LoanMintingEnabled = 'k', + LoanMintingInterest = 'l', + Ascendant = 'm', + Descendant = 'n', + Epitaph = 'o', + LoanPaybackCollateral = 'p', }; enum PoolKeys : uint8_t { @@ -88,7 +88,7 @@ enum PoolKeys : uint8_t { }; struct CDataStructureV0 { - uint8_t type; + uint8_t type; uint32_t typeId; uint32_t key; uint32_t keyId; @@ -188,11 +188,11 @@ enum FeeDirValues : uint8_t { Out }; -using CDexBalances = std::map; -using OracleSplits = std::map; +using CDexBalances = std::map; +using OracleSplits = std::map; using DescendantValue = std::pair; -using AscendantValue = std::pair; -using CAttributeType = std::variant; +using AscendantValue = std::pair; +using CAttributeType = std::variant; using CAttributeValue = std::variant; void TrackNegativeInterest(CCustomCSView& mnview, const CTokenAmount& amount); diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index f0dd629a50..530c90e961 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -4478,7 +4478,7 @@ Res PaybackWithCollateral(CCustomCSView& view, const CVaultData& vault, const CV if (!attributes) return Res::Err("Attributes unavailable"); - auto dUsdToken = view.GetToken("DUSD"); + const auto dUsdToken = view.GetToken("DUSD"); if (!dUsdToken) return Res::Err("Cannot find token DUSD"); @@ -4504,43 +4504,58 @@ Res PaybackWithCollateral(CCustomCSView& view, const CVaultData& vault, const CV } const auto& loanDUSD = loanAmounts->balances.at(dUsdToken->first); - auto rate = view.GetInterestRate(vaultId, dUsdToken->first, height); + const auto rate = view.GetInterestRate(vaultId, dUsdToken->first, height); if (!rate) return Res::Err("Cannot get interest rate for this token (DUSD)!"); - auto subInterest = TotalInterest(*rate, height); + const auto subInterest = TotalInterest(*rate, height); Res res{}; CAmount subLoanAmount{0}; CAmount subCollateralAmount{0}; - // Edge case where interest is greater than collateral + CAmount burnAmount{0}; + + // Case where interest > collateral: decrease interest, wipe collateral. if (subInterest > collateralDUSD) { subCollateralAmount = collateralDUSD; res = view.SubVaultCollateral(vaultId, {dUsdToken->first, subCollateralAmount}); - if (!res) - return res; + if (!res) return res; res = view.DecreaseInterest(height, vaultId, vault.schemeId, dUsdToken->first, 0, subCollateralAmount); - if (!res) - return res; + if (!res) return res; + + burnAmount = subCollateralAmount; } else { + // Postive interest: Loan + interest > collateral. + // Negative interest: Loan - abs(interest) > collateral. if (loanDUSD + subInterest > collateralDUSD) { subLoanAmount = collateralDUSD - subInterest; subCollateralAmount = collateralDUSD; } else { + // Common case: Collateral > loans. subLoanAmount = loanDUSD; subCollateralAmount = loanDUSD + subInterest; } - res = view.SubLoanToken(vaultId, {dUsdToken->first, subLoanAmount}); - if (!res) - return res; + if (subLoanAmount > 0) { + res = view.SubLoanToken(vaultId, {dUsdToken->first, subLoanAmount}); + if (!res) return res; + } - res = view.SubVaultCollateral(vaultId, {dUsdToken->first,subCollateralAmount}); - if (!res) - return res; + if (subCollateralAmount > 0) { + res = view.SubVaultCollateral(vaultId, {dUsdToken->first,subCollateralAmount}); + if (!res) return res; + } view.ResetInterest(height, vaultId, vault.schemeId, dUsdToken->first); + burnAmount = subInterest; + } + + + if (burnAmount > 0) + { + res = view.AddBalance(Params().GetConsensus().burnAddress, {dUsdToken->first, burnAmount}); + if (!res) return res; } // Guard against liquidation @@ -4553,11 +4568,22 @@ Res PaybackWithCollateral(CCustomCSView& view, const CVaultData& vault, const CV if (!collateralsLoans) return std::move(collateralsLoans); + // The check is required to do a ratio check safe guard, or the vault of ratio is unreliable. + // This can later be removed, if all edge cases of price deviations and max collateral factor for DUSD (1.5 currently) + // can be tested for economical stability. Taking the safer approach for now. + if (!IsVaultPriceValid(view, vaultId, height)) + return Res::Err("Cannot payback vault with non-DUSD assets while any of the asset's price is invalid"); + const auto scheme = view.GetLoanScheme(vault.schemeId); 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 view.SubMintedTokens(dUsdToken->first, subCollateralAmount); + if (subCollateralAmount > 0) { + res = view.SubMintedTokens(dUsdToken->first, subCollateralAmount); + if (!res) return res; + } + + return Res::Ok(); } Res storeGovVars(const CGovernanceHeightMessage& obj, CCustomCSView& view) { diff --git a/src/masternodes/rpc_accounts.cpp b/src/masternodes/rpc_accounts.cpp index fa20337bd3..8d7afe422e 100644 --- a/src/masternodes/rpc_accounts.cpp +++ b/src/masternodes/rpc_accounts.cpp @@ -1875,7 +1875,8 @@ UniValue getburninfo(const JSONRPCRequest& request) { } // withdraw burn if (value.category == uint8_t(CustomTxType::PaybackLoan) - || value.category == uint8_t(CustomTxType::PaybackLoanV2)) { + || value.category == uint8_t(CustomTxType::PaybackLoanV2) + || value.category == uint8_t(CustomTxType::PaybackWithCollateral)) { for (const auto& [id, amount] : value.diff) { paybackFee.Add({id, amount}); } diff --git a/test/functional/feature_loan_payback_with_collateral.py b/test/functional/feature_loan_payback_with_collateral.py index 157710bb02..076cb5805f 100755 --- a/test/functional/feature_loan_payback_with_collateral.py +++ b/test/functional/feature_loan_payback_with_collateral.py @@ -210,15 +210,9 @@ def test_guard_against_liquidation(self): self.nodes[0].deposittovault(self.vaultId, self.account0, "1000@" + self.symbolDUSD) self.nodes[0].generate(1) - vault = self.nodes[0].getvault(self.vaultId) - print("vault", vault) - self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": "1900@" + self.symbolTSLA }) self.nodes[0].generate(1) - vault = self.nodes[0].getvault(self.vaultId) - print("vault", vault) - self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": "100@" + self.symbolDUSD }) self.nodes[0].generate(1) @@ -312,6 +306,9 @@ def test_loans_greater_than_collaterals(self, payback_with_collateral): mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, mintedAmount + collateralDUSDAmount) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"][0], vaultBefore["interestAmounts"][0]) + self.rollback_to(height) def test_loans_equal_to_collaterals(self, payback_with_collateral): @@ -337,6 +334,8 @@ def test_loans_equal_to_collaterals(self, payback_with_collateral): assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + vaultBefore = self.nodes[0].getvault(self.vaultId) + payback_with_collateral() self.nodes[0].generate(1) @@ -352,6 +351,9 @@ def test_loans_equal_to_collaterals(self, payback_with_collateral): mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, Decimal(mintedAmount + Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_DOWN)) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"][0], vaultBefore["interestAmounts"][0]) + self.rollback_to(height) def test_interest_greater_than_collaterals(self, payback_with_collateral): @@ -375,6 +377,8 @@ def test_interest_greater_than_collaterals(self, payback_with_collateral): assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + vaultBefore = self.nodes[0].getvault(self.vaultId) + payback_with_collateral() self.nodes[0].generate(1) @@ -391,6 +395,9 @@ def test_interest_greater_than_collaterals(self, payback_with_collateral): mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, mintedAmount + Decimal(collateralDUSDAmount)) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"][0], vaultBefore["collateralAmounts"][1]) + self.rollback_to(height) def test_interest_equal_to_collaterals(self, payback_with_collateral): @@ -435,6 +442,9 @@ def test_interest_equal_to_collaterals(self, payback_with_collateral): mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, Decimal(mintedAmount + Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_DOWN)) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"][0], vaultBefore["interestAmounts"][0]) + self.rollback_to(height) def test_negative_interest_collaterals_greater_than_loans(self, payback_with_collateral): @@ -487,6 +497,9 @@ def test_negative_interest_collaterals_greater_than_loans(self, payback_with_col mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, mintedAmount + Decimal(DUSDloanAmount)) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"], []) # no burn on negative interest + self.rollback_to(height) def test_negative_interest_loans_greater_than_collaterals(self, payback_with_collateral): @@ -537,6 +550,9 @@ def test_negative_interest_loans_greater_than_collaterals(self, payback_with_col mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] assert_equal(mintedAmountBefore, mintedAmount + collateralDUSDAmount) + burnInfo = self.nodes[0].getburninfo() + assert_equal(burnInfo["paybackburn"], []) # no burn on negative interest + self.rollback_to(height) def run_test(self):