From e9299ab35661cdb2877daeb76011b6a241ce2947 Mon Sep 17 00:00:00 2001 From: jouzo Date: Thu, 8 Sep 2022 16:10:22 +0100 Subject: [PATCH 1/6] Allow payback of DUSD with vault's collateral --- src/masternodes/govvariables/attributes.cpp | 9 + src/masternodes/govvariables/attributes.h | 1 + src/masternodes/loan.h | 12 + src/masternodes/mn_checks.cpp | 114 +++- src/masternodes/mn_checks.h | 3 + src/masternodes/rpc_customtx.cpp | 4 + src/masternodes/rpc_loan.cpp | 67 +++ .../feature_loan_payback_with_collateral.py | 525 ++++++++++++++++++ test/functional/test_runner.py | 1 + 9 files changed, 731 insertions(+), 5 deletions(-) create mode 100755 test/functional/feature_loan_payback_with_collateral.py diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 1f17f56cd99..98c7a335dc7 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -129,6 +129,7 @@ const std::map>& ATTRIBUTES::allowedKeys {"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}, @@ -171,6 +172,7 @@ const std::map>& ATTRIBUTES::displayKeys {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"}, @@ -345,6 +347,7 @@ const std::maptype) { case AttributeTypes::Token: switch (attrV0->key) { + case TokenKeys::LoanPaybackCollateral: + if (view.GetLastHeight() < Params().GetConsensus().FortCanningEpilogueHeight) { + return Res::Err("Cannot be set before FortCanningEpilogue"); + } + + [[fallthrough]]; case TokenKeys::PaybackDFI: case TokenKeys::PaybackDFIFeePCT: if (!view.GetLoanTokenByID({attrV0->typeId})) { diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index 0930924faec..b968a50165a 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -75,6 +75,7 @@ enum TokenKeys : uint8_t { Ascendant = 'm', Descendant = 'n', Epitaph = 'o', + LoanPaybackCollateral = 'p', }; enum PoolKeys : uint8_t { diff --git a/src/masternodes/loan.h b/src/masternodes/loan.h index b5386215177..a19b70f0877 100644 --- a/src/masternodes/loan.h +++ b/src/masternodes/loan.h @@ -413,6 +413,18 @@ class CLoanPaybackLoanV2Message } }; +struct CPaybackWithCollateralMessage { + CVaultId vaultId; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(vaultId); + } +}; + class CLoanView : public virtual CStorageView { public: using CLoanSetCollateralTokenImpl = CLoanSetCollateralTokenImplementation; diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 39c52fbbc7c..d60895a6d21 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -75,6 +75,7 @@ std::string ToString(CustomTxType type) { case CustomTxType::UpdateVault: return "UpdateVault"; case CustomTxType::DepositToVault: return "DepositToVault"; case CustomTxType::WithdrawFromVault: return "WithdrawFromVault"; + case CustomTxType::PaybackWithCollateral: return "PaybackWithCollateral"; case CustomTxType::TakeLoan: return "TakeLoan"; case CustomTxType::PaybackLoan: return "PaybackLoan"; case CustomTxType::PaybackLoanV2: return "PaybackLoan"; @@ -174,6 +175,7 @@ CCustomTxMessage customTypeToMessage(CustomTxType txType) { case CustomTxType::UpdateVault: return CUpdateVaultMessage{}; case CustomTxType::DepositToVault: return CDepositToVaultMessage{}; case CustomTxType::WithdrawFromVault: return CWithdrawFromVaultMessage{}; + case CustomTxType::PaybackWithCollateral: return CPaybackWithCollateralMessage{}; case CustomTxType::TakeLoan: return CLoanTakeLoanMessage{}; case CustomTxType::PaybackLoan: return CLoanPaybackLoanMessage{}; case CustomTxType::PaybackLoanV2: return CLoanPaybackLoanV2Message{}; @@ -251,6 +253,13 @@ class CCustomMetadataParseVisitor return Res::Ok(); } + Res isPostFortCanningEpilogueFork() const { + if(static_cast(height) < consensus.FortCanningEpilogueHeight) { + return Res::Err("called before FortCanningEpilogue height"); + } + return Res::Ok(); + } + template Res serialize(T& obj) const { CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); @@ -568,6 +577,11 @@ class CCustomMetadataParseVisitor return !res ? res : serialize(obj); } + Res operator()(CPaybackWithCollateralMessage& obj) const { + auto res = isPostFortCanningEpilogueFork(); + return !res ? res : serialize(obj); + } + Res operator()(CLoanTakeLoanMessage& obj) const { auto res = isPostFortCanningFork(); return !res ? res : serialize(obj); @@ -3083,6 +3097,93 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return mnview.AddBalance(obj.to, obj.amount); } + Res operator()(const CPaybackWithCollateralMessage& obj) const { + auto res = CheckCustomTx(); + if (!res) + return res; + + const auto attributes = mnview.GetAttributes(); + if (!attributes) + return Res::Err("Attributes unavailable"); + + auto dUsdToken = mnview.GetToken("DUSD"); + if (!dUsdToken) + return Res::Err("Cannot find token DUSD"); + + CDataStructureV0 activeKey{AttributeTypes::Token, dUsdToken->first.v, TokenKeys::LoanPaybackCollateral}; + if (!attributes->GetValue(activeKey, false)) + return Res::Err("Payback of DUSD loan with collateral is not currently active"); + + // vault exists + auto vault = mnview.GetVault(obj.vaultId); + if (!vault) + return Res::Err("Vault <%s> not found", obj.vaultId.GetHex()); + + // vault under liquidation + if (vault->isUnderLiquidation) + return Res::Err("Cannot payback vault with collateral while vault's under liquidation"); + + // owner auth + if (!HasAuth(vault->ownerAddress)) + return Res::Err("tx must have at least one input from token owner"); + + if (!IsVaultPriceValid(mnview, obj.vaultId, height)) + return Res::Err("Cannot payback vault with collateral while any of the asset's price is invalid"); + + const auto collateralAmounts = mnview.GetVaultCollaterals(obj.vaultId); + if (!collateralAmounts) { + return Res::Err("Vault has no collaterals"); + } + if (!collateralAmounts->balances.count(dUsdToken->first)) { + return Res::Err("Vault does not have any DUSD collaterals"); + } + const auto& collateralDUSD = collateralAmounts->balances.at(dUsdToken->first); + + const auto loanAmounts = mnview.GetLoanTokens(obj.vaultId); + if (!loanAmounts) { + return Res::Err("Vault has no loans"); + } + if (!loanAmounts->balances.count(dUsdToken->first)) { + return Res::Err("Vault does not have any DUSD loans"); + } + const auto& loanDUSD = loanAmounts->balances.at(dUsdToken->first); + + auto rate = mnview.GetInterestRate(obj.vaultId, dUsdToken->first, height); + if (!rate) + return Res::Err("Cannot get interest rate for this token (DUSD)!"); + auto subInterest = TotalInterest(*rate, height); + + // Edge case where interest is greater than collateral + if (subInterest > collateralDUSD) { + res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first, collateralDUSD}); + if (!res) + return res; + + return mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, collateralDUSD); + } + + CTokenAmount subLoanAmount; + CTokenAmount subCollateralAmount; + if (loanDUSD + subInterest > collateralDUSD) { + subLoanAmount = {dUsdToken->first, collateralDUSD - subInterest}; + subCollateralAmount = {dUsdToken->first, collateralDUSD}; + } else { + subLoanAmount = {dUsdToken->first, loanDUSD}; + subCollateralAmount = {dUsdToken->first, loanDUSD + subInterest}; + } + + res = mnview.SubLoanToken(obj.vaultId, subLoanAmount); + if (!res) + return res; + + res = mnview.SubVaultCollateral(obj.vaultId, subCollateralAmount); + if (!res) + return res; + + mnview.ResetInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first); + return Res::Ok(); + } + Res operator()(const CLoanTakeLoanMessage& obj) const { auto res = CheckCustomTx(); if (!res) @@ -3133,14 +3234,14 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor const auto totalInterest = TotalInterest(*rate, height); if (totalInterest < 0) { - loanAmountChange = currentLoanAmount > std::abs(totalInterest) ? + loanAmountChange = currentLoanAmount > std::abs(totalInterest) ? // Interest to decrease smaller than overall existing loan amount. // So reduce interest from the borrowing principal. If this is negative, - // we'll reduce from principal. - tokenAmount + totalInterest : + // we'll reduce from principal. + tokenAmount + totalInterest : // Interest to decrease is larger than old loan amount. - // We reduce from the borrowing principal. If this is negative, - // we'll reduce from principal. + // We reduce from the borrowing principal. If this is negative, + // we'll reduce from principal. tokenAmount - currentLoanAmount; resetInterestToHeight = true; } @@ -3686,6 +3787,9 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, CAccountsHistoryWriter& } else if (txType == CustomTxType::WithdrawFromVault) { auto obj = std::get(txMessage); view.vaultID = obj.vaultId; + } else if (txType == CustomTxType::PaybackWithCollateral) { + auto obj = std::get(txMessage); + view.vaultID = obj.vaultId; } else if (txType == CustomTxType::TakeLoan) { auto obj = std::get(txMessage); view.vaultID = obj.vaultId; diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 979a0b54ecd..d50ea10bac5 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -96,6 +96,7 @@ enum class CustomTxType : uint8_t UpdateVault = 'v', DepositToVault = 'S', WithdrawFromVault = 'J', + PaybackWithCollateral = 'W', TakeLoan = 'X', PaybackLoan = 'H', PaybackLoanV2 = 'k', @@ -155,6 +156,7 @@ inline CustomTxType CustomTxCodeToType(uint8_t ch) { case CustomTxType::UpdateVault: case CustomTxType::DepositToVault: case CustomTxType::WithdrawFromVault: + case CustomTxType::PaybackWithCollateral: case CustomTxType::TakeLoan: case CustomTxType::PaybackLoan: case CustomTxType::PaybackLoanV2: @@ -377,6 +379,7 @@ using CCustomTxMessage = std::variant< CUpdateVaultMessage, CDepositToVaultMessage, CWithdrawFromVaultMessage, + CPaybackWithCollateralMessage, CLoanTakeLoanMessage, CLoanPaybackLoanMessage, CLoanPaybackLoanV2Message, diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index 25563724f3d..99b6c8e2148 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -420,6 +420,10 @@ class CCustomTxRpcVisitor rpcInfo.pushKV("amount", obj.amount.ToString()); } + void operator()(const CPaybackWithCollateralMessage& obj) const { + rpcInfo.pushKV("vaultId", obj.vaultId.GetHex()); + } + void operator()(const CLoanTakeLoanMessage& obj) const { rpcInfo.pushKV("vaultId", obj.vaultId.GetHex()); if (!obj.to.empty()) diff --git a/src/masternodes/rpc_loan.cpp b/src/masternodes/rpc_loan.cpp index b8e0f97b90e..11e5a091839 100644 --- a/src/masternodes/rpc_loan.cpp +++ b/src/masternodes/rpc_loan.cpp @@ -1550,6 +1550,72 @@ UniValue getinterest(const JSONRPCRequest& request) { return GetRPCResultCache().Set(request, ret); } +UniValue paybackwithcollateral(const JSONRPCRequest& request) { + auto pwallet = GetWallet(request); + + RPCHelpMan{"paybackwithcollateral", + "Payback vault's loans with vault's collaterals.\n", + { + {"vaultId", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "vault hex id"}, + }, + RPCResult{ + "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" + }, + RPCExamples{ + HelpExampleCli("paybackwithcollateral", R"(5474b2e9bfa96446e5ef3c9594634e1aa22d3a0722cb79084d61253acbdf87bf)") + + HelpExampleRpc("paybackwithcollateral", R"(5474b2e9bfa96446e5ef3c9594634e1aa22d3a0722cb79084d61253acbdf87bf)") + }, + }.Check(request); + + const auto vaultId = ParseHashV(request.params[0], "vaultId"); + CPaybackWithCollateralMessage msg{vaultId}; + CDataStream markedMetadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); + markedMetadata << static_cast(CustomTxType::PaybackWithCollateral) + << msg; + + CScript scriptMeta; + scriptMeta << OP_RETURN << ToByteVector(markedMetadata); + + int targetHeight; + CScript ownerAddress; + { + LOCK(cs_main); + targetHeight = ::ChainActive().Height() + 1; + // decode vaultId + auto vault = pcustomcsview->GetVault(vaultId); + if (!vault) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Vault <%s> not found", vaultId.GetHex())); + + ownerAddress = vault->ownerAddress; + } + + const auto txVersion = GetTransactionVersion(targetHeight); + CMutableTransaction rawTx(txVersion); + + rawTx.vout.push_back(CTxOut(0, scriptMeta)); + + UniValue const & txInputs = request.params[3]; + + CTransactionRef optAuthTx; + std::set auths{ownerAddress}; + rawTx.vin = GetAuthInputsSmart(pwallet, rawTx.nVersion, auths, false /*needFoundersAuth*/, optAuthTx, txInputs); + + CCoinControl coinControl; + + // Set change to from address + CTxDestination dest; + ExtractDestination(ownerAddress, dest); + if (IsValidDestination(dest)) { + coinControl.destChange = dest; + } + + fund(rawTx, pwallet, optAuthTx, &coinControl); + + // check execution + execTestTx(CTransaction(rawTx), targetHeight, optAuthTx); + + return signsend(rawTx, pwallet, optAuthTx)->GetHash().GetHex(); +} static const CRPCCommand commands[] = { @@ -1570,6 +1636,7 @@ static const CRPCCommand commands[] = {"loan", "getloanscheme", &getloanscheme, {"id"}}, {"loan", "takeloan", &takeloan, {"metadata", "inputs"}}, {"loan", "paybackloan", &paybackloan, {"metadata", "inputs"}}, + {"vault", "paybackwithcollateral", &paybackwithcollateral, {"vaultId"}}, {"loan", "getloaninfo", &getloaninfo, {}}, {"loan", "getinterest", &getinterest, {"id", "token"}}, }; diff --git a/test/functional/feature_loan_payback_with_collateral.py b/test/functional/feature_loan_payback_with_collateral.py new file mode 100755 index 00000000000..7896f474e5e --- /dev/null +++ b/test/functional/feature_loan_payback_with_collateral.py @@ -0,0 +1,525 @@ +#!/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 - Payback with collateral.""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal, assert_raises_rpc_error +from decimal import ROUND_UP, Decimal +import calendar +import time + +BLOCKS_PER_YEAR = Decimal(1051200) + +class LoanPaybackWithCollateralTest (DefiTestFramework): + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [[ + '-txnotokens=0', + '-amkheight=1', + '-bayfrontheight=1', + '-eunosheight=1', + '-fortcanningheight=1', + '-fortcanningmuseumheight=1', + '-fortcanninghillheight=1', + '-fortcanningroadheight=1', + '-fortcanningspringheight=1', + '-fortcanningcrunchheight=1', + '-fortcanninggreatworldheight=1', + '-fortcanningepilogueheight=1', + '-jellyfish_regtest=1', + '-simulatemainnet=1' + ]] + + def rollback_to(self, block): + self.log.info("rollback to: %d", block) + node = self.nodes[0] + current_height = node.getblockcount() + if current_height == block: + return + blockhash = node.getblockhash(block + 1) + node.invalidateblock(blockhash) + node.clearmempool() + assert_equal(block, node.getblockcount()) + + def createOracles(self): + self.oracle_address1 = self.nodes[0].getnewaddress("", "legacy") + price_feeds = [{"currency": "USD", "token": "DFI"}, + {"currency": "USD", "token": "DUSD"}, + {"currency": "USD", "token": "TSLA"}] + self.oracle_id1 = self.nodes[0].appointoracle(self.oracle_address1, price_feeds, 10) + self.nodes[0].generate(1) + + # feed oracle + oracle_prices = [{"currency": "USD", "tokenAmount": "1@TSLA"}, + {"currency": "USD", "tokenAmount": "1@DUSD"}, + {"currency": "USD", "tokenAmount": "10@DFI"}] + + timestamp = calendar.timegm(time.gmtime()) + self.nodes[0].setoracledata(self.oracle_id1, timestamp, oracle_prices) + + self.oracle_address2 = self.nodes[0].getnewaddress("", "legacy") + self.oracle_id2 = self.nodes[0].appointoracle(self.oracle_address2, price_feeds, 10) + self.nodes[0].generate(1) + + # feed oracle + timestamp = calendar.timegm(time.gmtime()) + self.nodes[0].setoracledata(self.oracle_id2, timestamp, oracle_prices) + self.nodes[0].generate(120) + + def setup(self): + self.nodes[0].generate(120) + + self.createOracles() + + self.mn_address = self.nodes[0].get_genesis_keys().ownerAuthAddress + self.account0 = self.nodes[0].getnewaddress() + + self.collateralAmount = 2000 + self.loanAmount = 1000 + + self.symbolDFI = "DFI" + self.symbolTSLA = "TSLA" + self.symbolDUSD = "DUSD" + + self.nodes[0].setloantoken({ + 'symbol': "DUSD", + 'name': "DUSD", + 'fixedIntervalPriceId': "DUSD/USD", + 'mintable': True, + 'interest': 0 + }) + self.nodes[0].generate(120) + + self.idDUSD = list(self.nodes[0].gettoken(self.symbolDUSD).keys())[0] + + # Mint DUSD + self.nodes[0].minttokens("100000@DUSD") + self.nodes[0].generate(1) + + # Create DFI tokens + self.nodes[0].utxostoaccount({self.mn_address: "100000@" + self.symbolDFI}) + self.nodes[0].generate(1) + + # Create pool pair + self.nodes[0].createpoolpair({ + "tokenA": self.symbolDFI, + "tokenB": self.symbolDUSD, + "commission": 0, + "status": True, + "ownerAddress": self.mn_address + }) + self.nodes[0].generate(1) + + # Add pool liquidity + self.nodes[0].addpoolliquidity({ + self.mn_address: [ + '10000@' + self.symbolDFI, + '8000@' + self.symbolDUSD] + }, self.mn_address) + self.nodes[0].generate(1) + + self.nodes[0].setloantoken({ + 'symbol': "TSLA", + 'name': "TSLA", + 'fixedIntervalPriceId': "TSLA/USD", + 'mintable': True, + 'interest': 0 + }) + self.nodes[0].generate(1) + + # Set collateral tokens + self.nodes[0].setcollateraltoken({ + 'token': self.symbolDFI, + 'factor': 1, + 'fixedIntervalPriceId': "DFI/USD" + }) + self.nodes[0].setcollateraltoken({ + 'token': self.symbolDUSD, + 'factor': 1, + 'fixedIntervalPriceId': "DUSD/USD" + }) + self.nodes[0].generate(120) + + # Create loan scheme + self.nodes[0].createloanscheme(150, 1, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].accounttoaccount(self.mn_address, {self.account0: str(self.collateralAmount) + "@" + self.symbolDUSD}) + self.nodes[0].accounttoaccount(self.mn_address, {self.account0: str(self.collateralAmount) + "@" + self.symbolDFI}) + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'true'}}) + self.nodes[0].generate(1) + + def test_guards(self): + height = self.nodes[0].getblockcount() + + vaultId = self.nodes[0].createvault(self.account0, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'false'}}) + self.nodes[0].generate(1) + assert_raises_rpc_error(-32600, "Payback of DUSD loan with collateral is not currently active", self.nodes[0].paybackwithcollateral, vaultId) + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'true'}}) + self.nodes[0].generate(1) + + assert_raises_rpc_error(-32600, "Vault has no collaterals", self.nodes[0].paybackwithcollateral, vaultId) + + # Deposit DUSD and DFI to vault + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + assert_raises_rpc_error(-32600, "Vault does not have any DUSD collaterals", self.nodes[0].paybackwithcollateral, vaultId) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + assert_raises_rpc_error(-32600, "Vault has no loans", self.nodes[0].paybackwithcollateral, vaultId) + + # take TSLA loan + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": "1@" + self.symbolTSLA }) + self.nodes[0].generate(1) + + assert_raises_rpc_error(-32600, "Vault does not have any DUSD loans", self.nodes[0].paybackwithcollateral, vaultId) + + self.rollback_to(height) + + def test_collaterals_greater_than_loans(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 2000 + loanDUSDAmount = 1000 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + # Take DUSD loan + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + vaultBefore = self.nodes[0].getvault(vaultId) + [collateralAmountBefore, _] = vaultBefore["collateralAmounts"][1].split("@") + [loanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + [collateralAmount, _] = vault["collateralAmounts"][1].split("@") + + assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Payback all DUSD loans + assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Payback all DUSD interests + assert_equal(Decimal(collateralAmount), Decimal(collateralAmountBefore) - Decimal(loanAmountBefore)) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.rollback_to(height) + + def test_loans_greater_than_collaterals(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 1000 + loanDUSDAmount = 1100 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + # Take DUSD loan + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000010464231354642313546") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + vaultBefore = self.nodes[0].getvault(vaultId) + [loanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + [interestAmount, _] = vault["interestAmounts"][0].split("@") + [loanAmount, _] = vault["loanAmounts"][0].split("@") + assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral + assert_equal(Decimal(loanAmount), Decimal(loanAmountBefore) - Decimal(collateralDUSDAmount) + Decimal(interestAmount)) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000000951293859113394216") + assert_equal(Decimal(interestAmount), Decimal(storedInterest["interestPerBlock"]).quantize(Decimal('1E-8'), rounding=ROUND_UP)) + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + self.rollback_to(height) + + def test_loans_equal_to_collaterals(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 1000 + loanDUSDAmount = 1000 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + expected_IPB = 0.00000952 + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount + expected_IPB) + "@" + self.symbolDUSD) # Deposit enough to match amount of loans + interest after one block + self.nodes[0].generate(1) + + # Take DUSD loan + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Payback all DUSD loans + assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Payback all DUSD interests + assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.rollback_to(height) + + def test_interest_greater_than_collaterals(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 0.000001 + loanDUSDAmount = 1000 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + vaultBefore = self.nodes[0].getvault(vaultId) + [interestAmountBefore, _] = vaultBefore["interestAmounts"][0].split("@") + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + [interestAmount, _] = vault["interestAmounts"][0].split("@") + assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0.000008512937595129375951")) + + assert_equal(Decimal(interestAmount), Decimal(Decimal(storedInterest["interestPerBlock"]) * 2 - Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_UP)) + + self.rollback_to(height) + + def test_interest_equal_to_collaterals(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 0 + loanDUSDAmount = 1000 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + expected_IPB = 0.00000952 + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount + expected_IPB) + "@" + self.symbolDUSD) # Deposit enough to match amount of interest after one block + self.nodes[0].generate(1) + + # Take DUSD loan + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + vaultBefore = self.nodes[0].getvault(vaultId) + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral + assert_equal(vault["interestAmounts"], vaultBefore["interestAmounts"]) + assert_equal(vault["loanAmounts"], vaultBefore["loanAmounts"]) + assert_equal(vault["collateralValue"], float(vaultBefore["collateralValue"]) - expected_IPB) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.rollback_to(height) + + def test_negative_interest_collaterals_greater_than_loans(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 1000 + loanDUSDAmount = 1000 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_minting_interest':'-500'}}) + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + # accrue negative interest + self.nodes[0].generate(5) + + vault = self.nodes[0].getvault(vaultId) + [DUSDInterestAmount, _] = vault["interestAmounts"][0].split("@") + assert(Decimal(DUSDInterestAmount) < 0) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "-0.004746955859969558599695") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + # collateral amount should be equal to the opposite of DUSDInterestAmount + [DUSDCollateralAmount, _] = vault["collateralAmounts"][1].split("@") + assert_equal(Decimal(DUSDCollateralAmount), Decimal(DUSDInterestAmount) * -1) + + assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Paid back all DUSD loans + assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Paid back all DUSD interests + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("0")) + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) + + self.rollback_to(height) + + def test_negative_interest_loans_greater_than_collaterals(self): + height = self.nodes[0].getblockcount() + + collateralDUSDAmount = 1000 + loanDUSDAmount = 1100 + + vault_address = self.nodes[0].getnewaddress() + + vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].generate(1) + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].generate(1) + + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_minting_interest':'-500'}}) + self.nodes[0].generate(1) + + self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].generate(1) + + # accrue negative interest + self.nodes[0].generate(5) + + vaultBefore = self.nodes[0].getvault(vaultId) + [DUSDLoanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") + [DUSDInterestAmount, _] = vaultBefore["interestAmounts"][0].split("@") + assert(Decimal(DUSDInterestAmount) < 0) + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(storedInterest["interestPerBlock"], "-0.005221651445966514459665") + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + self.nodes[0].paybackwithcollateral(vaultId) + self.nodes[0].generate(1) + + vault = self.nodes[0].getvault(vaultId) + [DUSDLoanAmount, _] = vault["loanAmounts"][0].split("@") + [DUSDInterestAmount, _] = vault["interestAmounts"][0].split("@") + assert_equal(Decimal(DUSDLoanAmount), Decimal(DUSDLoanAmountBefore) - Decimal(collateralDUSDAmount) + Decimal(DUSDInterestAmount)) + assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral + + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("-0.000474546864344558599695")) + assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) + + self.rollback_to(height) + + def run_test(self): + + self.setup() + + self.test_guards() + + self.test_collaterals_greater_than_loans() + + self.test_loans_greater_than_collaterals() + + self.test_loans_equal_to_collaterals() + + self.test_interest_greater_than_collaterals() + + self.test_interest_equal_to_collaterals() + + self.test_negative_interest_collaterals_greater_than_loans() + + self.test_negative_interest_loans_greater_than_collaterals() + +if __name__ == '__main__': + LoanPaybackWithCollateralTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 199a8ee1664..0a76705ddc5 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -177,6 +177,7 @@ 'feature_loan_listauctions.py', 'feature_loan_auctions.py', 'feature_loan_dusd_as_collateral.py', + 'feature_loan_payback_with_collateral.py', 'feature_any_accounts_to_accounts.py', 'feature_sendtokenstoaddress.py', 'feature_poolswap.py', From fcaf72809724d1a8af4ac452265d8fa289028839 Mon Sep 17 00:00:00 2001 From: jouzo Date: Thu, 8 Sep 2022 20:29:57 +0100 Subject: [PATCH 2/6] Fix test --- test/functional/feature_setgov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/feature_setgov.py b/test/functional/feature_setgov.py index 8f213244613..fa7259d262a 100755 --- a/test/functional/feature_setgov.py +++ b/test/functional/feature_setgov.py @@ -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: 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_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: 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, "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'}}) From 3d5ec26b1fe9bfce24d7f96ebb011f1f9b28f9a2 Mon Sep 17 00:00:00 2001 From: jouzo Date: Fri, 9 Sep 2022 09:06:27 +0100 Subject: [PATCH 3/6] Guard against liquidation --- src/masternodes/mn_checks.cpp | 51 +++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 1c2c62e1035..850c9de1f8b 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -3159,28 +3159,45 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!res) return res; - return mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, collateralDUSD); - } - - CTokenAmount subLoanAmount; - CTokenAmount subCollateralAmount; - if (loanDUSD + subInterest > collateralDUSD) { - subLoanAmount = {dUsdToken->first, collateralDUSD - subInterest}; - subCollateralAmount = {dUsdToken->first, collateralDUSD}; + res = mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, collateralDUSD); + if (!res) + return res; } else { - subLoanAmount = {dUsdToken->first, loanDUSD}; - subCollateralAmount = {dUsdToken->first, loanDUSD + subInterest}; + CTokenAmount subLoanAmount; + CTokenAmount subCollateralAmount; + if (loanDUSD + subInterest > collateralDUSD) { + subLoanAmount = {dUsdToken->first, collateralDUSD - subInterest}; + subCollateralAmount = {dUsdToken->first, collateralDUSD}; + } else { + subLoanAmount = {dUsdToken->first, loanDUSD}; + subCollateralAmount = {dUsdToken->first, loanDUSD + subInterest}; + } + + res = mnview.SubLoanToken(obj.vaultId, subLoanAmount); + if (!res) + return res; + + res = mnview.SubVaultCollateral(obj.vaultId, subCollateralAmount); + if (!res) + return res; + + mnview.ResetInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first); } - res = mnview.SubLoanToken(obj.vaultId, subLoanAmount); - if (!res) - return res; + // Guard against liquidation + const auto collaterals = mnview.GetVaultCollaterals(obj.vaultId); + const auto loans = mnview.GetLoanTokens(obj.vaultId); + if (!collaterals && loans) + return Res::Err("Vault cannot have loans without collaterals"); - res = mnview.SubVaultCollateral(obj.vaultId, subCollateralAmount); - if (!res) - return res; + auto collateralsLoans = mnview.GetLoanCollaterals(obj.vaultId, *collaterals, height, time); + if (!collateralsLoans) + return std::move(collateralsLoans); + + const auto scheme = mnview.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); - mnview.ResetInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first); return Res::Ok(); } From 99161a1ce7889ace1d42f260d4fc90c64774baff Mon Sep 17 00:00:00 2001 From: jouzo Date: Fri, 9 Sep 2022 09:15:44 +0100 Subject: [PATCH 4/6] Sub minted tokens --- src/masternodes/mn_checks.cpp | 27 +++++---- .../feature_loan_payback_with_collateral.py | 56 +++++++++++++++---- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 850c9de1f8b..4107926333a 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -3153,37 +3153,44 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("Cannot get interest rate for this token (DUSD)!"); auto subInterest = TotalInterest(*rate, height); + CAmount subLoanAmount{0}; + CAmount subCollateralAmount{0}; // Edge case where interest is greater than collateral if (subInterest > collateralDUSD) { - res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first, collateralDUSD}); + subCollateralAmount = collateralDUSD; + + res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first, subCollateralAmount}); if (!res) return res; - res = mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, collateralDUSD); + res = mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, subCollateralAmount); if (!res) return res; } else { - CTokenAmount subLoanAmount; - CTokenAmount subCollateralAmount; + if (loanDUSD + subInterest > collateralDUSD) { - subLoanAmount = {dUsdToken->first, collateralDUSD - subInterest}; - subCollateralAmount = {dUsdToken->first, collateralDUSD}; + subLoanAmount = collateralDUSD - subInterest; + subCollateralAmount = collateralDUSD; } else { - subLoanAmount = {dUsdToken->first, loanDUSD}; - subCollateralAmount = {dUsdToken->first, loanDUSD + subInterest}; + subLoanAmount = loanDUSD; + subCollateralAmount = loanDUSD + subInterest; } - res = mnview.SubLoanToken(obj.vaultId, subLoanAmount); + res = mnview.SubLoanToken(obj.vaultId, {dUsdToken->first, subLoanAmount}); if (!res) return res; - res = mnview.SubVaultCollateral(obj.vaultId, subCollateralAmount); + res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first,subCollateralAmount}); if (!res) return res; mnview.ResetInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first); } + res = mnview.SubMintedTokens(dUsdToken->first, subCollateralAmount); + if (!res) + return res; + // Guard against liquidation const auto collaterals = mnview.GetVaultCollaterals(obj.vaultId); const auto loans = mnview.GetLoanTokens(obj.vaultId); diff --git a/test/functional/feature_loan_payback_with_collateral.py b/test/functional/feature_loan_payback_with_collateral.py index 7896f474e5e..025d4aacfc0 100755 --- a/test/functional/feature_loan_payback_with_collateral.py +++ b/test/functional/feature_loan_payback_with_collateral.py @@ -8,7 +8,7 @@ from test_framework.test_framework import DefiTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error -from decimal import ROUND_UP, Decimal +from decimal import ROUND_UP, ROUND_DOWN, Decimal import calendar import time @@ -204,7 +204,6 @@ def test_collaterals_greater_than_loans(self): self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) @@ -212,6 +211,8 @@ def test_collaterals_greater_than_loans(self): self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + vaultBefore = self.nodes[0].getvault(vaultId) [collateralAmountBefore, _] = vaultBefore["collateralAmounts"][1].split("@") [loanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") @@ -230,6 +231,9 @@ def test_collaterals_greater_than_loans(self): assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, mintedAmount + Decimal(loanAmountBefore)) + self.rollback_to(height) def test_loans_greater_than_collaterals(self): @@ -253,6 +257,8 @@ def test_loans_greater_than_collaterals(self): self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000010464231354642313546") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -273,13 +279,18 @@ def test_loans_greater_than_collaterals(self): assert_equal(storedInterest["interestPerBlock"], "0.000000951293859113394216") assert_equal(Decimal(interestAmount), Decimal(storedInterest["interestPerBlock"]).quantize(Decimal('1E-8'), rounding=ROUND_UP)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, mintedAmount + collateralDUSDAmount) + self.rollback_to(height) def test_loans_equal_to_collaterals(self): height = self.nodes[0].getblockcount() - collateralDUSDAmount = 1000 + expected_IPB = 0.00000952 loanDUSDAmount = 1000 + collateralDUSDAmount = loanDUSDAmount + expected_IPB vault_address = self.nodes[0].getnewaddress() @@ -289,14 +300,15 @@ def test_loans_equal_to_collaterals(self): self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - expected_IPB = 0.00000952 - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount + expected_IPB) + "@" + self.symbolDUSD) # Deposit enough to match amount of loans + interest after one block + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of loans + interest after one block self.nodes[0].generate(1) # Take DUSD loan self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -313,6 +325,9 @@ def test_loans_equal_to_collaterals(self): assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, Decimal(mintedAmount + Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_DOWN)) + self.rollback_to(height) def test_interest_greater_than_collaterals(self): @@ -335,13 +350,12 @@ def test_interest_greater_than_collaterals(self): self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - vaultBefore = self.nodes[0].getvault(vaultId) - [interestAmountBefore, _] = vaultBefore["interestAmounts"][0].split("@") - self.nodes[0].paybackwithcollateral(vaultId) self.nodes[0].generate(1) @@ -355,13 +369,17 @@ def test_interest_greater_than_collaterals(self): assert_equal(Decimal(interestAmount), Decimal(Decimal(storedInterest["interestPerBlock"]) * 2 - Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_UP)) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, mintedAmount + Decimal(collateralDUSDAmount)) + self.rollback_to(height) def test_interest_equal_to_collaterals(self): height = self.nodes[0].getblockcount() - collateralDUSDAmount = 0 + expected_IPB = 0.00000952 loanDUSDAmount = 1000 + collateralDUSDAmount = expected_IPB vault_address = self.nodes[0].getnewaddress() @@ -371,14 +389,16 @@ def test_interest_equal_to_collaterals(self): self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - expected_IPB = 0.00000952 - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount + expected_IPB) + "@" + self.symbolDUSD) # Deposit enough to match amount of interest after one block + + self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of interest after one block self.nodes[0].generate(1) # Take DUSD loan self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -398,6 +418,9 @@ def test_interest_equal_to_collaterals(self): assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, Decimal(mintedAmount + Decimal(collateralDUSDAmount)).quantize(Decimal('1E-8'), rounding=ROUND_DOWN)) + self.rollback_to(height) def test_negative_interest_collaterals_greater_than_loans(self): @@ -426,8 +449,11 @@ def test_negative_interest_collaterals_greater_than_loans(self): # accrue negative interest self.nodes[0].generate(5) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + vault = self.nodes[0].getvault(vaultId) [DUSDInterestAmount, _] = vault["interestAmounts"][0].split("@") + [DUSDloanAmount, _] = vault["loanAmounts"][0].split("@") assert(Decimal(DUSDInterestAmount) < 0) storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) @@ -449,6 +475,9 @@ def test_negative_interest_collaterals_greater_than_loans(self): assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("0")) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, mintedAmount + Decimal(DUSDloanAmount)) + self.rollback_to(height) def test_negative_interest_loans_greater_than_collaterals(self): @@ -477,6 +506,8 @@ def test_negative_interest_loans_greater_than_collaterals(self): # accrue negative interest self.nodes[0].generate(5) + mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + vaultBefore = self.nodes[0].getvault(vaultId) [DUSDLoanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") [DUSDInterestAmount, _] = vaultBefore["interestAmounts"][0].split("@") @@ -499,6 +530,9 @@ def test_negative_interest_loans_greater_than_collaterals(self): assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("-0.000474546864344558599695")) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) + mintedAmount = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] + assert_equal(mintedAmountBefore, mintedAmount + collateralDUSDAmount) + self.rollback_to(height) def run_test(self): From ac3db4a8e6407e8cdafdf7562fe0b1af9cbfc72e Mon Sep 17 00:00:00 2001 From: jouzo Date: Fri, 9 Sep 2022 15:11:27 +0100 Subject: [PATCH 5/6] Add special case to payback with collateral via loanpayback --- src/masternodes/mn_checks.cpp | 202 +++++++++-------- src/masternodes/mn_checks.h | 2 + .../feature_loan_payback_with_collateral.py | 203 ++++++++---------- 3 files changed, 200 insertions(+), 207 deletions(-) diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 4107926333a..79a497037d8 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -3102,20 +3102,8 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!res) return res; - const auto attributes = mnview.GetAttributes(); - if (!attributes) - return Res::Err("Attributes unavailable"); - - auto dUsdToken = mnview.GetToken("DUSD"); - if (!dUsdToken) - return Res::Err("Cannot find token DUSD"); - - CDataStructureV0 activeKey{AttributeTypes::Token, dUsdToken->first.v, TokenKeys::LoanPaybackCollateral}; - if (!attributes->GetValue(activeKey, false)) - return Res::Err("Payback of DUSD loan with collateral is not currently active"); - // vault exists - auto vault = mnview.GetVault(obj.vaultId); + const auto vault = mnview.GetVault(obj.vaultId); if (!vault) return Res::Err("Vault <%s> not found", obj.vaultId.GetHex()); @@ -3127,85 +3115,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!HasAuth(vault->ownerAddress)) return Res::Err("tx must have at least one input from token owner"); - if (!IsVaultPriceValid(mnview, obj.vaultId, height)) - return Res::Err("Cannot payback vault with collateral while any of the asset's price is invalid"); - - const auto collateralAmounts = mnview.GetVaultCollaterals(obj.vaultId); - if (!collateralAmounts) { - return Res::Err("Vault has no collaterals"); - } - if (!collateralAmounts->balances.count(dUsdToken->first)) { - return Res::Err("Vault does not have any DUSD collaterals"); - } - const auto& collateralDUSD = collateralAmounts->balances.at(dUsdToken->first); - - const auto loanAmounts = mnview.GetLoanTokens(obj.vaultId); - if (!loanAmounts) { - return Res::Err("Vault has no loans"); - } - if (!loanAmounts->balances.count(dUsdToken->first)) { - return Res::Err("Vault does not have any DUSD loans"); - } - const auto& loanDUSD = loanAmounts->balances.at(dUsdToken->first); - - auto rate = mnview.GetInterestRate(obj.vaultId, dUsdToken->first, height); - if (!rate) - return Res::Err("Cannot get interest rate for this token (DUSD)!"); - auto subInterest = TotalInterest(*rate, height); - - CAmount subLoanAmount{0}; - CAmount subCollateralAmount{0}; - // Edge case where interest is greater than collateral - if (subInterest > collateralDUSD) { - subCollateralAmount = collateralDUSD; - - res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first, subCollateralAmount}); - if (!res) - return res; - - res = mnview.DecreaseInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first, 0, subCollateralAmount); - if (!res) - return res; - } else { - - if (loanDUSD + subInterest > collateralDUSD) { - subLoanAmount = collateralDUSD - subInterest; - subCollateralAmount = collateralDUSD; - } else { - subLoanAmount = loanDUSD; - subCollateralAmount = loanDUSD + subInterest; - } - - res = mnview.SubLoanToken(obj.vaultId, {dUsdToken->first, subLoanAmount}); - if (!res) - return res; - - res = mnview.SubVaultCollateral(obj.vaultId, {dUsdToken->first,subCollateralAmount}); - if (!res) - return res; - - mnview.ResetInterest(height, obj.vaultId, vault->schemeId, dUsdToken->first); - } - - res = mnview.SubMintedTokens(dUsdToken->first, subCollateralAmount); - if (!res) - return res; - - // Guard against liquidation - const auto collaterals = mnview.GetVaultCollaterals(obj.vaultId); - const auto loans = mnview.GetLoanTokens(obj.vaultId); - if (!collaterals && loans) - return Res::Err("Vault cannot have loans without collaterals"); - - auto collateralsLoans = mnview.GetLoanCollaterals(obj.vaultId, *collaterals, height, time); - if (!collateralsLoans) - return std::move(collateralsLoans); - - const auto scheme = mnview.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 Res::Ok(); + return PaybackWithCollateral(mnview, *vault, obj.vaultId, height, time); } Res operator()(const CLoanTakeLoanMessage& obj) const { @@ -3400,8 +3310,9 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (vault->isUnderLiquidation) return Res::Err("Cannot payback loan on vault under liquidation"); - if (!mnview.GetVaultCollaterals(obj.vaultId)) + if (!mnview.GetVaultCollaterals(obj.vaultId)) { return Res::Err("Vault with id %s has no collaterals", obj.vaultId.GetHex()); + } if (!HasAuth(obj.from)) return Res::Err("tx must have at least one input from token owner"); @@ -3409,6 +3320,12 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor 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"); + // Handle payback with collateral special case + if (static_cast(height) >= consensus.FortCanningEpilogueHeight + && IsPaybackWithCollateral(mnview, obj.loans)) { + return PaybackWithCollateral(mnview, *vault, obj.vaultId, height, time); + } + auto shouldSetVariable = false; auto attributes = mnview.GetAttributes(); @@ -4476,6 +4393,105 @@ bool IsVaultPriceValid(CCustomCSView& mnview, const CVaultId& vaultId, uint32_t return true; } +bool IsPaybackWithCollateral(CCustomCSView& view, const std::map& loans) { + auto tokenDUSD = view.GetToken("DUSD"); + if (!tokenDUSD) + return false; + + if (loans.size() == 1 + && loans.count(tokenDUSD->first) + && loans.at(tokenDUSD->first) == CBalances{{{tokenDUSD->first, 999999999999999999LL}}}) { + return true; + } + return false; +} + +Res PaybackWithCollateral(CCustomCSView& view, const CVaultData& vault, const CVaultId& vaultId, uint32_t height, uint64_t time) { + const auto attributes = view.GetAttributes(); + if (!attributes) + return Res::Err("Attributes unavailable"); + + auto dUsdToken = view.GetToken("DUSD"); + if (!dUsdToken) + return Res::Err("Cannot find token DUSD"); + + CDataStructureV0 activeKey{AttributeTypes::Token, dUsdToken->first.v, TokenKeys::LoanPaybackCollateral}; + if (!attributes->GetValue(activeKey, false)) + return Res::Err("Payback of DUSD loan with collateral is not currently active"); + + const auto collateralAmounts = view.GetVaultCollaterals(vaultId); + if (!collateralAmounts) { + return Res::Err("Vault has no collaterals"); + } + if (!collateralAmounts->balances.count(dUsdToken->first)) { + return Res::Err("Vault does not have any DUSD collaterals"); + } + const auto& collateralDUSD = collateralAmounts->balances.at(dUsdToken->first); + + const auto loanAmounts = view.GetLoanTokens(vaultId); + if (!loanAmounts) { + return Res::Err("Vault has no loans"); + } + if (!loanAmounts->balances.count(dUsdToken->first)) { + return Res::Err("Vault does not have any DUSD loans"); + } + const auto& loanDUSD = loanAmounts->balances.at(dUsdToken->first); + + 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); + + Res res{}; + CAmount subLoanAmount{0}; + CAmount subCollateralAmount{0}; + // Edge case where interest is greater than collateral + if (subInterest > collateralDUSD) { + subCollateralAmount = collateralDUSD; + + res = view.SubVaultCollateral(vaultId, {dUsdToken->first, subCollateralAmount}); + if (!res) + return res; + + res = view.DecreaseInterest(height, vaultId, vault.schemeId, dUsdToken->first, 0, subCollateralAmount); + if (!res) + return res; + } else { + if (loanDUSD + subInterest > collateralDUSD) { + subLoanAmount = collateralDUSD - subInterest; + subCollateralAmount = collateralDUSD; + } else { + subLoanAmount = loanDUSD; + subCollateralAmount = loanDUSD + subInterest; + } + + res = view.SubLoanToken(vaultId, {dUsdToken->first, subLoanAmount}); + if (!res) + return res; + + res = view.SubVaultCollateral(vaultId, {dUsdToken->first,subCollateralAmount}); + if (!res) + return res; + + view.ResetInterest(height, vaultId, vault.schemeId, dUsdToken->first); + } + + // Guard against liquidation + const auto collaterals = view.GetVaultCollaterals(vaultId); + const auto loans = view.GetLoanTokens(vaultId); + if (!collaterals && loans) + return Res::Err("Vault cannot have loans without collaterals"); + + auto collateralsLoans = view.GetLoanCollaterals(vaultId, *collaterals, height, time); + if (!collateralsLoans) + return std::move(collateralsLoans); + + 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); +} Res storeGovVars(const CGovernanceHeightMessage& obj, CCustomCSView& view) { diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index d50ea10bac5..911ff07dc41 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -396,6 +396,8 @@ ResVal ApplyAnchorRewardTx(CCustomCSView& mnview, const CTransaction& t ResVal ApplyAnchorRewardTxPlus(CCustomCSView& mnview, const CTransaction& tx, int height, const std::vector& metadata, const Consensus::Params& consensusParams); ResVal GetAggregatePrice(CCustomCSView& view, const std::string& token, const std::string& currency, uint64_t lastBlockTime); bool IsVaultPriceValid(CCustomCSView& mnview, const CVaultId& vaultId, uint32_t height); +bool IsPaybackWithCollateral(CCustomCSView& mnview, const std::map& loans); +Res PaybackWithCollateral(CCustomCSView& view, const CVaultData& vault, const CVaultId& vaultId, uint32_t height, uint64_t time); Res SwapToDFIorDUSD(CCustomCSView & mnview, DCT_ID tokenId, CAmount amount, CScript const & from, CScript const & to, uint32_t height, bool forceLoanSwap = false); Res storeGovVars(const CGovernanceHeightMessage& obj, CCustomCSView& view); diff --git a/test/functional/feature_loan_payback_with_collateral.py b/test/functional/feature_loan_payback_with_collateral.py index 025d4aacfc0..2af54a8d9fa 100755 --- a/test/functional/feature_loan_payback_with_collateral.py +++ b/test/functional/feature_loan_payback_with_collateral.py @@ -157,77 +157,74 @@ def setup(self): self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'true'}}) self.nodes[0].generate(1) - def test_guards(self): - height = self.nodes[0].getblockcount() + vault_address = self.nodes[0].getnewaddress() - vaultId = self.nodes[0].createvault(self.account0, 'LOAN001') + self.vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') self.nodes[0].generate(1) + def test_guards(self): + height = self.nodes[0].getblockcount() + self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'false'}}) self.nodes[0].generate(1) - assert_raises_rpc_error(-32600, "Payback of DUSD loan with collateral is not currently active", self.nodes[0].paybackwithcollateral, vaultId) + assert_raises_rpc_error(-32600, "Payback of DUSD loan with collateral is not currently active", self.nodes[0].paybackwithcollateral, self.vaultId) self.nodes[0].setgov({"ATTRIBUTES":{'v0/token/' + self.idDUSD + '/loan_payback_collateral':'true'}}) self.nodes[0].generate(1) - assert_raises_rpc_error(-32600, "Vault has no collaterals", self.nodes[0].paybackwithcollateral, vaultId) + assert_raises_rpc_error(-32600, "Vault has no collaterals", self.nodes[0].paybackwithcollateral, self.vaultId) # Deposit DUSD and DFI to vault - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - assert_raises_rpc_error(-32600, "Vault does not have any DUSD collaterals", self.nodes[0].paybackwithcollateral, vaultId) + assert_raises_rpc_error(-32600, "Vault does not have any DUSD collaterals", self.nodes[0].paybackwithcollateral, self.vaultId) - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDUSD) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) - assert_raises_rpc_error(-32600, "Vault has no loans", self.nodes[0].paybackwithcollateral, vaultId) + assert_raises_rpc_error(-32600, "Vault has no loans", self.nodes[0].paybackwithcollateral, self.vaultId) # take TSLA loan - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": "1@" + self.symbolTSLA }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": "1@" + self.symbolTSLA }) self.nodes[0].generate(1) - assert_raises_rpc_error(-32600, "Vault does not have any DUSD loans", self.nodes[0].paybackwithcollateral, vaultId) + assert_raises_rpc_error(-32600, "Vault does not have any DUSD loans", self.nodes[0].paybackwithcollateral, self.vaultId) self.rollback_to(height) - def test_collaterals_greater_than_loans(self): + def test_collaterals_greater_than_loans(self, payback_with_collateral): height = self.nodes[0].getblockcount() collateralDUSDAmount = 2000 loanDUSDAmount = 1000 - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) # Take DUSD loan - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - vaultBefore = self.nodes[0].getvault(vaultId) + vaultBefore = self.nodes[0].getvault(self.vaultId) [collateralAmountBefore, _] = vaultBefore["collateralAmounts"][1].split("@") [loanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) [collateralAmount, _] = vault["collateralAmounts"][1].split("@") assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Payback all DUSD loans assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Payback all DUSD interests assert_equal(Decimal(collateralAmount), Decimal(collateralAmountBefore) - Decimal(loanAmountBefore)) - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -236,46 +233,41 @@ def test_collaterals_greater_than_loans(self): self.rollback_to(height) - def test_loans_greater_than_collaterals(self): + def test_loans_greater_than_collaterals(self, payback_with_collateral): height = self.nodes[0].getblockcount() collateralDUSDAmount = 1000 loanDUSDAmount = 1100 - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) # Take DUSD loan - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000010464231354642313546") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - vaultBefore = self.nodes[0].getvault(vaultId) + vaultBefore = self.nodes[0].getvault(self.vaultId) [loanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) [interestAmount, _] = vault["interestAmounts"][0].split("@") [loanAmount, _] = vault["loanAmounts"][0].split("@") assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral assert_equal(Decimal(loanAmount), Decimal(loanAmountBefore) - Decimal(collateralDUSDAmount) + Decimal(interestAmount)) - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000000951293859113394216") assert_equal(Decimal(interestAmount), Decimal(storedInterest["interestPerBlock"]).quantize(Decimal('1E-8'), rounding=ROUND_UP)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -285,43 +277,38 @@ def test_loans_greater_than_collaterals(self): self.rollback_to(height) - def test_loans_equal_to_collaterals(self): + def test_loans_equal_to_collaterals(self, payback_with_collateral): height = self.nodes[0].getblockcount() expected_IPB = 0.00000952 loanDUSDAmount = 1000 collateralDUSDAmount = loanDUSDAmount + expected_IPB - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of loans + interest after one block + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of loans + interest after one block self.nodes[0].generate(1) # Take DUSD loan - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Payback all DUSD loans assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Payback all DUSD interests assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal(0)) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -330,40 +317,35 @@ def test_loans_equal_to_collaterals(self): self.rollback_to(height) - def test_interest_greater_than_collaterals(self): + def test_interest_greater_than_collaterals(self, payback_with_collateral): height = self.nodes[0].getblockcount() collateralDUSDAmount = 0.000001 loanDUSDAmount = 1000 - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) - self.nodes[0].generate(1) - - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) [interestAmount, _] = vault["interestAmounts"][0].split("@") assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0.000008512937595129375951")) @@ -374,47 +356,42 @@ def test_interest_greater_than_collaterals(self): self.rollback_to(height) - def test_interest_equal_to_collaterals(self): + def test_interest_equal_to_collaterals(self, payback_with_collateral): height = self.nodes[0].getblockcount() expected_IPB = 0.00000952 loanDUSDAmount = 1000 collateralDUSDAmount = expected_IPB - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of interest after one block + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) # Deposit enough to match amount of interest after one block self.nodes[0].generate(1) # Take DUSD loan - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - vaultBefore = self.nodes[0].getvault(vaultId) + vaultBefore = self.nodes[0].getvault(self.vaultId) - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral assert_equal(vault["interestAmounts"], vaultBefore["interestAmounts"]) assert_equal(vault["loanAmounts"], vaultBefore["loanAmounts"]) assert_equal(vault["collateralValue"], float(vaultBefore["collateralValue"]) - expected_IPB) - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "0.000009512937595129375951") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) @@ -423,27 +400,22 @@ def test_interest_equal_to_collaterals(self): self.rollback_to(height) - def test_negative_interest_collaterals_greater_than_loans(self): + def test_negative_interest_collaterals_greater_than_loans(self, payback_with_collateral): height = self.nodes[0].getblockcount() collateralDUSDAmount = 1000 loanDUSDAmount = 1000 - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_minting_interest':'-500'}}) self.nodes[0].generate(1) - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) # accrue negative interest @@ -451,19 +423,19 @@ def test_negative_interest_collaterals_greater_than_loans(self): mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) [DUSDInterestAmount, _] = vault["interestAmounts"][0].split("@") [DUSDloanAmount, _] = vault["loanAmounts"][0].split("@") assert(Decimal(DUSDInterestAmount) < 0) - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "-0.004746955859969558599695") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) # collateral amount should be equal to the opposite of DUSDInterestAmount [DUSDCollateralAmount, _] = vault["collateralAmounts"][1].split("@") assert_equal(Decimal(DUSDCollateralAmount), Decimal(DUSDInterestAmount) * -1) @@ -471,7 +443,7 @@ def test_negative_interest_collaterals_greater_than_loans(self): assert(not any("DUSD" in loan for loan in vault["loanAmounts"])) # Paid back all DUSD loans assert(not any("DUSD" in interest for interest in vault["interestAmounts"])) # Paid back all DUSD interests - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("0")) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) @@ -480,27 +452,22 @@ def test_negative_interest_collaterals_greater_than_loans(self): self.rollback_to(height) - def test_negative_interest_loans_greater_than_collaterals(self): + def test_negative_interest_loans_greater_than_collaterals(self, payback_with_collateral): height = self.nodes[0].getblockcount() collateralDUSDAmount = 1000 loanDUSDAmount = 1100 - vault_address = self.nodes[0].getnewaddress() - - vaultId = self.nodes[0].createvault(vault_address, 'LOAN001') + self.nodes[0].deposittovault(self.vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) self.nodes[0].generate(1) - self.nodes[0].deposittovault(vaultId, self.account0, str(self.collateralAmount) + "@" + self.symbolDFI) - self.nodes[0].generate(1) - - self.nodes[0].deposittovault(vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) + self.nodes[0].deposittovault(self.vaultId, self.account0, str(collateralDUSDAmount) + "@" + self.symbolDUSD) self.nodes[0].generate(1) self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_minting_interest':'-500'}}) self.nodes[0].generate(1) - self.nodes[0].takeloan({ "vaultId": vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) + self.nodes[0].takeloan({ "vaultId": self.vaultId, "amounts": str(loanDUSDAmount) + "@" + self.symbolDUSD }) self.nodes[0].generate(1) # accrue negative interest @@ -508,25 +475,25 @@ def test_negative_interest_loans_greater_than_collaterals(self): mintedAmountBefore = self.nodes[0].gettoken(self.symbolDUSD)[self.idDUSD]["minted"] - vaultBefore = self.nodes[0].getvault(vaultId) + vaultBefore = self.nodes[0].getvault(self.vaultId) [DUSDLoanAmountBefore, _] = vaultBefore["loanAmounts"][0].split("@") [DUSDInterestAmount, _] = vaultBefore["interestAmounts"][0].split("@") assert(Decimal(DUSDInterestAmount) < 0) - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(storedInterest["interestPerBlock"], "-0.005221651445966514459665") assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal(0)) - self.nodes[0].paybackwithcollateral(vaultId) + payback_with_collateral() self.nodes[0].generate(1) - vault = self.nodes[0].getvault(vaultId) + vault = self.nodes[0].getvault(self.vaultId) [DUSDLoanAmount, _] = vault["loanAmounts"][0].split("@") [DUSDInterestAmount, _] = vault["interestAmounts"][0].split("@") assert_equal(Decimal(DUSDLoanAmount), Decimal(DUSDLoanAmountBefore) - Decimal(collateralDUSDAmount) + Decimal(DUSDInterestAmount)) assert(not any("DUSD" in collateral for collateral in vault["collateralAmounts"])) # Used all DUSD collateral - storedInterest = self.nodes[0].getstoredinterest(vaultId, self.idDUSD) + storedInterest = self.nodes[0].getstoredinterest(self.vaultId, self.idDUSD) assert_equal(Decimal(storedInterest["interestPerBlock"]), Decimal("-0.000474546864344558599695")) assert_equal(Decimal(storedInterest["interestToHeight"]), Decimal("0")) @@ -536,24 +503,32 @@ def test_negative_interest_loans_greater_than_collaterals(self): self.rollback_to(height) def run_test(self): - self.setup() self.test_guards() - self.test_collaterals_greater_than_loans() + payback_fns = [ + lambda: self.nodes[0].paybackwithcollateral(self.vaultId), + lambda: self.nodes[0].paybackloan({ + 'vaultId': self.vaultId, + 'from': self.account0, + 'amounts': "9999999999.99999999@" + self.symbolDUSD}) + ] + + for payback_fn in payback_fns: + self.test_collaterals_greater_than_loans(payback_fn) - self.test_loans_greater_than_collaterals() + self.test_loans_greater_than_collaterals(payback_fn) - self.test_loans_equal_to_collaterals() + self.test_loans_equal_to_collaterals(payback_fn) - self.test_interest_greater_than_collaterals() + self.test_interest_greater_than_collaterals(payback_fn) - self.test_interest_equal_to_collaterals() + self.test_interest_equal_to_collaterals(payback_fn) - self.test_negative_interest_collaterals_greater_than_loans() + self.test_negative_interest_collaterals_greater_than_loans(payback_fn) - self.test_negative_interest_loans_greater_than_collaterals() + self.test_negative_interest_loans_greater_than_collaterals(payback_fn) if __name__ == '__main__': LoanPaybackWithCollateralTest().main() From bfc3c8fc4795ff5b325225a448e3fa46790c9671 Mon Sep 17 00:00:00 2001 From: jouzo Date: Sat, 10 Sep 2022 12:58:05 +0100 Subject: [PATCH 6/6] Test guard against liquidation --- .../feature_loan_payback_with_collateral.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/test/functional/feature_loan_payback_with_collateral.py b/test/functional/feature_loan_payback_with_collateral.py index 2af54a8d9fa..157710bb02c 100755 --- a/test/functional/feature_loan_payback_with_collateral.py +++ b/test/functional/feature_loan_payback_with_collateral.py @@ -133,6 +133,10 @@ def setup(self): }) self.nodes[0].generate(1) + # Create loan scheme + self.nodes[0].createloanscheme(150, 1, 'LOAN001') + self.nodes[0].generate(1) + # Set collateral tokens self.nodes[0].setcollateraltoken({ 'token': self.symbolDFI, @@ -146,9 +150,6 @@ def setup(self): }) self.nodes[0].generate(120) - # Create loan scheme - self.nodes[0].createloanscheme(150, 1, 'LOAN001') - self.nodes[0].generate(1) self.nodes[0].accounttoaccount(self.mn_address, {self.account0: str(self.collateralAmount) + "@" + self.symbolDUSD}) self.nodes[0].accounttoaccount(self.mn_address, {self.account0: str(self.collateralAmount) + "@" + self.symbolDFI}) @@ -192,6 +193,42 @@ def test_guards(self): self.rollback_to(height) + def test_guard_against_liquidation(self): + height = self.nodes[0].getblockcount() + + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_collateral_factor': '1.49'}}) + self.nodes[0].generate(1) + + # Check results + attributes = self.nodes[0].getgov('ATTRIBUTES')['ATTRIBUTES'] + assert_equal(attributes['v0/token/1/loan_collateral_factor'], '1.49') + + # Deposit DUSD and DFI to vault + self.nodes[0].deposittovault(self.vaultId, self.account0, "151@" + self.symbolDFI) + self.nodes[0].generate(1) + + 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) + + self.nodes[0].setgov({"ATTRIBUTES":{f'v0/token/{self.idDUSD}/loan_minting_interest':'50000000'}}) + self.nodes[0].generate(2) # accrue enough interest to drop below collateralization ratio + + assert_raises_rpc_error(-32600, "Vault does not have enough collateralization ratio defined by loan scheme - 143 < 150", self.nodes[0].paybackwithcollateral, self.vaultId) + + self.rollback_to(height) + def test_collaterals_greater_than_loans(self, payback_with_collateral): height = self.nodes[0].getblockcount() @@ -507,6 +544,8 @@ def run_test(self): self.test_guards() + self.test_guard_against_liquidation() + payback_fns = [ lambda: self.nodes[0].paybackwithcollateral(self.vaultId), lambda: self.nodes[0].paybackloan({ @@ -516,6 +555,7 @@ def run_test(self): ] for payback_fn in payback_fns: + self.test_collaterals_greater_than_loans(payback_fn) self.test_loans_greater_than_collaterals(payback_fn)